Inbound & threads
Receiving turns Drin from a send-only API into a two-way channel. Give a domain an inbox, publish one MX record, and every email that arrives is parsed, threaded with your outbound messages, and pushed to a webhook — ready for an app or an agent to read and reply.
The receiving loop has four moving parts. Enable it on a domain, point one MX record at Drin, create at least one inbox address, then read threads — a chronological join of inbound and outbound messages for the same conversation.
Re: subject, and the threading headers for you.WebhooksSubscribe to inbound_received to be pushed every arriving message instead of polling.01 Enable receiving on a domain
Receiving is opt-in per domain. Flip it on with a single PATCH; the response carries the MX record you need to publish. The domain must already be added to your account and verified for sending.
/v1/domains/{id}/receivingcurl -X PATCH https://api.drin.run/v1/domains/dom_8a1f.../receiving \
-H "Authorization: Bearer $DRIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'The response echoes the receiving state and the DNS records to add. Until the MX record resolves, verified stays false and real mail will not route to you — but simulation works regardless.
{
"enabled": true,
"records": [
{
"type": "MX",
"name": "yourdomain.com",
"value": "inbound.drin.run",
"priority": 10,
"purpose": "verification",
"verified": false
}
]
}02 Publish the one MX record
Add a single MX record at your domain's apex (or the subdomain you want to receive on), pointing at Drin's inbound host at priority 10. That is the whole DNS change — there is no second record and nothing to rotate. In the dashboard, DNS hosts that support Domain Connect can write this MX record for you after you enable receiving.
Type MX
Name yourdomain.com
Value inbound.drin.run
Priority 10inbox.yourdomain.com at Drin and create inboxes there instead.DNS changes propagate on their own schedule. Re-read the state any time with GET /v1/domains/{id}/receiving (or drin.domains.getReceiving(id)) — once the record resolves, verified flips to true.
03 Create an inbox address
A domain can route to many addresses. Create an inbox for each one you want to receive at — support@, billing@, or a dedicated address for an agent. Only mail addressed to an inbox you own is accepted; everything else is rejected at ingest.
/v1/inboxescurl https://api.drin.run/v1/inboxes \
-H "Authorization: Bearer $DRIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"address": "support@yourdomain.com",
"domainId": "dom_8a1f...",
"displayName": "Acme Support"
}'{
"id": "inb_4c2e...",
"senderId": "snd_1b9d...",
"domainId": "dom_8a1f...",
"address": "support@yourdomain.com",
"displayName": "Acme Support",
"type": "agent",
"createdAt": "2026-06-02T12:00:00.000Z"
}List inboxes with drin.inboxes.list(), or narrow to one domain with drin.inboxes.listByDomain(domainId). Deleting an inbox is refused with 409 conflict while it still holds messages, so the audit trail can't be erased.
04 Read the conversation as a thread
A thread is the unit you read from. Drin groups every message that belongs to the same conversation — both directions — and orders them oldest to newest. An inbound question and the outbound answer you sent back live in one list, so an app or an agent sees the full exchange without stitching events together.
/v1/threadscurl "https://api.drin.run/v1/threads?inboxId=inb_4c2e...&limit=20" \
-H "Authorization: Bearer $DRIN_API_KEY"GET /v1/threads returns a page of thread summaries ({ data, nextCursor }). Pass inboxId to scope the feed to one receive address, or omit it for every inbox. To read the messages, fetch one thread by id:
/v1/threads/{id}{
"id": "thr_77af...",
"senderId": "snd_1b9d...",
"inboxId": "inb_4c2e...",
"subject": "Where is my order?",
"threadKey": "where-is-my-order",
"lastMessageAt": "2026-06-02T12:04:10.000Z",
"createdAt": "2026-06-02T12:00:00.000Z",
"messages": [
{
"id": "msg_in_01",
"direction": "inbound",
"status": "received",
"from": "carol@example.com",
"to": ["support@yourdomain.com"],
"subject": "Where is my order?",
"occurredAt": "2026-06-02T12:00:00.000Z",
"testMode": false
},
{
"id": "msg_out_02",
"direction": "outbound",
"status": "delivered",
"from": "support@yourdomain.com",
"to": ["carol@example.com"],
"subject": "Re: Where is my order?",
"occurredAt": "2026-06-02T12:04:10.000Z",
"repliesToMessageId": "msg_in_01",
"testMode": false
}
]
}Each message carries its direction (inbound / outbound), status, addresses, occurredAt timestamp, and repliesToMessageId when it was sent as a reply. Fetch the rendered body or parsed attachments of any message with GET /v1/emails/{id}/body and /attachments.
05 Get pushed every arrival: inbound_received
Polling threads is fine for a cron loop, but the responsive path is a webhook. Subscribe to the inbound_received event and Drin POSTs you a signed payload the moment a message lands — with the new message id and thread id so you can fetch the conversation and reply.
{
"id": "evt_91bc...",
"type": "inbound_received",
"createdAt": "2026-06-02T12:00:00.000Z",
"data": {
"messageId": "msg_in_01",
"senderId": "snd_1b9d...",
"threadId": "thr_77af...",
"from": "carol@example.com",
"subject": "Where is my order?"
}
}Drin-Signature header. Verify it before trusting the body — the SDK does it in one call. See the webhooks guide for the scheme and drin.webhooks.verify().06 Test the whole loop with zero DNS
You shouldn't have to send a real email from Gmail to test your inbound integration. POST /v1/inbound/simulate synthesizes a delivery to one of your inboxes, runs the full ingest pipeline, threads it, and fires your webhook — exactly like a real arrival. The persisted row is flagged test_mode, so it is excluded from metrics, quotas, and billing, and it works even before your MX record resolves.
/v1/inbound/simulatecurl https://api.drin.run/v1/inbound/simulate \
-H "Authorization: Bearer $DRIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "support@yourdomain.com",
"from": { "email": "carol@example.com", "name": "Carol" },
"subject": "Where is my order?",
"text": "Order #4182 still hasnt shipped."
}'The to must resolve to an inbox you own. Supply html, text, or both — the response returns the synthesized message id, its thread, and the ids of the webhook deliveries it triggered, so a test can assert your endpoint actually received the event.
{
"messageId": "msg_in_01",
"threadId": "thr_77af...",
"webhookDeliveries": ["whd_3e10..."]
}- 1
Create a test inbox
POST /v1/inboxesfor an address likesupport@yourdomain.com. No DNS required for simulation. - 2
Subscribe your endpoint
Create a webhook forinbound_receivedand keep its signing secret. - 3
Fire a synthetic delivery
POST /v1/inbound/simulate— your endpoint receives a signed,test_modeevent identical in shape to a real one. - 4
Reply in-thread
Take the returnedmessageIdand callPOST /v1/emails/{id}/replyto close the loop.
Re: subject are all derived from the parent message — see how.