Errors
Errors and rate limits
Every error is an RFC 7807 application/problem+json body, with the standard title, status, detail, and instance fields alongside an errorCode extension. Branch on errorCode, not on detail - the message text can change; the code is the contract.
A key that has been revoked - 401
{
"title": "Unauthorized",
"status": 401,
"detail": "API key is invalid or revoked.",
"instance": "/api/v1/notifications/send",
"errorCode": "api_keys.invalid",
"correlationId": "0f2b8a1e-6c9d-4b3a-9e7f-2d4c8a1b6e3f",
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}Validation failures
A send or preview request that fails validation always carries the umbrella code notifications.validation_failed as its errorCode, so the documented promise that every error has one holds without exception. Alongside it, an errorCodes array lists every specific rule that actually fired - a single request can trip more than one at once, for example a missing tags field and an invalid recipient together. Branch on the umbrella code to detect "this request was malformed," then read errorCodes for which of the codes below caused it. The standard errors field is still present too, mapping each request property to its message text - useful for surfacing something to a human, but not for branching on.
Two rules fired at once - 400
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Tags": ["Tags collection is required."],
"EmailOptions.To[0]": ["To recipient must be a valid email address."]
},
"errorCode": "notifications.validation_failed",
"errorCodes": [
"notifications.tags_required",
"notifications.email.to_invalid"
],
"instance": "/api/v1/notifications/send",
"correlationId": "0f2b8a1e-6c9d-4b3a-9e7f-2d4c8a1b6e3f"
}Every error code
| Code | Status | Meaning |
|---|---|---|
| api_keys.invalid | 401 | The header was present, but the key doesn't resolve, or it has been revoked or has expired. |
| api_keys.missing | 401 | No X-Api-Key header was sent with the request. |
| api_keys.tenant_mismatch | 403 | The key is valid, but it belongs to a different workspace than the tenantId in the request body. |
| notifications.monthly_limit_exceeded | 429 | The workspace has used its monthly message allowance for the current billing period. |
| notifications.monthly_limit_not_configured | 403 | The workspace's plan has no monthly message limit configured, so the send cannot be authorized. Contact support. |
| notifications.no_live_version | 400 | The template has no version currently live. Publish one, or pin an explicit versionNumber to send or preview a specific version. |
| notifications.request_missing | 400 | The request body was empty or missing entirely. |
| notifications.tag_key_required | 400 | A tag's key was empty or missing. |
| notifications.tag_key_too_long | 400 | A tag's key was longer than 256 characters. |
| notifications.tags_required | 400 | No tags array was supplied on the request. An empty array is fine - the field just can't be missing. |
| notifications.template_id_required | 400 | No templateId was supplied on the request. |
| notifications.template_not_found | 404 | No template matches the given templateId in this workspace. |
| notifications.template_version_is_draft | 400 | The pinned versionNumber exists but is still a draft. Publish it before sending or previewing. |
| notifications.template_version_not_found | 400 | The pinned versionNumber doesn't exist for this template. |
| notifications.tenant_id_required | 400 | No tenantId was supplied on the request. |
| notifications.unsupported_notification_type | 400 | Preview was called on a template that isn't an email template - only email templates can be previewed. |
| notifications.validation_failed | 400 | The umbrella code for a failed send or preview validation. Read the errorCodes array alongside it for the specific rules that fired - see Validation failures below. |
| notifications.version_number_invalid | 400 | versionNumber was supplied but is not greater than zero. |
| notifications.webhook_delivery_not_available | 403 | The workspace's plan does not include webhook delivery. |
| notifications.webhook_destination_invalid | 400 | The webhook destination URL or headers were rejected - for example, the URL targets a disallowed host. |
| notifications.webhook_destination_required | 400 | A webhook template was sent without webhookOptions.urlOverride. |
Email field codes
Per-recipient validation on emailOptions. to, cc, bcc, and overrideRecipients are each checked only once you send a non-empty list for that field - omitting the field entirely never triggers these.
| Code | Status | Meaning |
|---|---|---|
| notifications.email.bcc_empty | 400 | An entry in emailOptions.bcc is empty or blank. |
| notifications.email.bcc_invalid | 400 | An entry in emailOptions.bcc is not a valid email address. |
| notifications.email.cc_empty | 400 | An entry in emailOptions.cc is empty or blank. |
| notifications.email.cc_invalid | 400 | An entry in emailOptions.cc is not a valid email address. |
| notifications.email.from_address_invalid | 400 | emailOptions.fromAddress is not a valid email address. |
| notifications.email.override_recipient_empty | 400 | An entry in emailOptions.overrideRecipients is empty or blank. |
| notifications.email.override_recipient_invalid | 400 | An entry in emailOptions.overrideRecipients is not a valid email address. |
| notifications.email.reply_to_invalid | 400 | emailOptions.replyTo is not a valid email address. |
| notifications.email.to_empty | 400 | An entry in emailOptions.to is empty or blank. |
| notifications.email.to_invalid | 400 | An entry in emailOptions.to is not a valid email address. |
Attachment field codes
Per-attachment validation on emailOptions.attachments, checked for each entry in the array.
| Code | Status | Meaning |
|---|---|---|
| notifications.attachment.content_invalid_base64 | 400 | An attachment's content is not valid base64. |
| notifications.attachment.content_required | 400 | An attachment's content is empty or missing. |
| notifications.attachment.content_too_large | 400 | An attachment's decoded content is larger than 10 MB. |
| notifications.attachment.content_type_required | 400 | An attachment's contentType is empty or missing. |
| notifications.attachment.content_type_too_long | 400 | An attachment's contentType is longer than 256 characters. |
| notifications.attachment.name_required | 400 | An attachment's name is empty or missing. |
| notifications.attachment.name_too_long | 400 | An attachment's name is longer than 255 characters. |
Rate limits
Limits are fixed windows, reset every minute, and return 429 once exceeded. The numbers below are the platform defaults CommuniQueue ships with - they're configurable per deployment, so treat them as what you should expect out of the box rather than a number you can rely on exactly.
- /notifications/send and /notifications/preview share a default budget of 300 requests per minute per API key.
- /api-keys/validate defaults to 120 requests per minute per source IP, since it is a key-validation check rather than a send.
- Inbound webhook intake defaults to 600 requests per minute per source IP.
Over the limit - 429
{
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Try again later.",
"instance": "/api/v1/notifications/send"
}