Errors & Rate Limits
Error envelope
All HTTP error responses use a single canonical JSON envelope:
{
"code": "ERROR_CODE",
"message": "Human-readable description safe to display to clients."
}
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error identifier (SCREAMING_SNAKE_CASE). Stable across releases — safe for client switch/case logic. |
message | string | Human-readable description. May change between releases; do not match on this field programmatically. |
Some validation failures additionally include a fields object mapping field
names to per-field messages.
Standard codes
| Code | HTTP status | Meaning |
|---|---|---|
BAD_REQUEST | 400 | Malformed request that cannot be understood. |
UNAUTHORIZED | 401 | Authentication is required or credentials are invalid. |
FORBIDDEN | 403 | Authenticated but not permitted to perform this action. |
NOT_FOUND | 404 | The requested resource does not exist. |
CONFLICT | 409 | The resource already exists or is in a conflicting state. |
VALIDATION_ERROR | 422 | Request body or parameters failed validation. |
INTERNAL_ERROR | 500 | An unexpected server-side error occurred. |
SERVICE_UNAVAILABLE | 503 | A downstream dependency is unavailable. |
Domain-specific codes
| Code | HTTP status | Meaning |
|---|---|---|
PAYMENT_REQUIRED | 402 | The plan quota for this billing period is exhausted. |
SEND_CAP_REACHED | 402 | Your own configured monthly send cap has been reached — raise the cap in Billing rather than upgrading the plan. |
TENANT_SUSPENDED | 403 | The workspace account has been suspended. |
TENANT_DELETED | 403 | The workspace account has been closed. |
FEATURE_NOT_AVAILABLE | 403 | The feature is not available on your current plan. Plan gates on a specific option (smart channel, optimal send time, A/B testing, digest batching) also carry feature (which one) and "upgrade_required": true. |
CHANNEL_TYPE_NOT_PERMITTED | 403 | Creating a channel of a type your plan does not include. The body also carries allowed (the channel types your plan permits) and "upgrade_required": true. |
IN_APP_NOT_PERMITTED | 403 | In-app messaging is not included in your plan. The body also carries "upgrade_required": true. |
MANAGED_EMAIL_NOT_PERMITTED | 403 | Hober-managed email is not enabled for this workspace. Hober support turns it on; it is not tied to a plan, so there is no upgrade_required. |
READ_ONLY_TOKEN | 403 | The token is read-only and cannot authorize write operations. |
FILE_TOO_LARGE | 413 | An uploaded file exceeds the maximum allowed size. |
TEMPLATE_NOT_FOUND | 422 | The template_id referenced in the request does not exist in this workspace. |
QUOTA_EXCEEDED | 429 | The workspace has exceeded a plan quota. |
RATE_LIMITED | 429 | Too many requests: the workspace's per-minute request limit, or a rate-limited surface such as account registration. |
Rate limits
Per-workspace request limit
Requests authenticated with a dashboard session token or a server key
(Bearer hober_srv_…) count against one per-workspace limit over a fixed
one-minute window. The limit depends on your plan:
| Plan | Requests per minute |
|---|---|
| Free | 60 |
| Starter | 300 |
| Growth | 1,000 |
| Scale | 3,000 |
| Agency | 5,000 |
| Enterprise | 10,000 |
The limit is being rolled out: requests over it are currently counted, not
rejected. Once it is enforced, a request over the limit gets 429 with code
RATE_LIMITED in the standard error envelope and a Retry-After header
giving the seconds until the window resets. Retry-After is the only
rate-limit header — there are no X-RateLimit-* headers.
Requests authenticated with a client SDK key come from your users' devices and are not counted against this limit; the event ingestion endpoint instead enforces per-tier payload caps, below.
Event ingestion caps
POST /v1/events applies fixed caps that depend on the credential tier:
Client (X-SDK-Key) | Server (Bearer hober_srv_…) | |
|---|---|---|
| Events per request | 50 | 1,000 |
| Request body | — | 500 KB (413 beyond) |
| Single event | — | 32 KB |
occurred_at backdating window | 48 hours | 90 days |
Cap violations return 422 with code VALIDATION_ERROR (oversized
server-tier bodies return 413). See Events for the full
endpoint reference.
Registration limit
Creating a new account is limited to 5 registrations per IP address per hour
and 10 per email domain per day. Exceeding either limit returns 429 with
code RATE_LIMITED and a Retry-After header.
Quotas are not rate limits
QUOTA_EXCEEDED (429), PAYMENT_REQUIRED (402), and SEND_CAP_REACHED
(402) signal plan or billing limits, not request-frequency limits — retrying
will not succeed until the quota resets, the plan changes, or the cap is
raised.
The monthly message quota counts messages handed to a provider: one per
device, address or in-app recipient a send reached, including sends the
provider rejected. Simulated sends and sends that fail before reaching a
provider do not count. A notification is accepted while any quota remains and,
once it starts sending, is delivered in full, so the last one in a period can
take usage past the limit; the next one is refused. A scheduled notification
is checked again when it is due: if the quota ran out in the meantime, it
ends failed without sending.
Handling errors
- Branch on
code, never onmessage. - On
429, honor theRetry-Afterheader before retrying; add jittered exponential backoff for repeated rejections. - Treat
500and503as transient and safe to retry with backoff; 4xx responses (other than429) will not succeed on retry without changing the request.