delivery source answers the question your agent logs cannot: did the message actually arrive?
A log drain 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.
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.Event names
failed is the only ERROR in the set, and it is the transition worth paging on. The Datadog monitor pack 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 carrieslua.log.type: "log" and lua.source: "delivery", like everything else in the stream. Beyond those:
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:
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:
--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 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:records[].id, as with every drain — never on X-Lua-Batch-Id.
Environment
A delivery has no environment of its own, so everylua.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.readis 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.expiredis 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 statusreports the drain’s own delivered, dropped and rejected counts; per-channel delivery outcomes are something you aggregate at the destination fromlua.delivery.statusandlua.channel. - A newly created or newly edited drain starts (or stops) receiving delivery records within about 30 seconds, not instantly.
Select it
--sources replaces the drain’s list rather than adding to it, so name every source you want to keep.
See also
Ship logs to your stack
Every selectable source, and what a drain is.
Event schema
The full attribute catalogue and the content rule.
Workflow audit
The other phase-2 source with its own page.
lua drains
Every verb, flag, and
--json shape.
