The envelope
Every failure returns the same three fields, at every status. error is stable and safe to branch on; message is written for a human reading a log and may change.
Some errors add fields — a rejected batch item carries its index, a throttled account carries the rate that caused it. Treat anything beyond the three below as advisory.
{
"error": "recipient_not_allowlisted",
"message": "Key allowlist is [@example.com]. Recipient is outside it.",
"docs": "https://www.sadasend.com/docs/errors#recipient_not_allowlisted"
}Authentication and scope
| Code | Status | Cause & Step-by-Step Remediation |
|---|---|---|
| missing_api_key | 401 | No Authorization header provided. Fix: Pass Authorization: Bearer sada_live_sk_... or set SADASEND_API_KEY in your environment. |
| invalid_api_key | 401 | The API key does not exist, or was permanently revoked. Fix: Check key prefix (sada_live_sk_...) or generate a new key under /app/keys. Keys cannot be recovered once shown. |
| unauthenticated | 401 | A dashboard session cookie was required and none was present. Fix: Sign in through /login to obtain an active session cookie. API keys do not authenticate dashboard-internal routes. |
| insufficient_scope | 403 | The key is valid but its permission scope set excludes this operation. Fix: Scopes are immutable after creation. Mint a new key with send, messages:read, or full_access under /app/keys. |
| api_key_expired | 401 | The API key has passed its expiration timestamp (expires_at). Fix: Mint a new key or update the expiration date in the dashboard under /app/keys. |
| ip_not_allowed | 403 | The client IP address is not permitted by this key’s CIDR IP allowlist (ip_allowlist). Fix: Add your server egress IPv4/IPv6 address or subnet (e.g. 198.51.100.1/32) to the key settings in /app/keys. |
| forbidden_origin | 403 | Browser Origin header from an untrusted origin was rejected. Fix: Invoke the API from backend application servers or authorized domains. |
| key_revoked | 409 | The key that originally sent this message has since been revoked, so it cannot be used to resend it. Fix: Re-dispatch the message using an active API key. |
| plan_tier_insufficient | 403 | Feature requires an upgraded plan tier (e.g. Slack 1-Click Interactive Approvals requires Starter or higher). Fix: Upgrade your plan tier under /app/billing. |
Sending
The codes worth handling explicitly. A send that fails with any of these has not been queued and will not be retried for you.
| Code | Status | Cause & Step-by-Step Remediation |
|---|---|---|
| missing_from | 400 | Missing from sender email. Fix: Provide a valid sender address belonging to a verified domain on your account (e.g. "from": "billing@yourdomain.com"). |
| missing_to | 400 | Missing to recipient. Fix: Provide at least one valid recipient address as a string or array of strings. |
| invalid_from | 400 | The from address is syntactically invalid, or its domain is not configured on your account. Fix: Verify domain spelling or register the domain under /app/domains. |
| invalid_source | 400 | Header X-SadaSend-Source or JSON source field must be one of: agent, backend, automation, or system. |
| invalid_agent_name | 400 | Header X-SadaSend-Agent or JSON agent_name exceeds 128 characters. Fix: Use a concise bot identifier (e.g. "SupportBot"). |
| domain_not_found | 403 | Sending domain is not registered on this account. Fix: Add the domain under /app/domains (POST /domains) before sending. |
| domain_not_verified | 403 | Domain exists on your account but SPF, DKIM, or DMARC DNS records have not verified. Fix: Publish the required DNS records from /app/domains and run a DNS check. |
| recipient_not_allowlisted | 403 | The API key enforces a recipient allowlist and this address is outside it. Fix: Add the recipient or wildcard domain (*@example.com) to allowlist in /app/keys, or test with permitted addresses. |
| sandbox_limit_reached | 403 | The shared onboarding domain (sadasend.net) lifetime send cap was reached. Fix: Add and verify your own custom domain in /app/domains to lift all sandbox limits. |
| recipient_suppressed | 422 | Recipient address is on your suppression list due to a prior hard bounce, spam complaint, or unsubscribe. Fix: Check status and manually remove under /app/suppressions if delivery is now safe. |
| attachment_too_large | 422 | An individual attachment exceeds the 10MB limit. Fix: Compress the file or host externally and provide a download link in the email body. |
| attachments_too_large | 422 | Combined attachments exceed the 25MB total limit per message. Fix: Reduce total attachment payload size below 25MB. |
| invalid_attachment | 422 | Attachment object is missing required filename or content, or base64 data is malformed. Fix: Validate base64 string format and provide filename. |
| quota_exceeded | 402 | Monthly email credit quota exhausted on your current plan. Fix: Upgrade your plan or purchase on-demand elastic credit packs under /app/billing. |
| account_suspended | 403 | Account is suspended due to excessive complaint rate (>0.25%) or security policy violation. Fix: Contact support at support@sadasend.com to review deliverability metrics. |
| payload_too_large | 413 | The HTTP request body exceeds maximum allowed size (25MB for mail dispatches, 1MB for API metadata). Fix: Reduce message body or attachment payload size. |
| account_throttled | 429 | Account complaint or bounce rate exceeded the warning threshold (0.10%). Transactional mail continues at reduced dispatch velocity. Fix: Review bounce forensics in /app/logs. |
| rate_limit_exceeded | 429 | API key or client IP exceeded burst rate limits. Fix: Inspect the Retry-After response header and implement exponential backoff retry logic. |
| daily_limit_reached | 429 | Free plan daily recipient limit reached. Fix: Wait for the rolling 24-hour window indicated by retryAfterSeconds, or upgrade to Starter ($12/mo) for unmetered daily sends. |
| idempotency_key_reused | 409 | Header Idempotency-Key UUID was previously used for a different request payload. Fix: Reuse the key only to retry identical sends; generate a fresh UUID for new emails. |
| idempotency_key_in_progress | 409 | The initial request with this Idempotency-Key is currently being processed. Fix: Wait 500ms and retry without minting a new key. |
| attachment_storage_failed | 503 | Transient storage cluster timeout saving attachment payload. Fix: The send was not accepted into queue; retrying with exponential backoff is safe. |
| dlp_secret_detected | 422 | Outbound Data Loss Prevention (DLP) scanner blocked email containing exposed cryptographic private keys, database credentials, or API tokens. Fix: Check preview and location in the error response to remove credentials, or pass "dlp": false in JSON for trusted diagnostic scripts. |
| dlp_violation | 403 | Pre-flight Data Loss Prevention policy violation: sensitive credentials or PII detected in outgoing message content. Fix: Remove matched credentials or credit card numbers from body text or HTML, or pass "dlp": false in JSON for trusted diagnostic tools. |
| duplicate_content_loop_detected | 429 | Autonomous loop circuit breaker tripped: identical body content hash sent to the exact same recipient within 5 minutes. Fix: Resolve agent loop / retry bug. For legitimate re-sends, vary content or wait 300 seconds. |
| prompt_injection_flagged | 422 | Inbound email classified with high prompt injection score (>0.85) containing jailbreak instructions or delimiter overrides. Fix: Quarantine message before agent context ingestion, or parse with /inbound/parse to extract clean Markdown. |
Remediating autonomous guardrail & security errors
Detailed diagnostic breakdowns and resolution recipes for the built-in safety controls and autonomous agent error conditions.
Outbound DLP secret detection (dlp_secret_detected & dlp_violation)
Autonomous AI agents running code, analyzing data, or querying internal APIs frequently handle sensitive runtime credentials (API keys, database strings, tokens, private keys). If an agent dumps debug context or runtime memory into an outgoing email, the Outbound DLP scanner intercepts it before network transmission.
The scanner inspects subject, text, and raw html (including HTML attributes, data properties, URLs, and comments) for:
• Provider API Secrets: Anthropic Claude (sk-ant-api03-...), OpenAI (sk-...), AWS Access Key IDs (AKIA...), GitHub PATs (ghp_... and github_pat_...), Stripe live keys (sk_live_..., rk_live_...), Slack bot tokens (xoxb-...), Google Cloud, SendGrid, and Resend.
• Cryptographic Private Keys: RSA, EC, OpenSSH, DSA, and encrypted private key blocks (-----BEGIN ... PRIVATE KEY-----).
• Payment Card Industry (PCI) PII: Visa, Mastercard, American Express, and Discover formats verified via Luhn mod 10 checksum validation to eliminate false positives on serial numbers.
• Government IDs: US Social Security Numbers (SSNs) with valid area/group structure (rejecting 000, 666, and 900+ series).
When intercepted, the API returns HTTP 422 with a cryptographically safe redacted preview and the exact location where the secret was found:
- Step 1 — Identify the leaked variable: Check
location(subject,text, orhtml) and search your template or agent prompt for the token prefix shown inpreview. - Step 2 — Scrub runtime environment variables: Ensure debug variables (
process.env,os.environ, API client responses) are not serialized into the prompt context or email body template. - Step 3 — Diagnostic Opt-Out: If you are intentionally running internal diagnostic tests or penetration tests, pass
"dlp": falsein the request body to bypass the pattern matcher.
{
"error": "dlp_secret_detected",
"message": "Outbound DLP scanner blocked email containing exposed Anthropic API token",
"preview": "sk-ant***890",
"location": "html",
"docs": "https://www.sadasend.com/docs/errors#dlp_secret_detected"
}Autonomous loop circuit breaker (duplicate_content_loop_detected)
Autonomous AI agents operating in agentic tool-use loops can enter infinite execution cycles due to retry bugs, unhandled exceptions, or looping reasoning models. Repeatedly dispatching identical emails burns customer quotas and damages sender reputation.
SadaSend computes a deterministic rolling SHA-256 fingerprint over sorted recipients, subject, and normalized body text with a 5-minute (300-second) sliding window:
• Transparent Deduplication: If an identical email is re-sent to the same recipient within 5 minutes, SadaSend transparently deduplicates the send, returning { ok: true, deduplicated: true } and echoing the original messageId without consuming monthly quota or queuing duplicate spam.
• Circuit Breaker Trip: If rapid runaway bursts continue beyond the deduplication threshold, HTTP 429 duplicate_content_loop_detected is returned.
• Broadcast Safety: The fingerprint binds strictly to the recipient set. Sending identical newsletters or transactional receipts to different customers is never deduplicated.
- Fix: Investigate your agent’s termination condition or exception handling. Ensure your agent checks for existing delivery IDs before re-issuing tool calls.
{
"error": "duplicate_content_loop_detected",
"message": "Circuit breaker tripped: autonomous loop detected after matching 5 identical body content hashes within a 5-minute rolling window",
"docs": "https://www.sadasend.com/docs/errors#duplicate_content_loop_detected"
}Inbound prompt injection threat (prompt_injection_flagged)
When autonomous agents process incoming emails, adversaries can embed prompt injections, system prompt overrides, or jailbreak instructions within reply text.
SadaSend runs edge heuristic classifiers on inbound emails. If the risk score exceeds 0.85, the payload is flagged with HTTP 422 prompt_injection_flagged:
- Fix: Ingest inbound emails through
POST /inbound/parseto extract token-efficient sanitized Markdown (cleanMarkdown) stripped of quoted chains and mobile signatures, and evaluatesecurity.quarantineRecommendedbefore passing text to LLM context.
{
"error": "prompt_injection_flagged",
"message": "Inbound email classified with high prompt injection score (>0.85) containing jailbreak instructions or delimiter overrides",
"docs": "https://www.sadasend.com/docs/errors#prompt_injection_flagged"
}Batches, logs and message actions
| Code | Status | Cause & Step-by-Step Remediation |
|---|---|---|
| empty_batch | 400 | The batch request body contained an empty emails array. Fix: Ensure your payload contains at least 1 message before submitting POST /v1/batch. |
| batch_too_large | 400 | The batch exceeds the maximum per-call limit of 100 messages. Fix: Split larger dispatches into chunks of 100 or fewer messages. |
| missing_emails | 400 | The batch JSON body was missing the required emails array. Fix: Wrap your message objects in a top-level emails array: {"emails": [...]}. |
| invalid_status | 400 | Unrecognized status filter provided to GET /v1/logs?status=.... Fix: Filter by supported statuses: queued, delivered, bounced, failed, held, or cancelled. |
| invalid_date | 400 | A date filter parameter (from or to) could not be parsed. Fix: Use standard ISO 8601 timestamps (e.g. 2026-10-25T00:00:00Z). |
| invalid_cursor | 400 | The pagination cursor is malformed, expired, or generated from a different query. Fix: Omit the cursor query parameter to restart pagination from page 1. |
| message_not_found | 404 | No message matching the given ID exists on this account. Fix: Verify the id returned from POST /v1/send and confirm you are querying with the matching workspace API key. |
| not_cancellable | 409 | The message has already completed dispatch or departed relay queues. Fix: Only scheduled future sends currently in queued status can be cancelled via DELETE /v1/messages/:id. |
| body_unavailable | 409 | The rendered message body has aged past your plan retention window (30 to 90 days). Fix: Upgrade your plan under /app/billing for extended retention, or archive critical message content in your application storage. |
| no_originating_key | 409 | The original API key that queued the message was revoked or deleted. Fix: Re-submit the send request using an active API key. |
Domains, keys and approvals
| Code | Status | Cause & Step-by-Step Remediation |
|---|---|---|
| invalid_domain | 400 | The domain name is syntactically invalid or uses an unsupported format. Fix: Provide a fully-qualified domain name without protocols or paths (e.g. domain.com or mail.domain.com). |
| domain_exists | 409 | This domain is already registered on your account or another workspace. Fix: Verify domain ownership or delete the domain from any test workspace before adding. |
| domain_quota_exceeded | 403 | The maximum registered sending domains limit for your current subscription plan has been reached. Fix: Remove unused domains or upgrade your plan tier under /app/billing. |
| history_not_found | 404 | Deleted domain record was not found in audit history for 1-click restore. Fix: Re-add the domain as a new record and configure fresh DKIM DNS records. |
| restore_failed | 500 | Database error while restoring deleted domain and its preserved DKIM keys. Fix: Retry domain restoration in /app/domains or re-register the domain anew. |
| invalid_scope | 400 | Unknown permission scope requested during API key creation. Fix: Specify valid scopes: mail:send, mail:batch, messages:read, domains:read, analytics:read, or webhooks:write. |
| invalid_mode | 400 | Key execution mode is unrecognized. Fix: Set mode to live (immediate dispatch), test (sandbox dry run), or approval (holds all outgoing messages for human dashboard sign-off). |
| invalid_rate_limit_max | 400 | rate_limit_max must be an integer between 1 and 1,000,000. Fix: Supply a positive integer limit or leave empty to inherit default account throughput. |
| invalid_rate_limit_window | 400 | rate_limit_window is unrecognized. Fix: Choose one of the supported rate limit duration windows: 1s, 1m, 1h, or 1d. |
| invalid_expires_at | 400 | Key expiration date is invalid or in the past. Fix: Provide a valid future ISO 8601 timestamp (e.g. 2027-01-01T00:00:00Z). |
| invalid_ip_allowlist | 400 | IP allowlist contains malformed addresses or CIDR masks. Fix: Provide valid IPv4, IPv6 addresses, or CIDR blocks (e.g. 198.51.100.1 or 198.51.100.0/24). |
| invalid_default_source | 400 | Default source identifier is unrecognized. Fix: Specify one of: agent, backend, automation, or system. |
| invalid_allowlist | 400 | Recipient allowlist was passed as an empty array. Fix: Provide at least one recipient email address/domain pattern (e.g. *@company.com), or omit property to permit all destinations. |
| key_not_found | 404 | No API key matching the provided ID exists on this account. Fix: Check active keys under /app/keys or mint a new key. |
| invalid_decision | 400 | Approval decision was unrecognized. Fix: Submit {"decision": "approve"} or {"decision": "reject"} when resolving held messages at POST /v1/approvals/:id. |
| already_decided | 409 | The held send has already been approved or rejected. Fix: Decisions are immutable. Submit a new send if resubmission is required. |
Webhooks
| Code | Status | Cause & Step-by-Step Remediation |
|---|---|---|
| missing_url | 400 | Webhook endpoint definition missing required url property. Fix: Provide a secure destination URL (e.g. https://api.yourdomain.com/webhooks). |
| invalid_url | 400 | Destination URL is malformed or could not be parsed. Fix: Ensure the URL includes protocol and a valid hostname. |
| insecure_url | 400 | Destination URL uses unencrypted HTTP. Fix: Endpoints must use TLS (https://) to protect cryptographically signed delivery events in transit. |
| no_events | 400 | The event subscriptions array was empty. Fix: Subscribe to at least one event (e.g. ['email.delivered', 'email.bounced']). |
| unknown_event | 400 | One or more subscribed event names are unrecognized. Fix: Use supported event names: email.sent, email.delivered, email.bounced, email.complained, email.opened, email.clicked, or email.held. |
| invalid_id | 400 | Webhook identifier format is invalid. Fix: Verify webhook ID format returned during endpoint registration. |
| invalid_json | 400 | The request payload was not valid JSON. Fix: Validate JSON syntax and ensure Content-Type: application/json is sent. |
| missing_target | 400 | Neither webhookId nor url was provided for test event dispatch. Fix: Provide an existing webhook endpoint ID or an explicit test URL. |
| unsupported_protocol | 400 | Simulation target URL does not use HTTP or HTTPS. Fix: Use http://localhost:PORT/webhook (local testing) or https:// endpoint. |
| invalid_range | 400 | Event replay window is malformed or exceeds maximum historical limit (30 days). Fix: Specify valid from and to timestamps within the last 30 days. |
| webhook_not_found | 404 | No webhook endpoint matching that ID exists on this account. Fix: Review registered endpoints under /app/webhooks. |
| webhook_disabled | 409 | Endpoint was automatically disabled after repeated consecutive delivery failures. Fix: Fix your receiving endpoint, verify HTTP 200 response, and re-enable the endpoint under /app/webhooks before replaying events. |
| signature_mismatch | 401 | HMAC-SHA256 signature verification failed on Slack or webhook callback. Fix: Verify that your account signing secret matches the secret configured in your receiving handler. |
| timestamp_out_of_range | 401 | Request timestamp difference exceeds the 5-minute replay defense tolerance window. Fix: Ensure your server clock is synchronized with NTP. |
Templates and suppressions
| Code | Status | Cause & Step-by-Step Remediation |
|---|---|---|
| missing_name | 400 | Template creation payload missing required name property. Fix: Provide a unique template identifier name. |
| name_too_long | 400 | Template or API key name exceeds 100 characters. Fix: Shorten the descriptive name to under 100 characters. |
| missing_subject | 400 | Template missing required default subject line. Fix: Add a subject line (supporting Liquid variable interpolation). |
| missing_content | 400 | Template contains neither html nor text content. Fix: Provide at least one rendered content format (html or text). |
| template_not_found | 404 | No template with that ID or name exists on this account. Fix: Check template catalog under /app/templates. |
| template_too_large | 413 | Template source exceeds the 512KB size limit. Fix: Optimize inline CSS and externalize large media assets to CDN URLs. |
| template_render_failed | 422 | Liquid template syntax error or missing mandatory variable. Fix: Inspect the error message identifying the exact missing tag or syntax error. |
| invalid_address | 400 | Email address provided for suppression is malformed. Fix: Supply a standard RFC 5322 compliant email address (e.g. user@example.com). |
| invalid_tag | 400 | Message tag exceeds 50 characters or contains disallowed characters. Fix: Use alphanumeric characters, dots, underscores, colons, and hyphens ([a-zA-Z0-9._:-]). |
| too_many_tags | 400 | Message specifies more than 10 tags. Fix: Reduce tags to 10 or fewer per message. |
| unsupported_engine | 400 | SadaSend server-side templates render with Liquid. Fix: Use Liquid syntax for server-rendered templates, or pre-render React Email templates client-side (see /docs/react-email). |
| suppression_not_found | 404 | The specified email address is not in your account suppression list. Fix: Check active suppressions under /app/suppressions. |
Account, team and support
These come from the dashboard-facing routes rather than from an integration, but they use the same envelope.
| Code | Status | Cause & Step-by-Step Remediation |
|---|---|---|
| invalid_body | 400 | Request body was not a valid JSON object. Fix: Ensure payload is formatted as a JSON object with Content-Type: application/json. |
| nothing_to_update | 400 | PATCH request contains no recognized or modifiable properties. Fix: Check API schema for allowed fields on the target resource. |
| invalid_name | 400 | Account or user name is empty or exceeds character limits. Fix: Provide a valid name string (1–100 characters). |
| missing_org | 400 | Organization name is required. Fix: Specify an organization name when provisioning team workspaces. |
| org_too_long | 400 | Organization name exceeds 100 characters. Fix: Shorten organization name under 100 characters. |
| invalid_country | 400 | Unrecognized country code. Fix: Use standard 2-letter ISO 3166-1 alpha-2 country codes (e.g. US, GB, DE). |
| invalid_phone | 400 | Phone number could not be parsed or validated. Fix: Use E.164 international format (e.g. +14155552671). |
| invalid_website | 400 | Organization website is not a valid URL. Fix: Provide a valid URL including protocol (e.g. https://company.com). |
| invalid_industry | 400 | Selected industry is not recognized. Fix: Select a valid industry option from the account settings dropdown. |
| invalid_referral | 400 | Selected referral source is not recognized. Fix: Choose a recognized referral channel from onboarding options. |
| invalid_account_type | 400 | Account type must be individual or team. Fix: Specify a valid account type value. |
| invalid_email | 400 | Email address format is invalid. Fix: Supply a valid RFC 5322 email address. |
| disposable_email | 400 | Address belongs to a disposable or temporary email provider. Fix: Use a permanent work or business email domain. |
| owner_only | 403 | The requested action requires workspace Owner privileges. Fix: Request the workspace owner to perform the action or adjust your role permissions under /app/team. |
| account_not_found | 404 | No account matching the identifier exists. Fix: Check account session or sign in again. |
| seat_quota_reached | 402 | Maximum team seats reached for your current plan tier. Fix: Upgrade to Scale plan or remove inactive team members under /app/team. |
| webhook_quota_reached | 402 | Maximum registered webhooks reached for your current plan. Fix: Delete unused endpoints under /app/webhooks or upgrade plan tier. |
| template_quota_reached | 402 | Maximum custom template limit reached. Fix: Upgrade your plan or archive obsolete templates under /app/templates. |
| request_limit_reached | 429 | Account-wide daily API request quota reached. Fix: Inspect Retry-After header or upgrade plan under /app/billing for higher burst and daily API allowances. |
| already_a_member | 409 | The invited user is already an active member of this workspace. Fix: Manage member permissions directly under /app/team. |
| invitation_not_found | 404 | Invitation has expired, been revoked, or already accepted. Fix: Issue a fresh invitation link from /app/team. |
| missing_message | 400 | Support ticket request lacks message body. Fix: Provide a descriptive message explaining your issue. |
| message_too_long | 400 | Support ticket message exceeds 5,000 characters. Fix: Summarize ticket details and attach diagnostic logs if necessary. |
| ticket_not_found | 404 | No support ticket with that ID exists on this account. Fix: Verify ticket ID in /app/support. |
Free tools and sign-in
The tool checks are unauthenticated and rate-limited by source. The sign-in codes surface in the browser rather than in an integration.
| Code | Status | Cause & Step-by-Step Remediation |
|---|---|---|
| unknown_check | 404 | The requested diagnostic check type is not supported. Fix: Supported tools include spf, dkim, dmarc, mx, bimi, smtp-check, and blacklist-check. |
| invalid_selector | 400 | The DKIM selector syntax is malformed. Fix: Provide a valid DKIM selector name (e.g. s1, sada1, google). |
| too_many_checks | 429 | Rate limit exceeded for unauthenticated free tool diagnostic lookups. Fix: Wait 60 seconds before issuing further DNS diagnostics, or sign in for unmetered checks. |
| missing_token | 400 | Magic sign-in link lacked the authentication token query parameter. Fix: Click the complete link received in your email or request a new login link. |
| link_expired | 400 | Magic sign-in link has expired or has already been used (single-use token). Fix: Request a fresh login link from /signin. |
| no_identity | 400 | Third-party OAuth provider did not return a verified email identity. Fix: Ensure your GitHub or Google account has a verified primary email address. |
| state_invalid | 400 | OAuth state token mismatch (cross-site request forgery prevention). Fix: Clear cookies and initiate sign-in again from the official login page. |
| unsupported_provider | 400 | Unrecognized authentication provider. Fix: Sign in using email (magic link), github, or google. |
| oauth_not_configured | 501 | The requested OAuth provider is not enabled on this environment. Fix: Use email magic link authentication. |