Inbound messages

Receive email at a domain you have verified with Sendoka, and SMS replies at a number you own. Every received message is stored, announced by webhook (inbound.email / inbound.sms), and readable back with GET /v1/inbound — the webhook is the fast path, the API the recovery path.

SMS replies need no setup beyond a number in a messaging pool; the rest of this page is about email.

Receive email at a domain

  1. Verify the domain for sending first (see domains.md). Inbound is refused on a pending domain with 409 DOMAIN_NOT_VERIFIED, and on your organization's {org-slug}.sandbox.sendoka.com sender with 409 DOMAIN_PLATFORM_MANAGED.

  2. Enable inbound — Overview → Domains → the domain → Inbound email → Enable, or:

    curl -X POST https://www.sendoka.com/api/v1/domains/dom_123/inbound \
      -H "Authorization: Bearer $SENDOKA_KEY"
    
    {
      "domain_id": "dom_123",
      "domain": "acme.com",
      "enabled": true,
      "available": true,
      "region": "us-east-1",
      "mx_record": {
        "type": "MX",
        "name": "acme.com",
        "value": "inbound-smtp.us-east-1.amazonaws.com",
        "priority": 10
      },
      "simulated": false
    }
    
  3. Publish the MX record from mx_record at your DNS provider. Mail starts arriving once it resolves.

GET /v1/domains/{id}/inbound reads the current state and the record again; DELETE turns inbound off (remove the MX record too — messages already received are kept). Enabling and disabling are idempotent and need write:domains; reading needs read:domains. In the dashboard, any member can see the state and only developers and owners can change it. Removing the domain also turns its inbound off.

Inbound follows verification. If the domain stops being verified (its DKIM records are removed or changed, and a recheck notices), inbound is turned off at the same time and you receive domain.unverified. Mail sent to the domain is then rejected and bounced to the sender, not accepted and lost. Once the domain verifies again, enable inbound again.

The MX record takes over the domain's mail. A domain has one set of MX records, and mail goes to whichever host they name. If people already receive mail at acme.com through Google Workspace, Microsoft 365 or any other mailbox provider, do not point acme.com's MX at Sendoka — add a subdomain such as inbound.acme.com as its own domain, verify it, and enable inbound there. Replies can be steered to it with a Reply-To header on what you send.

Inbound accepts every address at the domain (support@, anything@) but not its subdomains — x.acme.com is a domain of its own and needs its own row.

Test keys

With a sok_test_* key, POST and DELETE run the same checks (verified, not the sandbox sender) and answer simulated: true without changing anything; GET answers enabled: false, simulated: true because a test key does not read live routing. Use a live key to actually receive mail.

Errors

Code When
DOMAIN_NOT_VERIFIED (409) The domain is still pending.
DOMAIN_PLATFORM_MANAGED (409) The org's sandbox sender.
INBOUND_REGION_IDENTITY_MISSING (409) Mail is received in one region (region in the response) and the domain's identity is not verified there. Contact support.
INBOUND_NOT_CONFIGURED (503) Inbound email is not available on this deployment. GET reports it as available: false.
PROVIDER_UNAVAILABLE (502) The email provider could not be reached. Retry.

What you receive

The inbound.email webhook

{
  "event": "inbound.email",
  "data": {
    "inbound_message_id": "inb_V1StGXR8_Z5jdHi6B-myT0aB",
    "from": "jane@customer.example",
    "from_name": "Jane Doe",
    "envelope_from": "0100018f…@amazonses.com",
    "to": "support@acme.com",
    "subject": "Invoice question",
    "attachments": [
      {
        "index": 0,
        "filename": "invoice.pdf",
        "content_type": "application/pdf",
        "size": 48213,
        "disposition": "attachment",
        "content_id": null,
        "downloadable": true
      }
    ],
    "verdicts": {
      "spam": "PASS",
      "virus": "PASS",
      "spf": "PASS",
      "dkim": "PASS",
      "dmarc": "PASS"
    }
  },
  "timestamp": "2026-10-05T12:00:00.000Z",
  "tenant_id": null,
  "delivery_id": "whd_01HN..."
}

The envelope is the usual one (events.md), scoped to the domain's tenant. It carries no environment: there is no send behind it.

  • from is the address in the message's From header — who the person sent it as. envelope_from is the SMTP MAIL FROM (the Return-Path), which for mail sent through an email service is usually a bounce address of that service, not a person. Reply to from (or to Reply-To, from the headers).
  • The payload carries no bodies and no download links: webhook payloads are stored and retried for days, longer than a download link lives. Fetch the message for its text, HTML and headers.
  • verdicts — see Spam, virus and authentication verdicts.
  • It fires once the message has been fully read, its body and attachments included. For a few seconds before that, GET /v1/inbound can already list the message, with text and html still null and no attachments.

GET /v1/inbound/{id}

The whole message: from, to, subject, text, html, headers, message_id_header, in_reply_to, references, conversation_id, attachments (the same objects as in the webhook) and verdicts. GET /v1/inbound lists messages with a preview; filter with channel=email, to, from, q and a date range.

Bodies are decoded (transfer encodings and charsets) and each is kept up to 256 KB of text.

Attachments

curl https://www.sendoka.com/api/v1/inbound/inb_123/attachments/0 \
  -H "Authorization: Bearer $SENDOKA_KEY"
{
  "index": 0,
  "filename": "invoice.pdf",
  "content_type": "application/pdf",
  "size": 48213,
  "disposition": "attachment",
  "content_id": null,
  "downloadable": true,
  "url": "https://…amazonaws.com/attachments/…?X-Amz-Signature=…",
  "expires_at": "2026-10-05T12:10:00.000Z"
}

url is a direct download valid for 10 minutes; request a new one whenever it expires. Fetch it without your Authorization header — the link carries its own signature, and the storage host refuses a request that has both. Inline images (disposition: "inline", with a content_id the HTML references as cid:) are listed and downloadable the same way.

downloadable: false (and 404 ATTACHMENT_NOT_STORED from the download route) means the attachment was seen but its contents were not kept: only the first 50 attachments of a message are stored, none are stored for a message whose verdicts.virus is FAIL, and messages received before attachments were stored list none at all.

Spam, virus and authentication verdicts

Every received message is scanned, and the results come with it, in the webhook and in GET /v1/inbound/{id}:

"verdicts": { "spam": "PASS", "virus": "PASS", "spf": "PASS", "dkim": "GRAY", "dmarc": "PASS" }

Each value is PASS, FAIL, GRAY (no verdict could be reached), PROCESSING_FAILED, or DISABLED, and null when no verdict was recorded (messages received before verdicts were kept have verdicts: null).

A verdict does not stop the message reaching you. You decide what to do with spam or a failed SPF, DKIM or DMARC check. The one exception is a virus: when virus is FAIL, the message is stored and announced as usual, but its attachments are listed with downloadable: false and their contents are not kept.

Limits

  • Message size: 40 MB, headers and all attachments included, after encoding — the receiving server's own limit. Larger messages are rejected at the MX and the sender gets a bounce.
  • Attachments stored per message: 50. The rest are listed with downloadable: false.
  • Body text kept: 256 KB each for text and html.
  • Headers kept: 64 KB. A message whose header block is larger keeps Message-ID, In-Reply-To and References first, then the other headers in order until the limit; the rest are left out of headers. Threading (conversation_id) is still read from the full header block, and a long references value keeps its last 8 KB, which holds the ids it threads on.
  • Retention: received messages and their attachments are deleted after Sendoka's inbound retention period of 90 days. Erasing a tenant deletes its received messages at once, but the stored originals and attachments are not removed early: they expire on the same 90-day schedule. Until then they can no longer be reached through the API, because the messages that pointed at them are gone.
  • Messages are scanned for spam and viruses. The verdicts are reported to you and do not block delivery, except that a message with a virus has no attachments stored (see above).