A practical guide · Multi-store + Logistics BPP

Ship through a Logistics BPP,
not your own van.

Same multi-tenant seller BPP, same aggregating BAP as the in-house build — but now no store owns a delivery fleet. Your BAP wears a second hat: a Logistics BAP that books a carrier (JNE) over the network. Each store's Trade contract earns its own Logistics contract, joined by linkedContractId, and your marketplace escrow pays both the store and the carrier.

N
Tenant stores
2
Contracts per order
JNE
Logistics BPP
BAP
Books & pays
01 The picture

One BAP, two BPPs.

Your BAP still aggregates the multi-tenant seller BPP. What's new: once an order is confirmed on the Trade side, the BAP turns around and acts as a Logistics BAP, booking a carrier BPP (JNE) to collect from the store and deliver to Andi. The store hands the parcel to JNE; the parcel never rides a store van.

Topology: one BAP booking a seller BPP and a logistics BPP Consumer, BAP (acting as both Trade and Logistics BAP), the network, a multi-tenant seller BPP, and JNE the logistics BPP. The store hands the parcel to JNE, which delivers to the consumer; the two contracts are linked by linkedContractId. fig.01 topology · 1 BAP · 2 BPPs · N tenants CONSUMER Andi Surabaya YOUR BAP Buyer app Trade BAP + Logistics BAP holds escrow · books JNE port :8081 network gateway · registry YOUR ONIX-BPP · SELLER multi-tenant switch (provider_id) store-a · Toko Elektronik store-b · Roti Sumber store-c · Batik Solo goods only — no van JNE · LOGISTICS BPP the carrier LOG-PARCEL · NEXT_DAY FWA × your marketplace you don't build it trade logistics · books carrier pickup @ store JNE delivers → Andi's door linkedContractId
fig. 01 Trade messages in rust, Logistics in deep teal, async callbacks in muted grey. The amber dashed lines are the parcel itself — handed from the store to JNE, then carried to Andi's door. The two contracts (one per sector) are joined by linkedContractId.
02 The cast

Three participants, two of them yours.

You build the multi-tenant seller BPP and the aggregating BAP — exactly as in the in-house guide. JNE is an external Logistics BPP you discover and book over the network; you don't build it. ION stays in the background, providing the catalogue and policy registries everyone consults.

cast A single multi-store order jakarta stores → surabaya buyer, via JNE
Andi
Consumer in Surabaya. Opens your marketplace, fills one cart that may span several stores, pays once.
Your BAP
Two hats. Trade BAP: aggregates listings, dispatches the order. Logistics BAP: books JNE for each shipment. Holds the escrow and pays both sides.
Your seller BPP
Multi-tenant, one ONIX process. N stores, one provider per catalog. Sells goods only — declares no in-house delivery fleet.
JNE
External BPP · Logistics. LOG-PARCEL · NEXT_DAY. Operates under a Framework Agreement with your marketplace. Picks up at the store, delivers to Andi.
ION Services
Catalogue · CDS · Registry · Policy. Consulted, never blocked on — for both the Trade and Logistics sectors.
03 Multi-tenant catalogue

N catalogs, one provider each.

Unchanged from the in-house build: in Beckn 2.0 each catalogue binds to exactly one provider — beckn.yaml ties the Catalog schema to a single provider field, not an array. Your multi-tenant BPP publishes one catalogue per tenant; the BAP keys its local index on provider.id across all of them.

# beckn 2.0: one catalog, one provider — publish this once per tenant
catalog:
  descriptor:    { name: Toko Elektronik on Your Marketplace }
  provider:
    id:          store-a-toko-elektronik
    descriptor:  { name: Toko Elektronik }
    locations:   [ { id: JKT-001, gps: -6.20,106.84 } ]
    items:       [ { id: item-phone-x, price: 5,000,000, location_ids: [JKT-001] } ]
    fulfillments: [ { id: ful-A-3pl, type: DELIVERY, provider: jne } ]

One field differs from the in-house build: the fulfillment's provider is now the carrier (jne), not the store. That mismatch — fulfillment provider ≠ item provider — is the signal that carriage is delegated. The store advertises which carrier it ships with; the actual freight quote arrives later, from JNE.

04 Delegated logistics

Shipping is its own contract.

No store owns a van. When an order is confirmed on the Trade side, your BAP — now wearing its Logistics-BAP hat — books JNE over the network. That booking is a second Beckn exchange and earns its own Logistics contract, joined to the store's Trade contract by linkedContractId. One order, two linked contracts, per store.

Two linked contracts per store: Trade and Logistics joined by linkedContractId The Trade contract sits between the store and the BAP; the Logistics contract sits between the BAP and JNE; the Logistics contract's linkedContractId attribute points back at the Trade contract id. The pair repeats once per store. fig.02 two linked contracts · per store Store A merchant your BAP the hinge JNE carrier TRADE CONTRACT TRADE/2026/0429/A1F2 goods · store ↔ BAP LOGISTICS CONTRACT LOG/2026/0429/J7K9 linkedContractId = TRADE/2026/0429/A1F2 carriage · BAP ↔ JNE linkedContractId → × N stores each pair independent, cross-referenced
fig. 02 The BAP is the hinge: it's a party to both contracts. The Trade contract binds the store; the Logistics contract binds JNE; the carriage contract carries a linkedContractId pointing back at the goods contract. They settle independently — the link is a reference, not a merge. Repeat once per store in the cart.
logistics Delegated-logistics shape a second contract, booked by the BAP
who books it
The BAP, acting as a Logistics BAP. It discovers JNE on the network and runs the full /select → /confirm leg on Andi's behalf — the store never talks to the carrier.
Logistics /select → /confirm
Quotes the lane (Jakarta → Surabaya, weight, NEXT_DAY), binds the pickup at the store and drop at Andi, and receives an awbNumber.
contractAttributes.linkedContractId
On the Logistics contract, points at the Trade contract id. The two stay independent contracts — they only reference each other.
Framework Agreement
A standing FWA between your marketplace and JNE governs lane pricing and payment terms. The Logistics /init validates against it, not against Andi.
status forwarding
JNE's /on_status events land on your BAP; you relay them to Andi as one timeline, and mirror the milestone onto the Trade contract so the store stays informed.
per store, per shipment
A multi-store cart = N Trade contracts + N Logistics contracts. The BAP books one carriage per pickup location; each pair is linked separately.

The Trade × Logistics walk-through traces a single store's Trade-and-Logistics handshake end to end, including the linkedContractId exchange. The difference here is multiplicity: your BAP runs that handshake once per store in the cart, while the seller BPP fronts every store at once. Compare with the in-house build, where there is no carrier and no second contract.

05 Marketplace payment

One charge in, two kinds of payee out.

Same escrow as the in-house build — payment.collected_by = BAP — but now the BAP settles two kinds of payee. Andi still pays once. The BAP holds the funds, then pays each store for goods at Trade reconcile and pays JNE for freight at Logistics reconcile under the FWA, all net of your platform fee. ION carries references; the money moves off-network.

Marketplace escrow paying stores and the carrier The consumer authorizes one off-network payment to the BAP escrow; at reconcile the BAP remits each store for goods and JNE for freight, off-network, net of the platform fee. fig.03 escrow · pays goods + freight CONSUMER Andi pays once YOUR BAP · ESCROW holds funds collected_by: BAP − platform fee remits at reconcile STORE A · GOODS Toko Elektronik goods − fee STORE B · GOODS Roti Sumber goods − fee STORE C · GOODS Batik Solo goods − fee JNE · FREIGHT carrier freight · FWA authorize once · QRIS goods − fee · BI-FAST freight · under FWA on-network: /on_reconcile per Trade and per Logistics contract → statement · NTPN
fig. 03 Money stays muted grey, dashed, off-network — QRIS in from Andi; BI-FAST out to each store (goods) and to JNE (freight). The BAP holds it in between. The rust line is the only on-network trace: a /on_reconcile per contract — Trade and Logistics alike — carrying statements and NTPN, never the funds.
payment Escrow shape BAP collects, settles stores and carrier
payment.collected_by
BAP. The buyer app collects once and never lets go of the funds until each contract reconciles — neither the store nor JNE touches Andi's money.
two payee types
Goods → the stores, settled on each Trade /on_reconcile. Freight → JNE, settled on each Logistics /on_reconcile under the FWA. Both flow from the same escrow.
payment.type
PRE-FULFILLMENT. Andi authorises at Trade /confirm, before the BAP even books the carrier. The escrow funds the freight too.
platform-fee line
Your marketplace fee is its own breakup line on each Trade reconcile. Freight is a pass-through cost — the FWA price, settled to JNE without your margin.
per-contract remittance
/reconcile runs per contract: once per Trade contract (store payout + NTPN), once per Logistics contract (carrier settlement under FWA).
refunds & cancellation
Because the BAP holds the funds, a refund before pickup is clean. If JNE has already been dispatched, the freight leg settles per the FWA cancellation terms.

Same three shapes Beckn allows as the in-house build. Set collected_by: BPP and each store collects goods directly with its own QRIS while you still pay JNE — see the Trade × Logistics walk-through. Or promote payment to its own sector with a dedicated Payment BPP that authorises and settles on-network. In all of them the BAP only orchestrates — the funds stay off-network.

06 The lifecycle

One store, two linked contracts, end to end.

Walk one store's order through both sectors. Five lanes: Andi, your BAP, your seller BPP, JNE the Logistics BPP, and ION. The Trade leg books the goods; the Logistics leg books the carriage; linkedContractId joins them; your escrow settles both.

Lifecycle across Trade and Logistics sectors with a logistics BPP Sequence diagram with lanes for Consumer, BAP, seller BPP, JNE logistics BPP, and ION. Phases: catalog publish, search, trade select/init/confirm, logistics select/init/confirm with linkedContractId, pickup and transit, delivery, and reconcile of both contracts. Consumer Andi · Surabaya your BAP trade + logistics BAP · escrow seller BPP multi-tenant Store A · goods JNE Logistics BPP LOG-PARCEL ION Services Catalogue · CDS Registry · Policy Phase 01 · Catalog publish · seller (N tenants) + carrier /catalog/publish · provider = store-a … store-c /catalog/publish · LOG-PARCEL /on_subscribe → BAP indexes goods + carriers Phase 02 · Search → /on_search · Trade search "smartphone" /search /on_search · merged providers[] Phase 03 · Trade · /select → /on_select /select · provider.id = store-a /on_select · goods quote · freight TBD via 3PL Phase 04 · Trade · /init → /on_init /init · drop address · payment.collected_by = BAP /on_init · firm goods price · amount due Phase 05 · Trade · /confirm → /on_confirm · contract ACTIVE /confirm · escrow holds funds /on_confirm · Trade contract ACTIVE · tradeContractId Phase 06 · Logistics · /select → /on_select · BAP books JNE /select · lane JKT → SBY · weight · NEXT_DAY /on_select · freight quote Phase 07 · Logistics · /init → /on_init /init · pickup @ Store A · drop @ Andi /on_init · FWA validated · settlement preview Phase 08 · Logistics · /confirm → /on_confirm · links to Trade /confirm /on_confirm · Logistics contract ACTIVE contractAttributes.linkedContractId = tradeContractId · awbNumber JNE-2026-7K3M9X Phase 09 · Pickup & transit · pushed by JNE JNE collects parcel at Store A /on_status [PICKED_UP] awb · rider · proxy phone /on_status [IN_TRANSIT] BAP relays to Andi · one timeline /track /on_status [OUT_FOR_DELIVERY] Phase 10 · Delivered · both contracts COMPLETE /on_status [DELIVERED] deliveryProofUrl (photo) · signature /update [DELIVERED] · Logistics → COMPLETE /update · Trade → COMPLETE Phase 11 · Reconcile · goods (Trade) + freight (Logistics) 5★ + comment /reconcile (goods · per tenant) /on_reconcile · store payout = goods − fee · NTPN /reconcile (freight · under FWA) /on_reconcile · freight settled · NTPN Epilogue One store. Two linked contracts. Goods by the merchant, carriage by JNE — joined by linkedContractId, settled by your escrow. ~30 messages · 2 sectors · 1 store · 2 linked contracts · ×N in a cart
fig. 04 Trade messages in rust, Logistics in deep teal, physical status pushes in amber (dashed), money relays in grey. The whole right half of the diagram — the Logistics leg — is what the in-house build doesn't have. Repeat the Logistics leg once per store in a multi-store cart.
07 Routing, both sides

The BPP routes; the BAP chooses the carrier.

The seller BPP is unchanged from the in-house build. The buyer already picked the provider, so by /select message.order.provider.id is set; the BPP only forwards. One ONIX route, one backend, an internal switch on provider_id.

# seller BPP · config/local-routing.yaml — one backend, forward everything
routes:
  - when:     { }                                  # every verb, every provider
    target:   http://bpp-backend:9000/beckn        # switches on provider.id

What's new lives on the BAP side. After Trade /on_confirm, the BAP must pick a carrier. That's not provider selection — it's a routing decision over your carrier roster: the set of Logistics BPPs you hold an FWA with. Match the store's pickup pincode and the item's service level to a carrier, then run the Logistics leg against it.

# BAP · carrier roster — pick a Logistics BPP for the shipment
carriers:
  - when:   { service: NEXT_DAY, from: JKT* }
    bpp:    jne            # FWA: marketplace × JNE 2026Q2
  - when:   { service: SAME_DAY, from: JKT* }
    bpp:    gosend
  - fallback: jne

So the seller BPP routes inbound by provider; the BAP routes outbound by carrier. Neither one ever chooses the store — Andi did that — but the BAP does choose who carries the parcel, because Andi never sees JNE.

08 Operational concerns

What changes with a carrier in the loop.

Most per-tenant axes carry over from the in-house build. Delegating carriage adds a handful more — all of them living on the BAP, which now runs a second sector and a second class of settlement.

ops Per-tenant & per-carrier axes what the logistics leg adds
Carrier roster & FWAs
The BAP holds a Framework Agreement with each Logistics BPP. Store the FWA id, lane pricing, and cancellation terms; the Logistics /init validates against it.
Freight quotes
Freight no longer comes from a store rate card — it comes from JNE's /on_select. The BAP surfaces it to Andi and funds it from the same escrow.
Contract correlation
Persist the tradeContractId ↔ logisticsContractId mapping. linkedContractId lives on-network; your BAP needs the reverse index to forward status and reconcile.
AWB & status forwarding
The awbNumber and rider details come from the Logistics /on_confirm and /on_status. Relay them to Andi; mirror milestones onto the Trade contract.
Two reconciles
Each order settles twice: Trade (store payout, your fee, NTPN) and Logistics (freight to the carrier under FWA). Keep them as separate ledgers under one escrow.
Tax IDs (Faktur Pajak)
Goods Faktur Pajak from the PKP-registered store; freight tax invoice from the carrier. Two documents per order, surfaced on their respective reconcile bundles.
Failure modes
A confirmed Trade contract with a failed Logistics booking needs a compensating path: re-book another carrier from the roster, or cancel-and-refund from escrow before pickup.
Observability
Tag spans with both provider.id and logisticsContractId. The Logistics sector is a second set of dashboards — carrier SLA, pickup latency, lane cost.

The seller BPP's per-tenant list — signing keys, inventory, settlement bank, onboarding — is identical to the in-house build; only the fulfilment axis moved off the store and onto the carrier. The asymmetry is still the point: the BAP carries the breadth (every BPP, every carrier), the seller BPP carries the routing fidelity across tenants.