Docs
guides · receiving

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.

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.

PATCH/v1/domains/{id}/receiving
curl -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.

200 OK
{
  "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.

DNS record
Type      MX
Name      yourdomain.com
Value     inbound.drin.run
Priority  10
Use a subdomain for a clean splitReceiving on the apex shares the MX namespace with any existing mailbox provider. If the domain already receives human mail, point a subdomain like inbox.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.

POST/v1/inboxes
curl 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"
  }'
201 Created
{
  "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.

GET/v1/threads
curl "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:

GET/v1/threads/{id}
GET /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.

inbound_received delivery
{
  "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?"
  }
}
Always verify the signatureEvery delivery is signed with a 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.

POST/v1/inbound/simulate
curl 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.

202 Accepted
{
  "messageId": "msg_in_01",
  "threadId": "thr_77af...",
  "webhookDeliveries": ["whd_3e10..."]
}
  1. 1

    Create a test inbox

    POST /v1/inboxes for an address like support@yourdomain.com. No DNS required for simulation.
  2. 2

    Subscribe your endpoint

    Create a webhook for inbound_received and keep its signing secret.
  3. 3

    Fire a synthetic delivery

    POST /v1/inbound/simulate — your endpoint receives a signed, test_mode event identical in shape to a real one.
  4. 4

    Reply in-thread

    Take the returned messageId and call POST /v1/emails/{id}/reply to close the loop.
Reply in one callThreading headers, From, To, and the Re: subject are all derived from the parent message — see how.