← Noti5 · API reference · v1

API reference.

A single REST API for OTPs, transactional email, and WhatsApp notifications. Closed beta; access is by invitation. Base URL: https://api.noti5.app/v1.

Introduction

Every request is JSON over HTTPS. Authenticated with a site-scoped bearer token. All POSTs accept an idempotency key. Responses are spec-shape JSON; errors carry a stable code you can switch on.

Authentication

Pass your API key as a bearer token. Keys are generated in the dashboard under Sites → your site → API keys → Issue new key. Format: n5_live_… (production) or n5_test_… (test).

curl https://api.noti5.app/v1/site \
  -H "Authorization: Bearer n5_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Key rotation: issue a new key, switch your application to it, then revoke the old one from the dashboard. There's no downtime — multiple active keys per site are fine.

Errors

Every error is a JSON object under error:

{
  "error": {
    "code": "recipient_suppressed",
    "message": "This recipient is on the suppression list.",
    "request_id": "req_a3f29b…",
    "documentation_url": "https://docs.noti5.app/errors/recipient_suppressed"
  }
}
HTTPCommon codes
401unauthenticated, invalid_api_key, api_key_revoked
403site_suspended
404message_not_found, otp_not_found
409idempotency_conflict
410otp_expired, otp_already_consumed
422validation_failed, otp_invalid, recipient_suppressed
429rate_limited, resend_cooldown, otp_max_attempts
500server_error

Rate limits

Default: 60 requests per minute per API key. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining headers.

Idempotency

All POST endpoints accept an Idempotency-Key header. Identical key + identical body returns the original response within 24 hours. Different body with the same key returns 409 idempotency_conflict.

Sends

POST /v1/notify

Send a single email or WhatsApp message.

POST /v1/notify
{
  "event_type": "form.submission.ack",
  "channel": "email",
  "recipient": "enduser@example.com",
  "template_code": "form_submission_ack",
  "variables": {
    "user_name": "Priya",
    "brand_name": "Aban International School"
  },
  "metadata": { "form_id": "wpforms-123" }
}

Channels: email, whatsapp, auto (picks based on recipient shape). Response is 202 Accepted:

{
  "message": {
    "id": "msg_a3f29b1c4d5e6f7g",
    "status": "queued",
    "channel": "email",
    "recipient": "enduser@example.com",
    "queued_at": "2026-05-19T10:30:00Z"
  }
}
POST /v1/notify/batch

Send the same event to multiple recipients. Up to 1000 per call.

{
  "event_type": "form.submission.lead_notify",
  "channel": "whatsapp",
  "template_code": "form_submission_lead_notify",
  "recipients": [
    { "recipient": "+919876543210", "variables": { "team_member": "Sash" } },
    { "recipient": "+919876543211", "variables": { "team_member": "Sriram" } }
  ],
  "shared_variables": {
    "brand_name": "Aban International School",
    "lead_name": "Priya"
  }
}
GET /v1/messages

List messages. Query params: from, to, status, channel, event_type, recipient, limit (default 50, max 200), cursor.

GET /v1/messages/<id>

Retrieve a single message with its full event timeline.

OTP

POST /v1/otp/send

Generate and send a one-time password. The code itself is never returned — it's delivered to the recipient and verified separately.

{
  "purpose": "form_verification",
  "recipient": "+919876543210",
  "channel": "whatsapp",
  "fallback_channels": ["email"],
  "fallback_recipient_email": "user@example.com",
  "variables": { "brand_name": "Aban International School" },
  "ttl_seconds": 600,
  "code_length": 6
}

Returns 201 Created with otp.id, expires_at, max_attempts.

POST /v1/otp/verify
{ "otp_id": "otp_a3f29b1c4d5e6f7g", "code": "483921" }

On success: { "verified": true, "verified_at": "…" }. On failure: 422 with code = otp_invalid (with attempts_remaining) or 410 for otp_expired/otp_already_consumed, 429 for otp_max_attempts.

POST /v1/otp/resend

Re-issues a fresh code on the same OTP id. Cooldown: 30 seconds between resends — returns 429 resend_cooldown with a retry_after field if invoked too soon.

Site

GET /v1/site

Returns the resolved brand settings + list of templates available to this site. Used by the WP mu-plugin for self-discovery.

POST /v1/site/heartbeat

Mu-plugin pings periodically to refresh metadata (WP/PHP version, active plugins, etc.).

Privacy (DPDP)

POST /v1/privacy/delete

Spec §15.2 #4 — scrub body/variables for all messages sent to a recipient on this account. Logged in the audit trail.

{ "recipient": "user@example.com" }
GET /v1/privacy/export?recipient=user@example.com

Spec §15.2 #5 — returns all messages sent to a recipient on this account, with whatever body content hasn't yet been scrubbed by retention.

Outbound webhooks (Noti5 → your site)

Configure a webhook_url + webhook_secret on your Site to receive structured events when message status changes.

POST https://your-site.com/your-webhook
X-Noti5-Event: message.delivered
X-Noti5-Delivery-Id: whd_…
X-Noti5-Signature: sha256=<HMAC-SHA256(body, webhook_secret)>

{
  "event": "message.delivered",
  "occurred_at": "2026-05-19T10:30:15Z",
  "data": {
    "message_id": "msg_a3f29b…",
    "channel": "email",
    "event_type": "form.submission.ack",
    "recipient": "enduser@example.com",
    "status": "delivered"
  }
}

Expected response: 2xx within 10s. Otherwise retried with exponential backoff up to 8 attempts (~24h), then abandoned. Replay any failed delivery from the dashboard.

Questions? hello@noti5.app · abuse reports: abuse@noti5.app