> ## Documentation Index
> Fetch the complete documentation index at: https://docs.drin.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Inbound & threads

> Give a domain an inbox, publish one MX record, and read the full conversation. Threads join inbound and outbound messages oldest to newest, the inbound_received webhook fires on every delivery, and POST /v1/inbound/simulate lets you test the whole loop with zero DNS.

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.

<CardGroup cols={2}>
  <Card title="Reply in-thread" icon="inbox" href="/receiving/reply">
    One call sets From, To, the `Re:` subject, and the threading headers for you.
  </Card>

  <Card title="Webhooks" icon="bell" href="/webhooks">
    Subscribe to `inbound_received` to be pushed every arriving message instead of polling.
  </Card>
</CardGroup>

## 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`**

<CodeGroup>
  ```bash cURL theme={null}
  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 }'
  ```

  ```typescript Node.js theme={null}
  import { DrinClient } from "@drin00/sdk";

  const drin = new DrinClient({ apiKey: process.env.DRIN_API_KEY });

  // Returns { enabled: true, records: [ ...the MX record to publish ] }
  const { records } = await drin.domains.setReceiving("dom_8a1f...", true);
  console.log(records);
  ```
</CodeGroup>

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](#simulate) works regardless.

```json 200 OK theme={null}
{
  "enabled": true,
  "records": [
    {
      "type": "MX",
      "name": "yourdomain.com",
      "value": "inbound.drin.run",
      "priority": 10,
      "purpose": "verification",
      "verified": false
    }
  ]
}
```

## 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.

```text DNS record theme={null}
Type      MX
Name      yourdomain.com
Value     inbound.drin.run
Priority  10
```

<Tip>
  **Use a subdomain for a clean split.** Receiving 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.
</Tip>

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`.

## 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`**

<CodeGroup>
  ```bash cURL theme={null}
  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"
    }'
  ```

  ```typescript Node.js theme={null}
  await drin.inboxes.create({
    address: "support@yourdomain.com",
    domainId: "dom_8a1f...",
    displayName: "Acme Support",
  });
  ```
</CodeGroup>

```json 201 Created theme={null}
{
  "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.

## 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`**

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.drin.run/v1/threads?inboxId=inb_4c2e...&limit=20" \
    -H "Authorization: Bearer $DRIN_API_KEY"
  ```

  ```typescript Node.js theme={null}
  // One page (filter by inbox, or omit inboxId for all)
  const { data, nextCursor } = await drin.threads.list({
    inboxId: "inb_4c2e...",
    limit: 20,
  });

  // Or walk every thread, lazily paged
  for await (const thread of drin.threads.paginate()) {
    console.log(thread.subject, thread.lastMessageAt);
  }
  ```
</CodeGroup>

`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}`**

```json GET /v1/threads/{id} theme={null}
{
  "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`.

## 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.

```json inbound_received delivery theme={null}
{
  "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?"
  }
}
```

<Info>
  **Always verify the signature.** Every 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](/webhooks) for the scheme and `drin.webhooks.verify()`.
</Info>

## Test the whole loop with zero DNS \[#simulate]

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`**

<CodeGroup>
  ```bash cURL theme={null}
  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."
    }'
  ```

  ```typescript Node.js theme={null}
  const result = await drin.inbound.simulate({
    to: "support@yourdomain.com",
    from: { email: "carol@example.com", name: "Carol" },
    subject: "Where is my order?",
    text: "Order #4182 still hasn't shipped.",
  });

  // { messageId, threadId, webhookDeliveries: ["whd_..."] }
  console.log(result.threadId);
  ```
</CodeGroup>

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.

```json 202 Accepted theme={null}
{
  "messageId": "msg_in_01",
  "threadId": "thr_77af...",
  "webhookDeliveries": ["whd_3e10..."]
}
```

<Steps>
  <Step title="Create a test inbox">
    `POST /v1/inboxes` for an address like `support@yourdomain.com`. No DNS required for simulation.
  </Step>

  <Step title="Subscribe your endpoint">
    Create a webhook for `inbound_received` and keep its signing secret.
  </Step>

  <Step title="Fire a synthetic delivery">
    `POST /v1/inbound/simulate` — your endpoint receives a signed, `test_mode` event identical in shape to a real one.
  </Step>

  <Step title="Reply in-thread">
    Take the returned `messageId` and call `POST /v1/emails/{id}/reply` to close the loop.
  </Step>
</Steps>

<Card title="Reply in one call" icon="inbox" href="/receiving/reply">
  Threading headers, From, To, and the `Re:` subject are all derived from the parent message — see how.
</Card>
