Authentication
The public API authenticates with an API key in the X-API-Key header.
curl http://api.localhost/v1/contacts \
-H "X-API-Key: $NOTIFYZR_API_KEY"Keys are organization-scoped
A key belongs to exactly one organization. Everything it can read or write belongs to that organization, and there is no parameter that changes which one — so a leaked key exposes one organization's data and no more.
Mint, revoke and delete keys from the dashboard under Settings → API keys. Only the hash is stored, so the key itself is shown once at creation.
Revoking and deleting both stop a key working immediately; they differ in what is left behind. A revoked key stays in the list marked Revoked, so it is still visible that it existed and when it was issued. Deleting removes the row entirely — use it for a key that was created by mistake. Anything the key produced (messages, imports, activity entries) survives either way.
Expiry
A key may be given an expires_at. An expired or deactivated key is rejected exactly like an invalid one — with 401 and no hint about which it was.
Owners and admins are emailed as a key approaches expiry and again once it has passed, so a key does not lapse silently in a running integration.
What is not authenticated this way
| Surface | Auth |
|---|---|
POST /v1/forms/{id}/submissions | None. HTML forms post here directly; it is rate-limited instead. |
| The hosted unsubscribe page | None. The signed ?t= token in the link is the authorisation. |
| The dashboard | A JWT session, issued by the control-panel API. The browser never holds an API key. |
| The MCP server | The same X-API-Key, relayed to this API. See MCP server. |
Errors
A rejected request returns the platform's standard envelope:
{
"success": false,
"message": "Invalid or inactive API key",
"status_code": 401,
"error": { "code": "notifyzr.apikey.error.unauthorized", "message": "…", "issues": [] }
}Match on error.code, not on the message — codes are stable, messages are not.