A field guide · Beckn protocol v2.0 adapter

How ONIX
actually works.

Beckn-ONIX is the middleware that sits between a buyer or seller's backend and the open Beckn network. It validates, signs, routes, and publishes every message — and it does so through a plugin pipeline you assemble yourself. This is a working tour of what moves where, and why.

v2.0
Beckn protocol
14+
Plugin slots
4
Module types
1
Process per node
01 The picture

An adapter, not an app.

ONIX is not the buyer app and not the seller's backend. It is the protocol layer between them — a stateless gateway that turns private backend calls into signed, validated Beckn messages on a public network, and turns inbound network messages back into clean calls for the backend.

BUYER · BAP Backend consumer app / aggregator private API ONIX · BAP-SIDE Adapter validateSign addRoute validateSchema sign publish storePayload : 8081 network gateway · registry other participants ONIX · BPP-SIDE Adapter validateSign validateSchema checkPolicy addRoute storePayload signAck : 8081 SELLER · BPP Backend provider / merchant platform private API search · select · init · confirm on_search · on_select · on_init · on_confirm fig.01 end-to-end participants
fig. 01 A single Beckn transaction crosses four boundaries — two private, two public — and ONIX is the translator at each network edge.

Each ONIX node speaks two languages. To the backend it speaks HTTP-and-JSON in whatever shape the operator prefers. To the network it speaks the Beckn v2.0 protocol — strictly signed, strictly typed, strictly versioned. Everything in this document is about how it negotiates between those two sides.

02 Anatomy

Four layers, one process.

ONIX is a single Go process. Inside it, every request descends through four layers before doing real work: the HTTP server, the module router, the handler, and the step pipeline that does the actual protocol translation.

PROCESS · beckn-onix adapter L1 · HTTP SERVER net/http on :8081 read · write · idle timeouts · TLS termination L2 · MODULE ROUTER path → module /bap/caller/* /bap/receiver/* /bpp/receiver/* /bpp/caller/* L3 · HANDLER stdHandler loads plugins · owns role (bap|bpp) · owns subscriberId · spans & metrics L4 · STEP PIPELINE validateSign step ① ● addRoute step ② ● validateSchema step ③ ● sign step ④ ● publish step ⑤ ● ··· user-defined REQUEST DESCENDS
fig. 02 Each layer's only job is to pick the right thing in the next layer down. Steps do the actual work.

What each layer owns

L1 — HTTP server is just net/http. It enforces read, write, and idle timeouts and otherwise stays out of the way.

L2 — Module router matches the URL path prefix to a module. A module is bound to one of four well-known shapes: bapTxnCaller, bapTxnReceiver, bppTxnReceiver, bppTxnCaller. One adapter process may host any combination.

L3 — Handler is the only handler type that ships: stdHandler. It loads the plugins declared in config, attaches middleware, opens an OpenTelemetry span, and hands the request to the step pipeline.

L4 — Step pipeline is where everything interesting happens. Steps run in declared order. The first step that errors aborts the pipeline and produces a signed NACK.

03 Modules · the four shapes

Who's calling, who's receiving.

A node may play one role (BAP or BPP) or both. Each role splits in two directions — outbound calls into the network and inbound calls from the network. That gives four module shapes. Pick the ones relevant to the deployment.

BAP · outbound
bapTxnCaller
The buyer-side initiator. Receives a private call from the buyer's own backend and turns it into a signed Beckn request out to the network gateway or directly to a known BPP.
verbs  search · select · init · confirm · status · cancel · update · rating · support · track
BAP · inbound
bapTxnReceiver
The buyer-side callback sink. Receives asynchronous responses from BPPs, validates their signatures and schemas, and routes them back to the buyer's backend (or onto a queue).
verbs  on_search · on_select · on_init · on_confirm · on_status · on_cancel · on_update · on_rating · on_support · on_track
BPP · inbound
bppTxnReceiver
The seller-side request sink. Receives signed Beckn requests from BAPs, validates them, checks policy, and hands them off to the seller's backend or a fulfilment queue.
verbs  search · select · init · confirm · status · cancel · update · rating · support · track
BPP · outbound
bppTxnCaller
The seller-side responder. Receives a private callback from the seller's backend with results, then signs and sends them back to the originating BAP as a Beckn callback.
verbs  on_search · on_select · on_init · on_confirm · on_status · on_cancel · on_update · on_rating · on_support · on_track

The pattern is symmetric. For every verb on the Caller side there is a matching on_* verb on the Receiver side. The two sides do not block on each other — Beckn is asynchronous by design, and the callback may arrive seconds or minutes later from a different participant.

04 The pipeline

One request, step by step.

Walk through what happens when a buyer's backend calls POST /bap/caller/search. Click each stage to expand.

STEP 01
validateSign
SignValidator
STEP 02
addRoute
Router
STEP 03
validateSchema
SchemaValidator
STEP 04
sign
Signer · KeyManager
publish
or forward
STEP 05
Publisher · http

Reading the order

Step order is declared in the handler config and matters. The most common ordering is: verify what came in (validateSignvalidateSchema), decide where it goes (addRoute), prepare what goes out (sign), and finally hand it off (publish or HTTP forward). On the BPP receiver side you typically see checkPolicy inserted between schema validation and routing — a place to apply manifest-driven allow/deny rules before doing real work.

If any step returns an error the pipeline aborts. The handler composes a Beckn NACK, signs it with the ackSigner when configured, and returns it synchronously with an appropriate HTTP status. Every step also emits OpenTelemetry metrics — onix_step_executions_total, onix_step_execution_duration_seconds, and onix_step_errors_total — labelled with the module, action, and step name.

# a typical bapTxnCaller handler — config/local-simple.yaml
modules:
  - name: bapTxnCaller
    path: /bap/caller/
    handler:
      type: std
      role: bap
      plugins:
        router:      { id: router,        config: { routingConfig: ./config/local-routing.yaml } }
        schemaValidator: { id: schemav2validator, config: { type: url, location: https://.../beckn.yaml } }
        signer:      { id: signer }
        keyManager:  { id: simplekeymanager }
        cache:       { id: cache, config: { addr: localhost:6379 } }
      steps:
        - validateSchema
        - addRoute
        - sign
        - publish
05 Plugin library

Plugins are the verbs of the adapter.

Every job a handler can do is delegated to a plugin. Plugins are loaded as Go .so files at startup, slotted into named positions, and used by the steps. ONIX ships a working implementation for each slot; replace any of them without touching ONIX itself.

A Identity & signing Ed25519 · key lifecycle · ack signatures
Signer
Signs outbound requests and ACKs with the participant's private key.
SignValidator
Verifies the Authorization header on inbound requests and ACKs.
KeyManager
Manages key material. simplekeymanager for dev, keymanager for Vault, secretskeymanager for GCP/AWS secret stores.
Registry
Looks up other participants' public keys via the Beckn registry.
B Validation & policy schemas · manifests · open-policy
SchemaValidator
Validates against the Beckn v2.0 OpenAPI spec. schemav2validator can also pull domain-specific extended schemas via @context.
ManifestLoader
Loads the participant's published manifest (capabilities, schema versions, policies).
PolicyChecker
Evaluates manifest-derived policy rules against each message — allow, deny, or annotate.
PayloadTransformer
Reshapes the body between backend conventions and Beckn shapes via a JSONata-style mapping.
C Routing & transport where the message goes next
Router
Picks the next hop from declarative routing rules — by domain, version, endpoint, or dynamic bpp_uri.
Publisher
Hands messages to a queue — Pub/Sub or RabbitMQ — for asynchronous downstream processing.
TransportWrapper
Wraps the outbound http.RoundTripper to add retries, observability, or custom headers.
D Memory caches · stateful foundations
Cache
Backed by Redis. Used by KeyManager, PayloadStore, and any plugin that needs short-lived state. ONIX itself remains stateless.
PayloadStore
Records every inbound request indexed by message_id and transaction_id. The foundation under stateful plugins.
E Pipeline extension your own logic, slotted in
Step
Adds a custom processing step into the named pipeline order. Any plugin implementing the Step interface can be referenced from steps:.
Middleware
Standard Go http.Handler wrappers. Runs before the pipeline — auth, rate limiting, logging.
OtelSetup
Wires the MeterProvider, TracerProvider, and LogProvider so the whole adapter emits a single coherent OTLP stream.
06 Deployment

One process — or two, or many.

The same binary serves every deployment. What changes is the set of modules declared in the config file. Scale the role that needs scaling; collapse both into one when the load is modest.

Combined single binary
One adapter serves both buyer and seller traffic. Simplest to run; ideal for small networks or single-org pilots.
bapTxnCaller bapTxnReceiver bppTxnReceiver bppTxnCaller
BAP-only buyer scale-out
Dedicated buyer adapter. Scale buyer-side traffic independently and isolate failure domains.
bapTxnCaller bapTxnReceiver bppTxnReceiver bppTxnCaller
BPP-only seller scale-out
Dedicated seller adapter. Tune capacity to fulfilment load and apply seller-specific policy without disturbing buyer paths.
bapTxnCaller bapTxnReceiver bppTxnReceiver bppTxnCaller

Beyond the role split, the same binary supports local-development shapes that swap heavy dependencies for embedded ones — simplekeymanager instead of Vault, in-process publishers instead of RabbitMQ, file-based schemas instead of remote ones. Production configs simply re-bind those slots to real backends. Nothing about the pipeline changes.

07 Observability

Two consumers, one signal stream.

Every node emits a single OpenTelemetry stream — metrics, traces, audit logs — over OTLP. A companion collector deployed beside the adapter splits that stream into two pipelines: one for the node operator, one for the network observer.

ONIX · ADAPTER NODE handler & step metrics request & cache traces PII-masked audit logs runtime + redis metrics plugin inventory gauge OTel SDK · scope = "beckn-onix" OTLP / gRPC COMPANION COLLECTOR split one process per node NODE PIPELINE · full fidelity Node operator Prometheus · Grafana · Loki · Datadog · whatever all metrics · all traces · all logs NETWORK PIPELINE · filtered Network observer Network Orchestrator's shared collector http_request_count · spans · plugin info
fig. 03 Neither consumer instruments the adapter separately; the split is collector configuration, not code.

The node operator gets full-fidelity visibility into their own adapter — every step's latency, every cache hit, every plugin error — and can ship it to any OTLP backend they choose. The network observer gets only what crosses node boundaries: an http-request counter labelled by sender.id and recipient.id, a stitched distributed trace where the Beckn transaction_id becomes the trace ID, and a plugin-inventory gauge that reveals which plugins each node is actually running.

Audit logs are emitted with PII masked at the source. The fields to mask are themselves configuration — config/audit-fields.yaml — so the network observer never receives anything sensitive even when the node operator's pipeline does.