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

# Delivery records

> The delivery source: one record per outbound message status transition, its seven statuses and severities, the masked recipient, and the two statuses nothing reports yet

<Warning>
  **Available from lua-cli 3.39 and the platform release of 23 September 2026.** Until that release lands, `--sources delivery` is refused and no `lua.delivery.*` record is produced. The date is the one in force as this is written and is subject to change until the release is announced — treat the version as the reliable half.
</Warning>

The `delivery` source answers the question your agent logs cannot: **did the message actually arrive?**

A [log drain](/drains/overview) that selects it receives one record per **applied status transition** of an outbound message — a WhatsApp send the vendor refused, an email that bounced, a Slack post that never got a receipt — in the same stream, keyed to the same organization and agent, as the turn that produced it.

`delivery` is **on by default for a new drain**. Nothing in it is message content, a failed send is operational truth your on-call needs, and the volume is one record per message rather than per log line.

<Note>
  **Existing drains are untouched.** A drain you configured before this release keeps exactly the sources it had. Defaults apply when a drain is created and are never re-applied afterwards — add `delivery` with `lua drains update <id> --sources …` if you want it on a drain that predates it.
</Note>

## Event names

```text theme={null}
lua.delivery.<status>
```

| `eventName`              | `severityNumber` | When                                                                                          |
| ------------------------ | ---------------- | --------------------------------------------------------------------------------------------- |
| `lua.delivery.queued`    | `9` `INFO`       | Held behind a closed WhatsApp window. No vendor message id yet                                |
| `lua.delivery.accepted`  | `9` `INFO`       | The vendor accepted the send and returned a message id                                        |
| `lua.delivery.sent`      | `9` `INFO`       | Accepted on a provider that reports no receipts — terminal there                              |
| `lua.delivery.delivered` | `9` `INFO`       | The vendor confirmed delivery to the handset or mailbox                                       |
| `lua.delivery.read`      | `9` `INFO`       | The vendor reported a read receipt. [Not on every channel](#two-statuses-nothing-reports-yet) |
| `lua.delivery.failed`    | `17` `ERROR`     | The vendor rejected the send, or reported a failure                                           |
| `lua.delivery.expired`   | `13` `WARN`      | Declared, [not emitted today](#two-statuses-nothing-reports-yet)                              |

`failed` is the only `ERROR` in the set, and it is the transition worth paging on. The [Datadog monitor pack](/drains/packs/datadog-monitors) counts it.

The status also rides `lua.delivery.status`, with the same value as the name's suffix, for destinations that facet on attributes rather than on the event name.

## Attributes

Every record carries `lua.log.type: "log"` and `lua.source: "delivery"`, like everything else in the stream. Beyond those:

| Attribute                          | Type   | Value                                                              |
| ---------------------------------- | ------ | ------------------------------------------------------------------ |
| `lua.delivery.id`                  | string | The delivery's own id. The join key when you ask support about one |
| `lua.channel`                      | string | `whatsapp`, `email`, `slack`, `sms`, …                             |
| `lua.provider`                     | string | The vendor that carried it                                         |
| `lua.delivery.status`              | string | The event name's own suffix                                        |
| `lua.delivery.error`               | string | `category/code: title`, at most 512 characters. Failures only      |
| `lua.delivery.provider_message_id` | string | The vendor's own message id. Absent while a send is `queued`       |
| `user.id`                          | string | The end user, when the send resolved one                           |
| `lua.delivery.recipient`           | string | The vendor address, [masked](#the-recipient-is-masked)             |

**There is no `body`.** A delivery record holds no message content by design, and this source does not invent any: `body` is absent on every `lua.delivery.*` record, with or without `--include-content`.

An absent optional attribute is omitted, never sent as an empty string.

### Reading `lua.delivery.error`

It is three fields joined, deliberately:

```text theme={null}
unreachable/131026: Message undeliverable
```

| Part                    | What it is for                                                                                |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| `unreachable`           | The canonical cross-vendor reason. **Group and alert on this** — it is stable across channels |
| `131026`                | The vendor's own code, which is what a vendor support ticket quotes                           |
| `Message undeliverable` | The title, which makes the line readable without either of the above                          |

**The vendor's free-text detail is never included.** It is the one field that has historically carried recipient addresses and customer text, so it does not leave the platform at all.

## The recipient is masked

`lua.delivery.recipient` carries the **last four characters** of the vendor address behind a horizontal ellipsis:

```text theme={null}
+447700900123     →  …0123
alice@example.com →  ….com
```

An address of four characters or fewer is masked whole. The mask is by **character** and is the same rule on every channel — it is not the local part of an email or the subscriber number of an MSISDN.

The **full** address is delivered only to a drain with `--include-content`, and it is decided per drain at send time: two drains in one organization can disagree about it, and the unmasked value never reaches the one that did not ask. Putting `lua.delivery.recipient` on the drain's [`redact` list](/reference/rest/log-drains) drops the key entirely, either way.

A drain without `--include-content` therefore receives the masked value **and** `lua.content.withheld: true` — telling you there is a fuller value you are not seeing. It is not a withheld record: the delivery, its status, its error and its masked recipient all arrive. A delivery with **no** recipient at all carries neither the attribute nor the flag, because there is nothing to unmask.

## Deduplication

The record id is derived from the delivery and the status it moved to:

```text theme={null}
dl:<deliveryId>:<status>
```

That is what makes a redelivered vendor callback, a retried write, or two workers racing the same transition collapse to one record. Deduplicate on `records[].id`, as with every drain — never on `X-Lua-Batch-Id`.

## Environment

A delivery has no environment of its own, so every `lua.delivery.*` record is read as **`production`**.

**A drain scoped to `--environments sandbox` alone therefore receives no delivery records at all.** That is the truthful answer rather than a gap: an outbound message to a real recipient left the platform for real, whatever environment produced the turn behind it. A drain that wants deliveries needs `production` in its environment list.

## Two statuses nothing reports yet

Both values stay in the table above because the mapping exists and a future per-message writer would light them up. **Do not build an alert on the absence of either.**

* **`lua.delivery.read` is not emitted for Messenger or Instagram.** Those platforms report a read watermark — a *time* before which everything was read — rather than a per-message receipt, and a watermark names no message to attach a record to. Every other channel reports a per-message read and does produce the record.
* **`lua.delivery.expired` is not emitted at all today.** Queued messages that age out are expired in bulk, by the same kind of sweep, and the sweep names no individual message either.

## Other limits worth knowing

* **A callback for a message the platform never recorded a send for produces no record.** When a vendor calls back about something with no send-side row, the platform has no organization to attribute it to, and a record with no tenant is never routable. The same is true of deliveries written before the platform recorded an organization on them.
* **Delivery volume is visible in the records, not in a separate counter.** `lua drains status` reports the drain's own delivered, dropped and rejected counts; per-channel delivery outcomes are something you aggregate at the destination from `lua.delivery.status` and `lua.channel`.
* A newly created or newly edited drain starts (or stops) receiving delivery records within about **30 seconds**, not instantly.

## Select it

```bash theme={null}
lua drains create \
  --name "Delivery watch" \
  --type http \
  --endpoint https://logs.example.com/lua \
  --sources delivery,agent_error \
  --environments production
```

Add it to a drain that predates the release:

```bash theme={null}
lua drains update drn_9f31a7c04b2e615d8a03cc71 \
  --sources skill,job,webhook,execution,agent_error,delivery
```

`--sources` replaces the drain's list rather than adding to it, so name every source you want to keep.

## See also

<Columns cols={2}>
  <Card title="Ship logs to your stack" href="/drains/overview">Every selectable source, and what a drain is.</Card>
  <Card title="Event schema" href="/drains/event-schema">The full attribute catalogue and the content rule.</Card>
  <Card title="Workflow audit" href="/drains/sources/workflow-audit">The other phase-2 source with its own page.</Card>
  <Card title="lua drains" href="/reference/cli/drains">Every verb, flag, and `--json` shape.</Card>
</Columns>
