Reference

Errors

Every failure returns the same envelope. Branch on the stable code, never on the message.

The error envelope

When a request fails, Sendara returns an HTTP status in the 4xx or 5xx range and a JSON body with a single error object. The code is a stable, machine-readable string you can switch on; the message is for humans and may change; the status mirrors the HTTP status for convenience.

{
  "error": {
    "code": "from_not_verified",
    "message": "from_email domain is not a verified sending domain for this account",
    "status": 422
  }
}
Branch on error.code, not on error.message or wording — codes are part of the API contract and stable across releases; messages are not.

In a POST /v1/send/batch response, per-item failures use the same shape inside each entry — a failed item is { "success": false, "error": { … } } — so partial success is normal and is reported item by item.

Error codes

Every code the API emits, with the HTTP status it carries, the condition that triggers it, and how to recover.

Sendara error codes with HTTP status, trigger, and recovery
CodeStatusWhen it firesHow to recover
invalid_request400The request body is malformed, a required field is missing, or a value is out of range — including an unparseable pagination cursor.Read the message, fix the offending field, and retry. These never succeed on retry without a change.
unauthorized401The Authorization header is missing, malformed, or the API key is unknown, revoked, or expired.Send a valid Bearer key (Authorization: Bearer sk_live_…). If it was rotated or revoked, issue a new key.
invalid_signature401An inbound signed request (e.g. a verification webhook) failed HMAC verification — wrong secret, altered body, or a stale timestamp.Recompute the signature with the correct signing secret over the raw body. See verifying signatures below.
spend_cap_exceeded402The send would push the account past its configured hard spend cap for the period.Raise the cap in the dashboard or wait for the next billing period. Sends resume automatically once under the cap.
forbidden403The key authenticated but its scope does not permit the operation — e.g. a read key calling POST /v1/send.Use a key with a sufficient scope (send, read, or admin). Mint a new scoped key if needed.
from_not_verified422The from_email domain is not a verified sending domain for the account.Add and verify the domain (publish the DKIM, MAIL FROM, and DMARC records), then send from an address on it.
recipient_not_verified403A test_send was addressed to an email that is not a verified test recipient for the account.Register the address as a test recipient and confirm it from the verification email, then retry.
not_found404The referenced resource (message, template, domain, key, contact, list) does not exist or belongs to another account.Check the id. Cross-account access surfaces as not_found by design — never as forbidden.
recipient_suppressed409The recipient is on the suppression list for this channel (hard bounce, complaint, or a manual suppression).Remove the suppression with DELETE /v1/suppressions if the address is genuinely valid; otherwise stop sending to it.
idempotency_key_reused409An idempotency_key was reused with a different request body than the one it first succeeded with.Use a fresh key for a new logical send. Retries of the same send must carry the same key and identical body.
duplicate_contact409A contact with the same email already exists in the account or target list.Treat the contact as already present, or update the existing record instead of creating a new one.
duplicate_member409The contact is already a member of the target list.No action needed — the membership already exists.
invalid_template400The template body has malformed Mustache syntax, or render/preview was called with an invalid template.Fix the template markup so it parses, then re-render.
missing_variable400A render or send omitted a variable the template marks as required.Supply every required variable in template_vars and retry.
invalid_token400A one-time link token (password reset, unsubscribe) is invalid, already used, or expired.Request a fresh link — tokens are single-use and time-bound.
too_many_test_recipients422Registering this test recipient would exceed the limit of 3 verified test addresses per account.Remove an existing test recipient before adding another.
test_send_daily_limit429The per-recipient daily test_send cap (10 sends per verified address per day) was reached.Wait for the daily window to reset, or send to a different verified test recipient.
payload_too_large413An uploaded asset exceeds the 2 MiB limit.Compress or resize the file under 2 MiB and re-upload.
rate_limit_exceeded429The account or IP exceeded its request budget for the current window.Back off for the seconds given in Retry-After, then retry. Honor the X-RateLimit-* headers to stay under the limit.
billing_not_configured503A billing operation (checkout, portal, webhook) was attempted while billing is not configured for the deployment.Configure billing, or contact support. This is an environment-level condition, not a per-request fault.
internal_error500An unexpected server-side failure. The request may or may not have taken effect.Retry with the same idempotency_key — replays are safe and return the original result if it landed. Persisting? Contact support.

Status families

  • 400 / 422 — the request is malformed or fails a business rule. Fix the input; retrying unchanged will not help.
  • 401 / 403 — an auth or authorization problem. Check the key, its scope, or signature verification.
  • 402 — a spend cap was hit. Adjust limits or wait for the next period.
  • 404 — the resource is absent, including cross-account access.
  • 409 — a conflict with existing state (suppression, idempotency, or a duplicate).
  • 413 — the payload is too large.
  • 429 — you are being throttled. Honor Retry-After and back off.
  • 5xx — a transient server condition. Retry safely with the same idempotency key.

Handling errors

The SDKs raise a typed error carrying code, status, message, and — on 429retryAfter. Branch on the code and recover per the table above.

Retries & idempotency

Retry 429 and 5xx responses with exponential backoff. Because every send carries an idempotency_key, a replay after a timeout or internal_error returns the original result instead of sending twice. Reusing a key with a different body is rejected with idempotency_key_reused, so retries must send the exact same payload.

Do not retry 4xx errors other than 429 — they are deterministic and will fail again until you change the request.

Rate-limit headers

Every response carries the current budget so you can throttle before you are throttled. A 429 additionally returns Retry-After (in seconds) and the rate_limit_exceeded code.

  • X-RateLimit-Limit — the ceiling for the window.
  • X-RateLimit-Remaining — requests left in the window.
  • X-RateLimit-Reset — Unix epoch seconds when the window resets.

Verifying inbound signatures

Webhook deliveries are signed so you can prove they came from Sendara. Each request carries Sendara-Signature, Sendara-Timestamp, Sendara-Event-Id, and Sendara-Event-Type. The signature is HMAC-SHA256 of "{timestamp}.{raw body}", hex-encoded, keyed with the subscription's signing secret. Verify over the raw request body — before any JSON parsing or re-serialization — and compare in constant time.

A failed verification surfaces as invalid_signature (401) on inbound signed requests. Reject anything that does not verify, and treat handlers as idempotent — an event may be delivered more than once and always reuses the same Sendara-Event-Id across retries.