Skip to main content
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.
The 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 carries lua.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:
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 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:
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

Add it to a drain that predates the release:
--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.