Error shape
{
"error": "Invalid request",
"details": { /* validation issues, when applicable */ }
}Errors you can act on programmatically also carry a stable code:
{
"code": "ATTACHMENT_TYPE_BLOCKED",
"error": "Attachments of type .exe are not allowed",
"attachment": { "index": 1, "filename": "setup.exe" }
}Status codes
| Status | Meaning | Retry? |
|---|---|---|
| 400 | Validation failed — the details field lists the offending inputs | no — fix the request |
| 401 | Missing, malformed, or revoked API key | no |
| 404 | Resource not found in this workspace (template version, contact, topic) | no |
| 410 | Idempotent replay of a send whose attachments have expired | no — send again with a new key |
| 413 | Attachment or request body too large | no — shrink or switch to url attachments |
| 422 | Recipient blocked for this type of email, unknown topic, or a url attachment could not be fetched | no |
| 429 | Rate limited, or a plan quota exhausted | yes for rate limits — after backoff |
| 5xx | Transient server error | yes — with backoff |
Error codes
| Code | Status | Meaning |
|---|---|---|
| RECIPIENT_SUPPRESSED | 422 | Recipient is blocked for this type of email; the message is recorded but not sent. details says why — see below |
| TOPIC_NOT_FOUND | 422 / 404 | topic names no active topic in this workspace (unknown or archived). 422 from send, 404 from contacts/unsubscribe and resubscribe |
| QUOTA_EXCEEDED | 429 | Workspace email quota for the billing period is exhausted |
| ATTACHMENT_INVALID | 400 | Bad base64, non-https url, blocked host, duplicate contentId, or empty file |
| ATTACHMENT_TYPE_BLOCKED | 400 | Executable, script, HTML or SVG attachment |
| ATTACHMENT_TOO_LARGE | 413 | Inline or total attachment size limit exceeded |
| REQUEST_TOO_LARGE | 413 | Request body over 4 MB (inline attachments count toward it) |
| ATTACHMENTS_EXPIRED | 410 | Replay of a failed send whose attachments were deleted after 6 months |
| ATTACHMENT_FETCH_FAILED | 422 | A url attachment returned an error, timed out, redirected too often, or was empty |
| ATTACHMENT_QUOTA_EXCEEDED | 429 | Workspace attachment byte quota for the billing period is exhausted |
RECIPIENT_SUPPRESSED carries the resolved type and topic of the send, the recorded messageId, and a details object:
{
"code": "RECIPIENT_SUPPRESSED",
"error": "maya@example.com unsubscribed from all marketing email. ...",
"reason": "OPT_OUT_ALL",
"details": { "kind": "opt_out", "reason": "OPT_OUT_ALL" },
"type": "marketing",
"messageId": "msg_..."
}details.kind is suppression (a delivery block: HARD_BOUNCE, MANUAL, or COMPLAINT) or opt_out (OPT_OUT_TOPIC, with details.topic naming the topic slug, or OPT_OUT_ALL). Opt-outs and complaints block marketing sends only; hard bounces and manual suppressions block every send. Which block applies to which type is tabled in Subscriptions and unsubscribe.
Attachment errors include an attachment object with the index and filename of the rejected file. None of them are retried by the SDK. Bodies over 4.5 MB are rejected by the platform before reaching the API and return a plain 413 without a code.
Rate limits
- 100 requests / minute per API key
- 1,000 requests / hour per IP address
Exceeding either returns 429 with standard draft-6 rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) so clients can back off precisely.
@ewyn/client retries 429 and 5xx automatically with exponential backoff, and idempotencyKey guarantees retries never double-send.
Timeouts
The SDK aborts requests after 60 seconds by default and surfaces them as an EwynApiError with status 408 and code TIMEOUT. Timeouts are not retried automatically — pair sends with an idempotency key and retry on your side if delivery is critical.