Why the envelope matters more here
Beckn has no synchronous responses that carry business data. A BAP sends search and receives 200 ACK; the actual catalogue arrives later as a separate HTTP request in the opposite direction, possibly from several providers, possibly seconds apart.
That inversion means the reply cannot be matched to the request by connection. It has to be matched by data, and context is that data. In a synchronous protocol the envelope is bookkeeping; here it is the mechanism.
The fields
| Field | Purpose | Changes |
|---|---|---|
domain | Which network — e.g. ev-charging:uei | Never within a transaction |
action | search, on_search, select… | Every message |
version | Core spec version, e.g. 1.1.0 | Never |
bap_id / bap_uri | Buyer platform identity and callback address | Never |
bpp_id / bpp_uri | Provider identity and address | Set once a provider is chosen |
transaction_id | The whole business transaction | Never |
message_id | One request and its callback | Every request |
timestamp | When sent, RFC3339 | Every message |
ttl | How long this is valid, ISO 8601 duration | Per message |
location | Country and city for discovery routing | Rarely |
transaction_id versus message_id
This is the distinction that causes the most damage when it is missed.
transaction_id is the business transaction. One driver deciding to charge, from the first search through to the final settled quote. It survives every action in the chain. It is what you use to reconstruct what happened when a customer calls three weeks later.
message_id is one request and its callback. A new one for every action you send. It is what tells you that this on_select answers that select.
transaction_id: txn-4471 ← constant throughout
search message_id: msg-9021 → on_search msg-9021
select message_id: msg-9022 → on_select msg-9022
init message_id: msg-9023 → on_init msg-9023
confirm message_id: msg-9024 → on_confirm msg-9024
status message_id: msg-9031 → on_status msg-9031message_id across actions. It appears to work in testing, where one action completes before the next begins. In production, where a slow on_select can land after init has been sent, callbacks get applied to the wrong step. The symptom is corrupted orders that nobody can reproduce.The mirror-image bug is generating a fresh transaction_id per action, which destroys the ability to correlate anything and turns a five-step transaction into five unrelated events.
Search is the exception
One search produces many on_search callbacks — one per responding BPP. They all carry the same transaction_id and the same message_id, distinguished only by bpp_id.
So the correlation key for search results is the triple (transaction_id, message_id, bpp_id). Storing catalogue responses keyed on message_id alone means each provider's catalogue overwrites the last, and the driver sees results from whichever BPP happened to answer slowest.
TTL, and honouring it
ttl is an ISO 8601 duration — PT30S for thirty seconds, PT2M for two minutes. It states how long the message is meaningful.
- As a sender, set it to something the receiver can actually meet. A one-second TTL on a search that fans out across a network guarantees failure.
- As a receiver, stop processing once it expires. Answering a search whose TTL lapsed wastes work nobody will read.
- On callbacks, discard anything arriving after its window. A quote that lands after the driver moved on must not be applied — showing a price they never saw is worse than showing nothing.
TTL is also your defence against unbounded state. Without it, a BAP accumulates open transactions waiting for callbacks that will never come.
Production lessons
- Generate
transaction_idonce, at the first search, and thread it through everything. - Generate a fresh
message_idper outbound request. Never reuse. - Key search results on
(transaction_id, message_id, bpp_id). - Log the full context on every message, both directions. It is the entire audit trail, and partner disputes are unresolvable without it.
- Validate
bap_uriagainst the registry, do not trust the payload. An unvalidated callback URI is an open redirect — see signing and the registry. - Enforce TTL on both sides, and expire stale transactions rather than letting them accumulate.
- Never mutate
domainorversionmid-transaction.
Frequently asked questions
transaction_id identifies the whole business transaction and stays constant from the first search through to completion. message_id identifies one request and its matching callback, and must be new for every request sent. Conflating them causes callbacks to be applied to the wrong step.
By the triple transaction_id, message_id and bpp_id. One search produces many on_search callbacks that share the same transaction_id and message_id, distinguished only by which provider sent them, so keying on message_id alone causes each catalogue to overwrite the last.
An ISO 8601 duration stating how long the message remains meaningful — PT30S is thirty seconds. Receivers should stop processing expired requests, and senders should discard callbacks that arrive after the window, since applying a stale quote shows a driver a price they never saw.
Because Beckn callbacks arrive as separate HTTP requests in the opposite direction, often from multiple providers seconds apart. The reply cannot be matched to the request by connection, so it has to be matched by data, and context carries that data.