Skip to content

Conventions ​

Everything on this page applies to every endpoint, and is not repeated on the resource pages.

Response envelope ​

Every JSON response uses the same shape:

json
{
  "success": true,
  "message": "Contacts retrieved",
  "status_code": 200,
  "error": null,
  "payload": { }
}

On failure, error is populated instead and payload is null:

json
{
  "success": false,
  "message": "Template is a draft and cannot be used to send",
  "status_code": 422,
  "error": {
    "code": "notifyzr.template.error.not_published",
    "message": "Template is a draft and cannot be used to send",
    "issues": []
  },
  "payload": null
}

Match on error.code. Codes are namespaced notifyzr.<domain>.error.<name> and are stable; the human-readable message is not. See the error code catalog.

issues carries per-field detail when a request fails schema validation.

Examples on the resource pages are unwrapped

Every JSON example on the resource pages shows only what is inside payload. In a real response it arrives wrapped in the envelope above.

Status codes ​

CodeMeaning
200Success
201Created
202Accepted — queued work: a send, a broadcast fan-out, a contact import
400Malformed request
401Missing, invalid, revoked or expired API key
403Authenticated, but not permitted
404Not found in your organization — which is also the answer when it exists in another one
415Unsupported content type (form submissions and contact imports)
422Well-formed but rejected by a business rule
429Rate limited

Why 404 and not 403

Cross-organization access returns 404, never 403. A 403 would confirm that the id exists somewhere, which is information about another organization.

202 is the norm for anything that sends. The API records the work and returns an id; a worker performs it. A 202 means accepted, never delivered — poll /v1/messages/{id} or subscribe to a webhook for the outcome.

Pagination ​

Paginated list endpoints take page and per_page. The default is 25 and the maximum 100; a value outside the range is clamped, not rejected.

bash
curl "http://api.localhost/v1/contacts?page=2&per_page=100" \
  -H "X-API-Key: $NOTIFYZR_API_KEY"

They return the rows alongside a pagination object:

json
{
  "contacts": [ ],
  "pagination": { "page": 2, "per_page": 100, "total": 4213, "total_pages": 43 }
}

Not every list is paginated — only the ones that grow without bound.

Paginated: /v1/contacts, /v1/messages, /v1/unsubscribes, /v1/contact-imports, /v1/forms/{id}/submissions, /v1/segments/{id}/contacts, /v1/automations/{id}/runs.

Returned whole: /v1/segments, /v1/templates, /v1/template-folders, /v1/forms, /v1/automations, /v1/broadcasts, /v1/channels, /v1/domains, /v1/webhooks, /v1/contact-properties. These are bounded by a plan quota, so page and per_page are ignored on them.

Dates and times ​

Timestamps are returned as ISO 8601 strings in UTC. Where a request takes a time — deliver_at on a send, expires_at on a key — send an ISO 8601 string with an offset. A date-only value in a segment filter is YYYY-MM-DD.

Rate limiting ​

Requests are rate-limited, and every response carries the current state:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the window
X-RateLimit-RemainingRequests left in it
X-RateLimit-ResetSeconds until the window resets

Exceeding the limit returns 429 with notifyzr.common.error.rate_limited and a Retry-After header.

The limit is per client IP and path, not per key

A counter is kept per (client IP, route) pair. Two keys called from the same host share a budget; one key called from two hosts does not. The default is 100 requests per 60 seconds, configurable per instance.

Work that runs inside the platform — a broadcast fan-out, an automation's actions — is governed by quotas rather than by this limit.

Quotas ​

Plans carry ceilings: messages and automation runs per month, and a maximum number of contacts, automations, forms, webhooks, domains, channels, templates, members and API keys. Read the current state at GET /v1/usage.

Exceeding one returns 422 with notifyzr.quota.error.exceeded.

Crossing a ceiling never corrupts work in flight. A contact import or a broadcast fan-out that runs out of quota completes, with the remainder counted as skipped — the records that fit are real. Owners and admins are emailed at 80% of a monthly quota and again at the limit.

CORS ​

The API answers preflight requests and its allowed origin is configured per instance (ACCESS_CONTROL_ALLOW_ORIGIN, * by default). This matters for the public form-submission endpoint, which is the one route a browser on your own site calls directly.

Idempotency ​

There are no idempotency keys. Two identical POST /v1/send calls send two emails. Where repeating a call is genuinely a no-op it is documented on the resource page — unsubscribing an already-suppressed address, for instance, emits no second event.

Released under the Apache 2.0 License.