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.
- Create an inbox, optionally on your own domain (Inboxes, Your own domain).
- Set its webhook and keep the signing secret (Webhooks).
- 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(activeorpaused: mail to a paused inbox is refused),max_bytesPUT /api/v1/inboxes/{id}/webhook{"url": "…"}— returns a newwebhook_secret, shown once;DELETE /api/v1/inboxes/{id}/webhookDELETE /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.dkimlists each signature's domain and result.dmarc—passwhen 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_policyis 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.
| Code | HTTP | Meaning |
|---|---|---|
invalid_key | 401 | The key is unknown, revoked, expired, or lacks the scope for this. |
account_suspended | 403 | This account is suspended. |
rate_limited | 429 | Too many requests in the last minute for this account's plan. |
not_found | 404 | No such item on this account. |
invalid_request | 422 | The request is not valid. |
inbox_limit | 422 | This account's plan allows no more inboxes. |
domain_unverified | 422 | The domain is not verified yet: publish its TXT record, then verify it. |
domain_taken | 409 | That domain is already registered with sendto.it. |
domain_in_use | 409 | Inboxes still use this domain; delete them first. |
address_taken | 409 | That address is already in use. |
no_webhook | 409 | This inbox has no webhook. |
invalid_url | 422 | The URL is not a valid https address. |
blocked_address | 422 | The URL points at a private or reserved network address. |
Plans and limits
| Plan | Inboxes | Messages a day | API calls a minute |
|---|---|---|---|
| Trial | 3 | 200 | 60 |
| Starter | 25 | 5,000 | 120 |
| Business | 500 | 50,000 | 600 |
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.