The async pair
The BAP sends search — usually to the gateway, which fans it out to registered BPPs. Each BPP responds 200 ACK immediately and later POSTs its catalogue to the BAP's bap_uri as on_search.
// BAP -> gateway / BPPs
POST /search
{
"context": {
"domain": "ev-charging:uei",
"action": "search",
"version": "1.1.0",
"location": { "country": { "code": "IND" }, "city": { "code": "std:0120" } },
"bap_id": "emsp.example.com",
"bap_uri": "https://emsp.example.com/beckn",
"transaction_id": "txn-4471",
"message_id": "msg-9021",
"timestamp": "2026-08-11T09:00:00Z",
"ttl": "PT10S"
},
"message": {
"intent": {
"fulfillment": {
"stops": [{
"location": {
"circle": {
"gps": "28.6270,77.3721",
"radius": { "value": "5", "unit": "km" }
}
}
}]
},
"item": { "descriptor": { "code": "energy" } }
}
}
}And later, independently, from each provider:
// BPP -> BAP callback
POST https://emsp.example.com/beckn/on_search
{
"context": { "...": "same transaction_id and message_id",
"action": "on_search", "bpp_id": "cpo.electreefi.in",
"bpp_uri": "https://cpo.electreefi.in/beckn" },
"message": {
"catalog": {
"providers": [{
"id": "EFI",
"descriptor": { "name": "ElectreeFi Charging" },
"locations": [{ "id": "LOC-ND-014", "gps": "28.6270,77.3721" }],
"items": [{
"id": "EVSE-014-02",
"descriptor": { "name": "Sector 62 Hub - 60kW CCS2" },
"location_ids": ["LOC-ND-014"],
"price": { "value": "18.50", "currency": "INR/kWh" },
"tags": [{
"descriptor": { "code": "connector" },
"list": [
{ "descriptor": { "code": "type" }, "value": "CCS2" },
{ "descriptor": { "code": "power" }, "value": "60kW" },
{ "descriptor": { "code": "status" }, "value": "AVAILABLE" }
]
}]
}]
}]
}
}
}
The intent object
intent is a description of what the buyer wants, deliberately loose. A BPP matches against whatever it understands and ignores the rest, which is what allows providers with different capabilities to answer the same search.
| Intent element | Expresses | Typical EV use |
|---|---|---|
fulfillment.stops[].location.circle | Geographic area | GPS point plus radius — the most common filter |
item.descriptor.code | What is being bought | energy |
fulfillment.stops[].time | When | Planned arrival, for future booking |
item.tags | Item characteristics | Connector type, minimum power |
provider.descriptor | A specific provider | Searching one known operator |
Implementing it without pain
- Correlate on
transaction_id+message_id+bpp_id. Multiple BPPs answer one search over seconds, all sharing the first two fields. Keying onmessage_idalone means each catalogue overwrites the last — see the context object. - Never block. The BAP aggregates as callbacks arrive. There is no completion signal and no expected count.
- Set a UX budget: render as results stream in, close at roughly 3–5 seconds. A driver will not wait for the slowest participant on the network.
- BPP side: answer from a pre-built catalogue cache, not a live database fan-out. Search volume on an open network is not under your control.
- Validate signatures on every callback. The registry says who may speak; the signature proves who did. See signing and the registry.
What to do with the results
Raw catalogues are not a driver-facing list. Four things happen between callback and screen:
- Deduplicate. The same physical site can arrive from more than one BPP where a CPO is reachable through multiple aggregators. Match on location and connector rather than on ID, since IDs differ per provider.
- Normalise price. Providers quote differently — per kWh, per minute, with or without tax. Comparing them requires converting to a common basis, and showing an inconsistent basis to a driver is worse than showing none.
- Rank deliberately. Distance, price, power and availability all matter, and the weighting is a product decision. Arrival order is not a ranking.
- Age the availability. Catalogue status is a snapshot from when the BPP answered. By the time the driver taps, it may be stale — which is why select re-checks.
After search
Discovery flows into the transaction chain: select confirms the item and returns a quote, init and confirm create the booking, then update and status track the session — each with its on_* callback twin, each correlated by the same transaction_id.
The search pattern you build first becomes the template for everything after it: send, acknowledge, wait for a callback, correlate, expire on TTL. Get the plumbing right here and the remaining six actions are variations on it.
Frequently asked questions
The BAP sends one search request, usually to the gateway, which forwards it to every BPP subscribed to that domain and location. Each BPP acknowledges immediately and later POSTs its catalogue back to the BAP's callback address as on_search.
It does not. There is no response count and no completion signal, and some providers never answer. A BAP renders results progressively as callbacks arrive and closes the window on a timer, typically three to five seconds.
On transaction_id, message_id and bpp_id together. All responses to one search share the same transaction_id and message_id, so bpp_id is what distinguishes them. Keying on message_id alone causes each provider's catalogue to overwrite the previous one.
No. Broadcast search volume on an open network is not under the provider's control, so catalogues should be served from a pre-built cache. A live database fan-out per search will not survive network growth.
Because a BPP that cannot interpret a filter may return nothing rather than everything. Filtering broadly at the protocol level and narrowing client-side surfaces more chargers to the driver and keeps ranking under the buyer app's control.