Developer guide
Sell, issue and reconcile in a few verbs.
REST and JSON over HTTPS. Whole-number amounts in minor units. An idempotency key on every write. This page covers what you need to be live: the three roles (seller, issuer, owner), the signed token that carries a sale to the system that issues the ticket, webhooks, and how to read the statement.
Getting started
You do not need to write code to begin. An event owner invites you from its dashboard and sends you a link. Open it, name your organisation, and you get your own API key, shown once, attached to the event on the terms the owner chose. Sign in to the dashboard at api.bandariva.com/app with that key to see what you can do; everything the dashboard does, the API does too.
Base URL: https://api.bandariva.com/v1. Public keys for verifying tokens: https://api.bandariva.com/public/keys. To get an account without an invitation, ask us.
curl https://api.bandariva.com/v1/catalogue \ -H "Authorization: Bearer $BANDARIVA_KEY"
The catalogue lists the published events you have been invited to, your allocation in each category, and the price range you must stay inside.
Keys and roles
Keys look like bnd_…, are stored hashed, and are shown once. Send them as Authorization: Bearer. Give each system the least it needs.
| Key role | For |
|---|---|
REPORTER | A seller’s system: report sales and read your own figures. |
ISSUER | A ticketing system: redeem reservation tokens and say whether it issued the tickets. |
OPERATOR | Run events day to day: events, partners, allocations, sales. |
ADMIN | Everything for your organisation, including keys. |
DEVELOPER, VIEWER, READER, DOOR | Webhooks and keys, read-only, read with revenue, and door counts for named events. |
A key can also be limited to named events, to your server’s addresses, and to a lifetime. A key can never create keys or people. People sign in to the dashboard with email, password and an authenticator code, which is required for anything that moves access.
Writes, idempotency and errors
- Send an
Idempotency-Keywith every write, derived from your own order id. Reuse it on every retry: a repeated request returns the first answer and does nothing twice. - Amounts are whole numbers in the smallest unit (
amountCents) with an ISOcurrencythat must match the event’s. Never floats. - Retry only network failures,
429and502/503/504, with growing delays. Readerror.code, not the message. - Ordinary requests are limited per credential; heavy routes (such as building a layout) to 30 a minute. A
429means slow down.
{ "error": { "code": "hold_expired", "message": "That reservation has lapsed. Reserve again." } }Selling: reserve, take payment, confirm
If you sell from an allocation and take the buyer’s payment yourself, reserve first so two checkouts can never take the same last seat. A reservation lasts 60 seconds to 15 minutes (default 5), counts against what is left of your allocation, and frees itself if it lapses. You keep your own checkout, payment methods and brand.
# hold seats (or send "seatIds": [...] to choose them) POST /v1/events/{eventId}/seat-reservations Idempotency-Key: checkout-8841 { "bestAvailable": { "quantity": 2 }, "ttlSeconds": 300, "reference": "checkout-8841" } # the buyer pays in your checkout, then: POST /v1/events/{eventId}/seat-reservations/{reservationId}/confirm { "amountCents": 600000, "reference": "order-8841" } # not paid, or the buyer left: DELETE /v1/events/{eventId}/seat-reservations/{reservationId} # still paying? extend (never past 15 minutes in all): POST /v1/events/{eventId}/seat-reservations/{reservationId}/extend { "ttlSeconds": 300 }
# hold tickets by quantity POST /v1/events/{eventId}/holds { "categoryId": "…", "quantity": 2, "ttlSeconds": 300, "reference": "checkout-123" } # paid: records the sale POST /v1/events/{eventId}/holds/{holdId}/confirm { "amountCents": 30000, "currency": "KES" } # the buyer left: frees the tickets DELETE /v1/events/{eventId}/holds/{holdId}
Best-available puts a party together in the rows nearest the stage, never leaves a single seat stranded when it can be avoided, and keeps wheelchair spaces and restricted views for those who ask. Seat-tier events are sold by seat, not by quantity.
Report sales made elsewhere
Sell from your own system and just tell Bandariva? Send each sale or refund once or a hundred times; your reference makes it count once. A sale is never refused for exceeding your allocation, because it already happened. It shows up for the owner as an oversold discrepancy instead.
POST /v1/events/{eventId}/sales
Idempotency-Key: 7c1f…
{ "categoryId": "…", "quantity": 2, "amountCents": 15000, "currency": "KES",
"reference": "order-8841", "occurredAt": "2026-11-02T09:30:00Z" }
# many at once, all or nothing: POST /v1/events/{eventId}/sales/batch { "sales": [ … ] }
# seats named: POST /v1/events/{eventId}/seat-sales { "sales": [ { "seats": [ … ], … } ] }
# a refund: "type": "SALE_REVERSAL" (or POST …/seats/reversals for seats)Prefer a file? POST /v1/events/{eventId}/imports/preview with a CSV (category,type,quantity,amount_cents,buyer_fee_cents,currency,reference,occurred_at) changes nothing and tells you what would be accepted; …/imports/commit applies it, all or nothing.
Your own position is always one read away: GET /v1/events/{eventId}/overview returns, per category, what you were allocated, reported sold, and what remains. You see your own figures only.
Issuing: verify the token, issue the ticket, redeem
Bandariva never holds or sees an entry ticket. When a seller confirms, it receives a reservation token: a signed statement, valid for 15 minutes, that this seller holds N tickets in this category of this event. The seller presents it to your system. Give your integration a key with the ISSUER role; you are the issuer when the owner names your organisation on the event.
1. Verify the token offline
Tokens are compact Ed25519 (EdDSA) signatures. Fetch GET /public/keys once and cache it; keys change only on rotation. Check the signature with the key whose kid is in the token header, then reject it if it has expired, if aud is not your organisation id, or if evt is not the event you are issuing for.
import crypto from "node:crypto"; const [h, p, sig] = token.split("."); const kid = JSON.parse(Buffer.from(h, "base64url")).kid; const key = crypto.createPublicKey(keys.find((k) => k.kid === kid).publicKeyPem); const ok = crypto.verify(null, Buffer.from(h + "." + p), key, Buffer.from(sig, "base64url")); const claims = JSON.parse(Buffer.from(p, "base64url")); // aud, sub (the seller), evt, cat, catName, qty, amt, cur, ref, exp, and seats (up to 25) or seatCount
For up to 25 seats the token names them (for example "Stalls C-5") so exactly those seats are issued. For more, it carries seatCount and you fetch the list with GET /v1/events/{eventId}/holds/{holdId}/seats.
2. Issue the tickets in your system, then redeem
POST /v1/events/{eventId}/holds/redeem
{ "token": "…", "issuedRef": "your ticket or order reference" }Repeating the call with the same issuedRef is safe; a different one for the same token is refused. Always redeem: verifying offline alone cannot stop a token being replayed.
3. If you cannot issue
POST /v1/events/{eventId}/holds/{holdId}/not-issued
{ "reason": "The seat is no longer available" }The sale is reversed, the seats go back to the seller, and the seller is told to refund its buyer. A seller that lost its token can fetch a fresh one until the tickets are issued: POST /v1/events/{eventId}/holds/{holdId}/token.
PUT /v1/events/{id}/issuer-attestation). Bandariva lists, reserves and tracks; you void and reissue and report back; payment between the parties stays outside it.Owning an event
Create the event with its categories (ticket types), publish it, then choose who sells. Invite a seller by link or add an existing organisation, and set its terms: commission, settlement days, an allowed price range, and whether it may reserve live from your unallocated stock. Terms are locked once the seller reports a first sale.
POST /v1/events/{eventId}/partners
{ "organizationId": "…", "participationMode": "PLATFORM_CONTROLLED",
"commissionBps": 1000, "settlementDays": 7 }
POST /v1/events/{eventId}/allocations
{ "partnerOrgId": "…", "categoryId": "…", "quantity": 200 }You can also open an event to verified sellers you have not invited on a single set of terms, and every verified organisation that has accepted the commercial terms can join it. GET /v1/events/{eventId}/overview and /discrepancies show where figures disagree; a door count you record from your own credentials shows the gap between tickets sold and people admitted.
Seats
Describe a hall once as sections of rows (or an explicit list of seats, or a CSV), reuse it on every event made at that venue, and give each seat group a price tier. Add the stage’s side and best-available means nearest the stage.
POST /v1/venues/{venueId}/configurations
{ "name": "Concert", "stage": "TOP", "sections": [
{ "name": "Stalls", "level": "Ground",
"rows": { "labels": "A-T", "seatsPerRow": 24, "numbering": "CENTER_OUT_ODD_LEFT", "aisleAfter": [8, 16] },
"seatAttrs": [{ "rows": ["T"], "numbers": ["1", "24"], "attrs": ["ACCESSIBLE"] }] },
{ "name": "Standing", "kind": "GENERAL", "capacity": 300 } ] }- Seat marks:
ACCESSIBLE(a wheelchair space),COMPANION,RESTRICTED_VIEW,AISLE. - A seat moves between the owner’s pool, a seller, a reservation and a sale, and every move is written to the same ledger as quantities, so statements do not change.
- Every seat has a history you can read, oldest first, that nobody can edit.
GET /v1/events/{eventId}/seatmap/summarygives counts only; over 15,000 seats, ask for one section at a time.
Webhooks
Register an HTTPS endpoint; the signing secret is shown once.
POST /v1/webhook-endpoints { "url": "https://you.example/hooks/bandariva", "eventTypes": ["allocation.changed"] }
# each delivery
POST https://you.example/hooks/bandariva
Bandariva-Event: allocation.changed
Bandariva-Delivery: <delivery id>
Bandariva-Signature: t=<unix seconds>,v1=<hex>
{ "id": "…", "type": "allocation.changed", "createdAt": "…", "data": { … } }Verify it: HMAC-SHA256 with your secret over "<t>.<raw body>", compare to v1 in constant time, and reject if t is more than five minutes old. Delivery is at least once: answer any 2xx to accept, de-duplicate on the id. Anything else (or a timeout after 10 seconds) is retried with growing delays, up to 8 tries over about an hour. Treat a webhook as a prompt to look, not as the only source of truth: reads such as /seatmap/summary and /overview are always right.
Event types include allocation.changed, sale.reported, hold.confirmed, hold.fulfilled, hold.fulfilment_failed, seatmap.changed, seat.conflict, event.updated, event.cancelled and the resale events. Issuers receive hold.confirmed when a seller confirms; sellers receive hold.fulfilled or hold.fulfilment_failed.
Commission and statements
The owner sets each seller’s terms when it adds or invites it. Four settings cover the ways ticket sellers are commonly paid:
| Term | Meaning |
|---|---|
commissionBps | The seller keeps this share of the ticket price and remits the rest. |
ownerFeeShareBps | The share of a fee the buyer paid on top that the seller passes to the owner (0 means the seller keeps it all). |
maxBuyerFeeBps | The most the seller may add on top, as a share of the ticket price. A higher fee is flagged on the statement, never refused. |
collectedBy | SELLER collects and remits; OWNER collects and the seller earns its commission as a referral. |
Report what the buyer paid beyond the ticket price as buyerFeeCents next to amountCents on any sale, confirmation or reversal. GET /v1/events/{eventId}/statements returns, for the owner, each seller’s tickets, gross, commission, buyer fees, the owner’s share, the amount due (positive: the seller owes the owner; negative: the owner owes the seller) and the due date. A seller sees only its own line. Bandariva works out the figures and shows both sides the same page; it does not collect or pay the money and does not decide disputes.
Before you go live
- Every write sends an idempotency key you derive from your own order id.
- You release a reservation as soon as a payment fails or the buyer leaves, rather than letting it lapse. A seller that reserves a great deal and sells little is visible to the owner and may be limited.
- You never record a sale from a lapsed reservation, and you refund if you charged one.
- You verify webhook signatures and de-duplicate on the delivery id.
- Issuers: you verify every token offline, always redeem, and treat
seatCountas “ask for the list”. - You use the section or summary reads for large venues, not the full map on every page view.
- You handle
429by slowing down.