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:
{
"success": true,
"message": "Contacts retrieved",
"status_code": 200,
"error": null,
"payload": { }
}On failure, error is populated instead and payload is null:
{
"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
| Code | Meaning |
|---|---|
200 | Success |
201 | Created |
202 | Accepted — queued work: a send, a broadcast fan-out, a contact import |
400 | Malformed request |
401 | Missing, invalid, revoked or expired API key |
403 | Authenticated, but not permitted |
404 | Not found in your organization — which is also the answer when it exists in another one |
415 | Unsupported content type (form submissions and contact imports) |
422 | Well-formed but rejected by a business rule |
429 | Rate 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.
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:
{
"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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the window |
X-RateLimit-Remaining | Requests left in it |
X-RateLimit-Reset | Seconds 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.