Every notification,
one API.

Nofy is the notification layer for your product. Send one request, and Nofy delivers it on every channel, retries when a provider has a bad minute, and tells you what happened to every recipient.

Delivers by

POST /v2/messages
Idempotency-Key: shift-8841-sms

{
  "tenant": "company-uuid",
  "event_type": "shift.closed",
  "reference": "shift-8841",
  "messages": [{
    "channel": "sms",
    "recipients": ["255700000000"],
    "content": {
      "kind": "rendered",
      "body": "Shift closed at 18:00."
    }
  }]
}

Why Nofy

Stop building notifications into every product

Every product ends up with the same plumbing: an SMS gateway, a WhatsApp Business account, SMTP, Firebase. Each one has its own credentials, errors and retry rules. Then a customer asks whether the message arrived, and nobody can say.

Nofy handles all of that once, for all your products. Your code sends one request. Nofy delivers it and keeps a record you can check.

Channels

Four ways out. One way back.

Every message goes out through the channel you name, using the same request format. Every result comes back to you the same way, as a signed webhook.

Email

via SMTP
To
An email address
Content
Subject and text, with optional HTML and inline images
Status
Sent once the mail server accepts it, or failed with the reason

WhatsApp

via Cloud API
To
A phone number
Content
A template approved by Meta, plus its parameters
Status
Sent or failed, then delivered and read as the phone reports them

Push

via Firebase
To
Your own user ID. Nofy reaches all of that user's active phones
Content
Title and text, with optional data for your app
Status
Sent or failed. Dead device tokens are switched off automatically

SMS

via Beem
To
A phone number, with or without the leading +
Content
Plain text, sent from your approved sender ID
Status
Sent or failed, with the provider's reason when it refuses

Webhooks

Add a callback_url to any send and Nofy posts each delivery's result to your server as soon as it's final, whatever the channel.

  • Signed. Every post carries an X-ENS-Signature header: an HMAC-SHA256 of the body, using a secret issued only to you.
  • Retried. If your endpoint is down, the post is tried again.
  • Backed up. Anything you still missed is in GET /v2/deliveries?updated_since=…
Read the webhooks guide →

POST https://api.acme.co.tz/nofy/status

Content-Type: application/json
X-ENS-Signature: 9f2c41…e07b

{
  "delivery_id": "dL91xTo8",
  "reference": "shift-8841",
  "channel": "sms",
  "recipient": "255700000000",
  "status": "sent",
  "attempts": 1,
  "provider_message_id": "beem-req-42",
  "sent_at": "2026-09-25T18:00:03Z",
  "last_error": null
}

How it works

Four API calls from credentials to delivery

Your operator issues the OAuth credentials. Everything after that is self-service. Pick a step to see the request and what comes back.

Request Step 1 of 4

POST /v1/oauth2/token/
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Response 200 OK

{
  "access_token": "abc123",
  "expires_in": 36000,
  "token_type": "Bearer"
}

Reliability

Know whether it arrived

Each recipient gets its own delivery record with its status, attempt count and the provider's message ID. When a customer asks what happened, you can answer.

Automatic retries

Temporary failures are retried up to 5 times, waiting 1, 2, 4 and then 8 minutes between tries. Hitting a rate limit never uses up an attempt.

Permanent failures stop immediately

A wrong number, a paused template or a refused address fails straight away, and the delivery records the reason.

Callbacks you can verify

Every status callback is signed with HMAC-SHA256 using a secret issued to you, so you can reject anything forged.

Outage alerts

If a whole channel starts failing, for example an expired token or an empty SMS balance, your team is emailed. A second email confirms when it recovers.

WhatsApp · +255 700 000 000 ref shift-8841
  1. Accepted202 returned with the idempotency key recorded
  2. SentWhatsApp accepted it; signed callback posted to you
  3. DeliveredReached the recipient's phone
  4. ReadRecipient opened the message

Callbacks can miss if your server is down. GET /v2/deliveries?updated_since=… always has the full record.

Built for multi-tenant products

One customer's spike won't block the rest

Tenants are your customers, identified by your own IDs. Nofy uses them to apply limits and settings to each customer separately.

Daily quotas for each tenant

A customer who goes over their allowance gets 429 responses while every other customer keeps sending. A separate daily cap covers your whole application.

Bursts are slowed, not dropped

Each provider account has a per-minute limit. When a burst goes over it, messages wait their turn instead of failing.

Your data stays yours

Sending to an unregistered tenant is rejected, so a typo can't create a new one. Each API client sees only its own deliveries.

pip install ens_sdk
from ens_sdk import ENS

# Reads ENS_SERVER_URL, ENS_CLIENT_ID,
# ENS_CLIENT_SECRET and ENS_TENANT
ens = ENS()

ens.send_email(
    "ops@acme.co.tz", "Shift closed", "Shift 7 closed.")
ens.send_whatsapp_template(
    "+255700000000", "otp_login", "en", ["482916"])
ens.send_push(
    "user-uuid-1", "Shift closed", "Tap for the summary.")

Developers

Plain HTTP, plus a Python SDK

The Python SDK handles tokens, idempotency keys and error types for you. Any other language can call the same HTTP API; the docs include working examples for each.

Python SDKNode.jsPHPGoRustcURL
  • Unknown request fields are rejected, so a misspelled callback_url fails loudly
  • Clear status codes: 400, 401, 403, 413, 429
  • Deliveries can be paged with a cursor, up to 500 per call

FAQ

Common questions

Anything else? Email support@encipher.co.tz.

Does a 202 mean my message was delivered?

No. 202 means Nofy has accepted and queued it. The response lists one delivery per recipient, and each one updates to sent or failed. WhatsApp also reports when a message is delivered and read.

Can I retry a request that timed out?

Yes. Send it again with the same Idempotency-Key. You get the original response and nothing is sent twice. Keys are scoped to your client, so they can't clash with another application's.

What if my callback endpoint is down?

Callbacks are retried, but treat them as a speed-up rather than your only record. Call GET /v2/deliveries?updated_since=… to catch up on anything you missed.

Can I send free-form WhatsApp messages?

No. WhatsApp business messages must use templates Meta has approved. Nofy rejects free-form bodies with a 400 before anything is stored, rather than letting Meta reject them later.

How do I get credentials?

Your Nofy operator issues an OAuth client ID and secret. Keep the secret on your servers only; never put it in a mobile app or browser code.

Send your first notification today

The quickstart takes about five minutes. We can help with credentials, access or outages.