SendTo.it

sendto.it documentation

sendto.it turns email into HTTP. You create an inbox, get an address such as k7x9m2pqra{{ $shared }}, and each message sent to it is POSTed to your webhook as signed JSON.

  1. Create an inbox, optionally on your own domain (Inboxes, Your own domain).
  2. Set its webhook and keep the signing secret (Webhooks).
  3. Hand out the address, with a tag if you like (Tags).

API calls carry a key's secret: Authorization: Bearer ss_…. Keep it on your server: never in a web page or an app people download.

The API lives at https://sendto.it/api/v1. Bodies are JSON; times are ISO 8601 in UTC.

Inboxes

POST /api/v1/inboxes
{"name": "Support log", "prefix": "support", "external_ref": "org-42",
 "webhook_url": "https://your-app.example/hooks/sendto"}

201 {"id": "inb_3ZpQ7kLm", "address": "support-k7x9m2pqra{{ $shared }}", "status": "active",
     "webhook_url": "https://your-app.example/hooks/sendto", "webhook_secret": "whsec_…", …}

On in.sendto.it an address is always a random part, after your optional prefix, so it can't be guessed. On your own verified domain you may choose the local_part instead: {"domain": "log.example.com", "local_part": "acme"}. external_ref is your own id for the inbox; filter with GET /api/v1/inboxes?external_ref=org-42.

  • GET /api/v1/inboxes, GET /api/v1/inboxes/{id}
  • PATCH /api/v1/inboxes/{id} — name, external_ref, status (active or paused: mail to a paused inbox is refused), max_bytes
  • PUT /api/v1/inboxes/{id}/webhook {"url": "…"} — returns a new webhook_secret, shown once; DELETE /api/v1/inboxes/{id}/webhook
  • DELETE /api/v1/inboxes/{id} — mail to it is refused from then on; the address is never reused.

Mail for addresses that don't exist is refused while the sending server is still connected (550), so the sender learns at once and nobody receives a bounce they didn't cause.

Tags

Anything after a + in the address reaches the same inbox and is passed on as envelope.tag: mail to support-k7x9m2pqra+order-1234{{ $shared }} arrives with "tag": "order-1234". Use tags to say what a message is about — a customer, a job, a ticket — without creating an inbox for each. The tag keeps the sender's capitals. It also works for BCC: the address a message was delivered to is in envelope.to even when no header names it.

Your own domain

POST /api/v1/domains   {"name": "log.example.com"}

201 {"name": "log.example.com", "verified": false, "dns": [
  {"type": "TXT", "name": "_sendto.log.example.com", "value": "sendto-verify=…"},
  {"type": "MX",  "name": "log.example.com", "value": "mx.sendto.it", "priority": 10}]}

Publish both records, then POST /api/v1/domains/log.example.com/verify (we also check every 15 minutes for two weeks). Once the TXT record is seen the domain is yours on sendto.it and inboxes can use it. A subdomain such as log. leaves your main domain's mail untouched. DELETE /api/v1/domains/{name} once no inbox uses it.

Webhooks

POST https://your-app.example/hooks/sendto
webhook-id: msg_8Kq2…
webhook-timestamp: 1762099512
webhook-signature: v1,K5o…=
Content-Type: application/json

{"type": "message.received", "id": "msg_8Kq2…", …}

They follow the Standard Webhooks scheme, so its libraries verify them. By hand: base64-decode your secret after whsec_, compute HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, base64 it, and compare with the value after v1,. Reject timestamps more than 5 minutes old.

Answer with any 2xx within 15 seconds. Otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then stop; each inbox's later messages wait behind a failing one within each round. Webhook URLs must be HTTPS on a public address; redirects aren't followed. webhook-id is the message id on every attempt: use it to ignore repeats.

What a webhook carries

{
  "type": "message.received",
  "id": "msg_8Kq2…",
  "inbox": {"id": "inb_3ZpQ7kLm", "address": "support-k7x9m2pqra{{ $shared }}", "external_ref": "org-42"},
  "received_at": "2026-11-02T14:02:11Z",
  "envelope": {"from": "bounce@vendor.example", "to": "support-k7x9m2pqra+order-1234{{ $shared }}",
               "tag": "order-1234", "client_ip": "203.0.113.7", "helo": "mail.vendor.example", "tls": true},
  "auth": {"spf": "pass", "dkim": "pass", "dmarc": "pass", "from_domain": "vendor.example", "dmarc_policy": "reject", …},
  "from": {"address": "orders@vendor.example", "name": "Vendor Orders"},
  "to": [{"address": "…", "name": null}], "cc": [], "reply_to": [],
  "subject": "Order 1234 shipped", "date": "2026-11-02T14:02:05Z",
  "message_id": "<…>", "in_reply_to": null, "references": [],
  "text": "…", "html": "…",
  "headers": [["Received", "…"], ["Subject", "Order 1234 shipped"], …],
  "attachments": [{"filename": "invoice.pdf", "content_type": "application/pdf", "size": 48211,
                   "sha256": "…", "content_id": null, "inline": false, "url": "https://sendto.it/a/msg_8Kq2…/0?expires=…&signature=…"}],
  "size": 61234,
  "raw_url": "https://sendto.it/a/msg_8Kq2…/raw?expires=…&signature=…"
}

Text and HTML are always UTF-8, whatever the message used. Attachment and raw_url links (the message exactly as received) work for 72 hours, and never after the message is deleted: fetch what you want to keep.

Sender checks

Anyone can put any address in a From line. auth says whether to believe it:

  • spf — was the sending server allowed to send for the envelope sender's domain?
  • dkim — did a signature on the message verify? auth.dkim lists each signature's domain and result.
  • dmarc — pass when SPF or DKIM passed for the From line's own domain. This is the one to check before trusting who a message is from; dmarc_policy is what that domain asks receivers to do with failures.

Errors

Errors are {"error": {"code": "…", "message": "…"}}, with details for invalid fields. Program against the code; messages may be reworded. Codes are only ever added.

CodeHTTPMeaning
invalid_key401The key is unknown, revoked, expired, or lacks the scope for this.
account_suspended403This account is suspended.
rate_limited429Too many requests in the last minute for this account's plan.
not_found404No such item on this account.
invalid_request422The request is not valid.
inbox_limit422This account's plan allows no more inboxes.
domain_unverified422The domain is not verified yet: publish its TXT record, then verify it.
domain_taken409That domain is already registered with sendto.it.
domain_in_use409Inboxes still use this domain; delete them first.
address_taken409That address is already in use.
no_webhook409This inbox has no webhook.
invalid_url422The URL is not a valid https address.
blocked_address422The URL points at a private or reserved network address.

Plans and limits

PlanInboxesMessages a dayAPI calls a minute
Trial3 20060
Starter25 5,000120
Business500 50,000600

Messages up to 25 MB as sent (attachments are about a third larger encoded than on disk), up to 50 recipients each. Beyond the daily limit, messages are refused rather than stored.

Privacy and retention

  • Messages and attachments are encrypted while stored and deleted 7 days after they arrive, delivered or not.
  • Delivery history (when, where, the status code) is kept 30 days; it holds no message content.
  • Each time message content is opened in sendto.it's admin pages, it is recorded.
  • sendto.it only receives mail: it never relays, forwards by email, or sends bounces.
  • Questions or requests: contact@scaledapps.com. See also the terms and privacy policy.