Skip to content

How Do I Attach Custom Metadata (Tenant / Campaign) to a Message?

The standard pattern for SMTP injection is to pass a value in a header, copy it into message metadata, and (optionally) strip the header so it is not transmitted to the recipient. If you inject over the HTTP API instead, you can attach metadata directly on the request — see below. Either way, metadata travels with the message through retries and can be logged, but is never sent on the wire.

Tenant and campaign

The queue helper does this for you via header options:

# queues.toml
tenant_header = "X-Tenant"
remove_tenant_header = true
campaign_header = "X-Campaign"
remove_campaign_header = true

Or set it explicitly in Lua:

kumo.on('smtp_server_message_received', function(msg)
  local tenant = msg:get_first_named_header_value 'X-Tenant'
  if tenant then
    msg:set_meta('tenant', tenant)
  end
end)

Arbitrary custom metadata

Capture any header into metadata and log it. This is cheaper than logging the header directly:

kumo.on('smtp_server_message_received', function(msg)
  msg:import_x_headers { 'x-client-id' }
end)
kumo.configure_local_logs {
  meta = { 'tenant', 'campaign', 'x_client_id' },
}

Note

To see tenant/campaign in log-hook records, include them in log_parameters at injection. Inside a log hook the message is a log-record message and does not carry the original message's meta.

Attaching metadata via the HTTP injection API

If you inject over the HTTP injection API rather than SMTP, skip the header round-trip: each recipient accepts a metadata object that is attached directly to the generated message.

{
  "envelope_sender": "noreply@example.com",
  "content": "Subject: hello\n\nHello there",
  "recipients": [
    {
      "email": "recipient@example.com",
      "metadata": {
        "campaign_id": "promo-2026-q2",
        "user_segment": "premium"
      }
    }
  ]
}

These key/value pairs land under the extra metadata key, so read them from Lua via the extra table rather than as top-level meta:

kumo.on('http_message_generated', function(msg)
  local extra = msg:get_meta 'extra'
  if extra then
    msg:set_meta('campaign', extra.campaign_id)
  end
end)

Note that the queue helper's tenant_header / campaign_header options act on headers, not on this extra metadata. To drive tenant/campaign from an HTTP injection, either set the corresponding headers in content (or the headers field) so the queue helper picks them up, or copy the values out of extra into tenant / campaign meta in http_message_generated as shown above.

See also