Migration Guides
Guides for moving a Beckn Network deployment between various topologies without breaking network interoperability.
Protocol upgrades happen while the network keeps running — consumer-side (BAP) and provider-side (BPP) applications can't all be rewritten and redeployed at once. These guides cover how to move a deployment from one network topology to another with a phased, low-risk approach.
This guide covers migration using the request mapper plugin built into the ONIX adapter.
Business Context
Why — the migration problem
Beckn Protocol v2.x introduces a significantly improved data model designed to handle complex, multi-party commerce more precisely and consistently across sectors such as agriculture, financial services, and utilities. It standardises concepts that were loosely defined in v1 — for example, what constitutes an "item", how pricing is structured, how a transaction is tracked end-to-end, and how metadata is attached to catalog entries.
However, the real-world networks and applications built on Beckn v1.x — including buyer-side (BAP) and seller-side (BPP) platforms — represent significant investment. Requiring every participant to simultaneously rewrite their systems to speak v2 is both risky and operationally challenging. A failed or delayed migration by any single participant could break the entire network's interoperability.
The core business problem is: how do we upgrade the network protocol without forcing every participant to upgrade at the same time?
What — the mapper as a migration bridge
The mapper is the answer to that problem. It is a configuration-driven translation layer built into the ONIX adapter that converts payloads between v1 and v2 transparently — in both directions.
From a business perspective, this means:
BPP operators (catalog providers, scheme administrators, procurement platforms) continue publishing and responding in v1 format. They do not need to change their backend systems.
BAP operators (buyer apps, beneficiary portals, market linkage platforms) continue sending and receiving v1 format. No application rebuild is required.
The Beckn network operates on v2, gaining the benefits of the improved schema. Once BAP and BPP applications are ready, they can upgrade to v2 independently and in parallel — at which point the mapper can be bypassed and eventually retired, completing the migration with no forced coordination between participants.
The adapter sits in between, translating every message as it passes through — invisibly to both the application and the network participant on the other side.
This decouples the protocol upgrade from the application upgrade, enabling a phased, low-risk migration.
How — the approach
The adapter intercepts every Beckn message at the boundary between the application and the network. Depending on direction:
Outbound (application → network): the application sends a v1-formatted request. The adapter converts it to v2 before forwarding it to the Beckn network.
Inbound (network → application): the network sends a v2-formatted message. The adapter converts it back to v1 before delivering it to the application.
This conversion is driven entirely by configuration — a YAML file of JSONata transformation expressions, one per Beckn action (search, publish, confirm, status, etc.) and per role (buyer-side or seller-side). No adapter code changes are needed when mapping rules evolve; only the configuration file is updated and the adapter is restarted.
The result is that both the application layer and the network layer believe they are talking to a compatible peer, while the adapter silently handles all schema translation between them.
Technical Overview
The ONIX adapter bridges Beckn Protocol v1.x and v2.x networks using a file-based transformation layer called the request mapper. Rather than hard-coding payload shapes in the adapter binary, all transformation logic lives in a single YAML file containing JSONata expressions. The adapter loads this file at startup and applies the right expression automatically based on the action and the participant role (BAP or BPP).
This guide explains how the mapper is wired into ONIX, what it transforms, and how transformations are structured for each Beckn action.
How the Mapper Plugs Into ONIX
Configuration
The mapper is declared as a plugin named payloadTransformer in the adapter config (configs/bpp-config.yaml / bap-config.yaml):
payloadTransformer: id: reqmapper config: role: bpp # or "bap" mappingsFile: /app/config/mappings.yaml
role tells the plugin which side of the mapping to apply. mappingsFile is the path to the YAML file mounted into the container.
The transformPayload step
Each module (Receiver and Caller) defines an ordered list of pipeline steps. The transformPayload step is where payload conversion happens:
BAP Receiver (/bap/receiver/) — receives v2 callbacks from the network, converts to v1 for the BAP app:
validateSign → addRoute → validateSchema → transformPayload
BAP Caller (/bap/caller/) — receives v1 requests from the BAP app, converts to v2 for the network:
transformPayload → addRoute → sign → validateSchema
BPP Receiver (/bpp/receiver/) — receives v2 from the network, converts to v1 for the BPP app:
validateSign → addRoute → validateSchema → transformPayload
BPP Caller (/bpp/caller/) — receives v1 from the BPP app, converts to v2 for the network:
transformPayload → addRoute → sign → checkPolicy → validateSchema
The step position determines which payload version the surrounding steps see. On Receiver modules, schema validation runs before transformPayload (validating the incoming v2 payload); on Caller modules, it runs after (validating the outbound v2 payload).
Mapping Architecture: bapMappings vs bppMappings
Each Beckn action in the mappings file has two entries:
mappings: <action>: bapMappings: | (JSONata expression...) bppMappings: | (JSONata expression...)
Entry | Direction | Applied when |
|---|---|---|
bapMappings | v1 → v2 (outbound) or v2 → v1 (inbound) | adapter running as BAP |
bppMappings | v2 → v1 (inbound) or v1 → v2 (outbound) | adapter running as BPP |
The role field in the plugin config (bap or bpp) determines which entry is used at runtime. This keeps a single mappings file shared between both roles.
Transformation Levels
Transformations happen at two levels in every action:
1. Context level (universal)
Every message — regardless of action — carries a context block. The field names changed between v1 and v2:
v1 field | v2 field | Notes |
|---|---|---|
domain | networkId | Identifies the Beckn network/domain |
bap_id | bapId | Consumer platform identifier |
bap_uri | bapUri | Consumer platform endpoint |
bpp_id | bppId | Provider platform identifier |
bpp_uri | bppUri | Provider platform endpoint |
transaction_id | transactionId | Correlation ID for a full transaction |
message_id | messageId | Unique ID per request |
version: "1.0.0" | version: "2.0.0" | Protocol version literal |
The mapper always rewrites version and all snake_case → camelCase context fields.
2. Message/payload level (per-action)
Each action also transforms the message body. These are the substantive schema changes between v1 and v2.
Per-Action Transformations
discover (v1 search → v2 discover)
bapMappings (BAP outbound: v1 → v2)
The v1 message.intent is deconstructed into two v2 constructs:
filters— JSONPath expressions derived from intent fields:Language preference → JSONPath on
descriptor.tagsFulfillment type → JSONPath on
fulfillments[].typeAddress/city/state → JSONPath on provider location
Price range → JSONPath on
offers[].considerations[].considerationAttributesGovernment scheme eligibility criteria → JSONPath on
entitlementIds
spatial— geo-search using theS_DWITHINoperator, built from GPS coordinates in the intent:
{ "op": "S_DWITHIN", "path": "$.provider.availableAt", "value": [lon, lat], "distance": 50000 }
Coordinates follow GeoJSON convention ([longitude, latitude]); default radius is 50 km.
bppMappings (BPP inbound: v2 → v1)
Reverses the above: v2 filter/spatial objects are not applicable on the BPP side; the BPP receives a standard v1 search intent.
on_discover (v2 catalog response → v1 on_search)
This is the most complex mapping because v2 restructured the catalog schema entirely.
bapMappings (BAP inbound: v2 → v1)
The v2 message.catalogs[] structure is mapped back to the v1 message.catalog.providers[] shape:
v2 structure | v1 structure |
|---|---|
catalogs[].resources[] | providers[].items[] |
catalogs[].offers[] | Merged into items via resourceIds linkage |
resourceAttributes.entitlementIds[] | items[].tags[] (structured tag groups) |
catalogs[].provider.availableAt[] | providers[].locations[] |
catalogs[].offers[].considerations[] | items[].price |
The entitlementIds → tags conversion handles multiple named groups:
entitlementIds[].code | v1 tags[].descriptor.code |
|---|---|
targetBeneficiaryType | target-beneficiary-type |
fundingModel | funding-model |
benefitFor + benefitValue | benefit |
requiredDoc | required-docs |
economicEligibility | economic-eligibility |
demographicEligibility | demographic-eligibility |
categoryIds | category_ids (item field) |
locationIds | location_ids (item field) |
Unknown codes | Passed through as generic display tags |
catalog/publish (v1 catalog publish → v2)
bppMappings (BPP inbound: v1 → v2)
A BPP application sends a v1-style catalog publish. The mapper converts it to the v2 catalog structure:
v1 structure | v2 structure |
|---|---|
catalog.providers[] | catalogs[] (one catalog per provider) |
providers[].items[] | catalogs[].resources[] + catalogs[].offers[] |
items[].tags[] | resources[].resourceAttributes.entitlementIds[] |
items[].price | offers[].considerations[].considerationAttributes |
providers[].locations[] | catalogs[].provider.availableAt[] |
providers[].fulfillments[] | catalogs[].resources[].resourceAttributes fields |
Each v1 item becomes both a resource (the thing being offered) and an offer (the commercial terms). Offer resourceIds reference the corresponding resource id, establishing the linkage.
The tags → entitlementIds conversion is the inverse of the on_discover conversion above.
confirm (v1 order → v2 contract)
This action renamed the top-level transactional object.
bapMappings (BAP outbound: v1 → v2)
v1 structure | v2 structure |
|---|---|
message.order | message.contract |
order.provider | contract.participants[0] |
order.items[] | contract.commitments[] (each item → a commitment with resource + offer) |
order.fulfillments[] | contract.performance[].performanceAttributes |
order.payment | contract.commitments[].offer.considerations[].considerationAttributes |
items[].tags[] | commitments[].resources[].resourceAttributes.entitlementIds[] |
bppMappings (BPP inbound: v2 → v1)
Reverses the above — contract → order, commitments → items, performance → fulfillments.
status (v1 status → v2 status)
bapMappings (BAP outbound: v1 → v2)
Minimal transformation: only the context fields change. The message body maps order_id to contract.id:
v1: message.order_id v2: message.contract.id
bppMappings (BPP inbound: v2 → v1)
Reverses: contract.id → order_id.
on_confirm and on_status (v2 contract response → v1 order response)
Both follow the same pattern as confirm.bapMappings in reverse:
bppMappings (BPP outbound: v1 → v2)
The BPP app returns a v1 order with status. The mapper builds:
contract.idfromorder.idcontract.status.codefromorder.state(mapped through a status-code lookup)contract.participants[]fromorder.providercontract.commitments[]fromorder.items[](includingentitlementIdsfromtags)contract.performance[]fromorder.fulfillments[]
bapMappings (BAP inbound: v2 → v1)
Converts the v2 contract response back to v1 order format for the BAP application, reconstructing the full tags structure from entitlementIds.
Summary of Schema Changes (v1 → v2)
Category | v1 | v2 |
|---|---|---|
Context field names | snake_case (bap_id, bpp_uri) | camelCase (bapId, bppUri) |
Protocol version | "1.0.0" | "2.0.0" |
Domain identifier | context.domain | context.networkId |
Catalog items | providers[].items[] | catalogs[].resources[] + catalogs[].offers[] |
Item metadata | items[].tags[] (grouped, hierarchical) | resourceAttributes.entitlementIds[] (flat, coded) |
Order object | message.order | message.contract |
Order parties | order.provider + order.fulfillments[].agent | contract.participants[] |
Order items | order.items[] | contract.commitments[] |
Fulfillment status | order.fulfillments[].state | contract.performance[].status |
Price | items[].price.value (string) | considerationAttributes.value (number) |
Geo search | Intent-based (flat address) | filters (JSONPath) + spatial (S_DWITHIN) |
Adapter Configuration
The mapper file path is configured separately in the BAP and BPP adapter configs under payloadTransformer.config.mappingsFile. Both roles (BAP and BPP) share the same mappings file — the role field in the plugin config (bap or bpp) determines which entry (bapMappings or bppMappings) is applied at runtime.
Adapter config | Role | Modules configured |
|---|---|---|
bap-config.yaml | bap | bapTxnReceiver (port 8081, /bap/receiver/) and bapTxnCaller (/bap/caller/) |
bpp-config.yaml | bpp | bppTxnReceiver (port 8082, /bpp/receiver/) and bppTxnCaller (/bpp/caller/) |
When deploying, the mappings file is mounted into the container and its path is referenced by mappingsFile. No adapter code changes are required when updating the mappings — only a container restart is needed.
Challenges and Maintenance Considerations
Mapper must be updated for every new field
The JSONata expressions in the mappings file explicitly enumerate every field that needs to be carried across. If a new field is added to a v1 payload (by the BPP or BAP application) and is not handled in the corresponding mapping expression, that field will be silently dropped during transformation. The adapter will not throw an error — it simply produces an output that omits the unrecognised field.
This means:
Any new v1 field added to
order,items,tags,fulfillments, orcontextmust have a corresponding mapping added toconfirm.bapMappings/confirm.bppMappings(and likewise for other actions).Any new v2 field added by the network side must be handled in the reverse direction.
The same applies to new
entitlementIdscodes: if a BPP starts sending a new code in v2, theon_discover.bapMappings(andon_confirm/on_statusequivalents) must add a rule to convert it back to the correct v1tagsgroup. Without this, the tag is either dropped or falls through to the generic "unknown" passthrough handler (where one exists).
No schema-awareness at transform time
The mapper operates on raw JSON; it has no knowledge of the Beckn schema. If the input payload has an unexpected structure (e.g., items is an object instead of an array), the JSONata expression may produce an incorrect output silently. Schema validation (the validateSchema step) is the safety net — but it runs separately from the transform, and only validates the version it receives.
Bidirectional parity must be maintained
bapMappings and bppMappings for a given action are mirror images of each other. When one is updated, the other typically needs a matching update. Allowing them to diverge can cause round-trip data loss (e.g., a field survives the outbound transform but is missing from the inbound response).
Adding or Modifying a Mapping
Open the mappings file (path configured in the adapter config under
payloadTransformer.config.mappingsFile).Find the relevant action block (e.g.,
on_discover:).Edit the
bapMappingsorbppMappingsJSONata expression — and update the mirror direction if the change affects round-trip data.Redeploy the adapter — no code changes needed, only a container restart.
Verify with the relevant test collection covering the affected action.
JSONata expressions have access to the full input payload via $ and support functions like $map, $filter, $merge, $append, $reduce, $string, $number, $boolean, $replace, startswith, and sprintf-style string formatting via the adapter's runtime.