Billing & payments
Three separate movements of money surround a booking, and this page is only about the last one — what you owe Norba. The other two are on Payment Flow.
| Flow | Who pays whom | Where it’s documented |
|---|---|---|
| The traveller pays the agency | The whole price — the airline’s amount plus the agency’s markup — taken on the agency’s own checkout, with its own payment provider. It never reaches Norba. | Payment Flow |
| The agency pays the carrier | The airline’s amount only, on the agency’s own card, carried through payment at POST /v1/orders, or later when it pays a hold. The carrier authorises it and issues the ticket; Norba transmits it and holds none of it. | Payment Flow |
| The agency pays Norba | A share of what it sold, plus metered usage, accrued through the month and charged to a card on file via Stripe. | This page |
What a booking costs
Fees are ad valorem — a percentage of the airline’s own numbers, never of anything the client sent in the request body — with a floor and a ceiling per line and a cap across a whole order. The full rate card, including the pay-as-you-go and enterprise tiers and the monthly volume ladder, is on the pricing page. The rules that don’t fit a rate card:
- The basis is the airline’s price, never the client’s. The booking fee is computed from the base fare the carrier quoted when the offer was priced (or, failing that, the total the carrier confirmed), and every seat/ancillary fee from the catalogue price the API served for it — nothing you send in a request body can be the basis of a percentage.
- Currency conversion is dated, never guessed. Fees are defined in EUR; a basis in another currency converts at a dated FX rate, and the usage line keeps the carrier’s original figure and the rate used, so every fee can be reconciled.
- A sale after the booking is still a sale. A seat or a bag added later through
POST /v1/orders/{orderID}/commitorPUT /v1/orders/{orderID}, and fare added by a reissue, carry the same ad-valorem fee they would have carried at booking — worked out from how much the order’s total grew, by the carrier’s own figures. The flat servicing fee is charged as well, not instead: one is for the operation, the other for what it sold. - A hold is a booking. An order created without a payment is the ordinary unpaid hold: the airline has confirmed it and is holding the seats, so it carries the booking fee from the moment it is created, exactly as a ticketed order does. Paying it later costs nothing more: a booking is charged once, so paying a hold raises no servicing fee. A hold left to lapse keeps its booking fee; one cancelled within 24 hours of booking is credited like any other booking.
- One servicing fee per operation that took effect. The flat servicing fee is raised once for each post-sale operation the airline carried out: adding seats or extras (
…/seats,…/services, or aPUTor…/committhat adds them), a change or reissue (…/change/confirm), a cancellation (DELETEor…/cancel/confirm) and a refund (…/refund). It is not raised on a retry answered with the result of the first request (Idempotent-Replayed: true), on a repeat that changes nothing — cancelling an order that is already cancelled, refunding one that is already refunded, or refunding one already cancelled, whose cancellation carried the fee — or on an operation the airline refused. An order is closed once, so cancelling and refunding it carry one fee between them, however many requests race to do it. Paying a hold (PUT /v1/orders/{orderID}withticketing,paymentorcomplete_payment, or…/commitwith nothing to add) raises none: the booking was charged when it was made. - A same-day cancellation costs nothing. An order cancelled or refunded within 24 hours of booking is credited everything it was charged — the booking fee, its seats and extras, what was added after the sale, and the servicing fees of the operations performed on it — and the cancellation’s own servicing fee is waived, so the order nets €0.00. The credit is a mirror line, net of anything already credited, so cancelling twice does not credit twice. The API calls themselves are metered as usual.
- A quote is a call, not an operation.
POST …/cancel,…/change,…/reshopand…/quotechange nothing and raise no servicing fee; the confirmation that acts on the quote does.
Standing: what stops a booking
Every request that carries an API key is a request. It counts one against the account’s free allowance — 500 requests granted once, for the whole life of the account, never renewed — and, past the allowance, costs its category’s per-call price: €0.005 for a call that reaches an airline (search, price, seat map, creating or servicing an order), €0.0025 for a call answered from our own data (reference data, reading your orders, monitoring). There is no free surface. Booking and servicing fees come on top of the call. Not counted: a request the standing gate itself refused (any of the 402 and 403 reasons below), a 429 from a rate limit or the burst guard, and a 5xx, which is our failure. Everything the handler actually answered counts — a 400 you sent, a 404 for an order that is not there: the work was done either way. The flat servicing fee is narrower than the call it rides on — it is raised only when the servicing operation itself took effect, once per operation, so a 4xx on a servicing call, a replayed retry and a repeat that changes nothing are metered calls and nothing more.
Excess look-to-book. Every airline caps, in its own contract, how many searches it will serve per booking made — its look-to-book ratio, and breaching it is the most common reason an NDC connection gets suspended. While an account’s ratio with one airline is above what that airline allows, each request to that airline carries €0.01 on top of the call, as a separate line naming the carrier. Nothing is refused and nothing is throttled. Three things bound it: it applies only to the airline the ratio is breached with, never to the others; it is not applied below 1,000 searches with that carrier, because a ratio built on fewer means nothing and a new integration’s first morning is all searches and no sales; and it stops by itself the moment the ratio comes back inside — a booking on that airline recalculates it immediately rather than at the end of any period. Your ratio with each airline, its limit and anything charged are on the Airlines page of your dashboard.
Every metered request, and every order, is checked against the account’s standing first. The table below is about production keys: in the sandbox every call is still metered against the allowance, but no booking fee and no servicing fee is raised, and the booking gate does not run at all — a sandbox order is never refused for want of a card, and the dashboard does not warn a sandbox-only agency that it would be. What a sandbox booking’s fee and post-sale commission would have been in production is listed on your billing page as a simulation, and is never invoiced or charged.
A booking the carrier refused raises no booking fee. If the airline declines the card you sent, it collected nothing and you are left with a reservation you cannot ticket, so charging a percentage of it would be charging you for the airline’s refusal. Two things this does not cover: an order sent with no payment at all is the ordinary unpaid hold and is billed like any other booking, and a payment still pending may yet capture, so it is not read as a refusal. The call is metered either way — it was made and answered.
| Situation | Any call | Servicing fee | Booking |
|---|---|---|---|
| Inside the free allowance, card on file | free (counts 1) | €2.00 on top | allowed |
| Inside the free allowance, no card | free (counts 1) | €2.00 on top | 402 payment_method_required (pay-as-you-go); enterprise books without a card |
| Past the allowance, card on file | metered per call | €2.00 on top | allowed |
| Past the allowance, no card | 402 payment_required | not raised — the call was refused | 402 payment_method_required (pay-as-you-go); 402 payment_required on enterprise, because the call itself cannot be billed |
| Email never verified, no card | 403 email_verification_required | not raised | 402 payment_method_required (pay-as-you-go); 403 email_verification_required on enterprise |
| Delinquent (unpaid invoices) | allowance only, then 402 delinquent | €2.00 on top | 402 delinquent |
The last two columns are independent of the first. The booking gate runs before the allowance is consulted, so 500 untouched free requests do not let a pay-as-you-go account create an order without a card: the allowance pays for the call, it never satisfies the gate. An enterprise account books without a card on file, but the booking call is still a call: once the free requests are spent, it needs a card like any other.
The allowance is per account, full stop — every account gets its own 500, for its own life, with nothing shared with any other. A verified email is required to draw it, and a disposable inbox cannot register in the first place. A transfer of an agency’s ownership does not grant a fresh 500: the new owner inherits what the previous owner had left, and the previous owner is left with none.
Every gate answers with the same structured body, with the reason in error, code and reason alike. Most reasons are 402; an unverified email and a plan code the catalogue does not recognise are 403, because a card is not what fixes them:
{
"error": "payment_method_required",
"reason": "payment_method_required",
"message": "Your plan requires a payment method on file before an order can be created. Add a card in your dashboard to book.",
"docs_url": "/billing",
"plan_code": "payg",
"period_start": "2026-09-01T00:00:00Z",
"period_end": "2026-10-01T00:00:00Z"
}| HTTP | reason | Meaning |
|---|---|---|
| 402 | payment_required | The account's free requests are spent — they were a one-off allowance, not a monthly one, so this does not clear at month end — and no card is on file to bill past them. The default reason; every other value below is more specific. |
| 402 | payment_method_required | The plan requires a card on file before an order can be created, and none is attached. Add one, then retry the booking — nothing else needs to change. |
| 402 | delinquent | The account has unpaid invoices. Metered reads still work inside the allowance; booking and anything past it does not, until the open invoices are settled. |
| 402 | past_due | The subscription itself is in dunning with Stripe. New charges are refused until it is settled. |
| 403 | email_verification_required | The account never verified its email address, so it gets no free allowance. Open the verification link (or add a card — calls are then billed at the metered rate). |
| 403 | unknown_plan | The account's plan code does not match anything in the catalogue. Contact support — this is a configuration problem, not something a card fixes. |
| 429 | allowance_burst | Requests are arriving faster than the free requests you have left can be counted, and there is no card to bill an overshoot. Nothing is owed: wait for Retry-After and send the request again, or add a card. The body also carries allowance_remaining. |
Settlement: from usage to invoice to card
Usage accrues through the month and is settled in one of two ways:
- The monthly close. Once a period can no longer grow, its usage is totalled by category, the volume-ladder discount and any discount agreed for your account are added as their own lines, any credit balance on the account is drawn down, the invoice is issued and the default card is charged. A period under €0.50 settles to a zero invoice; a period that nets negative carries the surplus forward as credit instead of issuing a refund. Re-running a close never bills the same usage twice.
- A settlement you ask for mid-period.
POST /v1/dev/billing/settlepays everything owed now — the usage of every period not yet invoiced and every open invoice — with the default card, or the saved card named inpaymentMethodId(itsidfrom the card list). Removing your last card withDELETE /v1/dev/billing/payment-methods/{id}?charge_outstanding=trueruns that same settlement first and detaches the card only once it has collected. Either way what is owed so far is invoiced at the plan rate and charged; the month-end close still bills what comes after, and its volume ladder still counts the orders that were settled early. Removing your last card closes the account’s balance out: anything under €0.50 still owed is waived on a zero invoice, so nothing is left hanging on an account with no card.
A declined card leaves the invoice open — nothing is re-billed twice. The card stays on file, the charge is retried once a day for up to ten attempts, and adding a new default card triggers an immediate retry rather than waiting for the next scheduled one. An invoice Norba voids is voided on Stripe too, so a debt cancelled on one side never stays payable on the other.
A charge whose answer was lost is not a decline. If the card network or Stripe does not answer, Norba asks Stripe what happened before concluding anything, and until it knows, the settle and remove-card calls answer 409 payment_outcome_unknown: the card stays on file and nothing more is charged. Try again in a few minutes — the retry picks up the same charge rather than starting a second one, so an invoice is never paid twice.
Credit and coupons
An account can hold a credit balance. It is drawn down against the next invoice before the card is charged, and a month whose fees net negative — a same-day cancellation credited after the booking was billed, for example — adds to it instead of producing a refund. The current balance, and how much of it would apply if you settled today, are in the billing summary.
Norba sometimes issues a billing credit coupon. Redeem it from the billing page, or with POST /v1/dev/billing/coupons/redeem and { "code": "…" }: its amount is added to the credit balance of the account your agency is billed on, and is used up by your next invoices. Each person can redeem a given coupon once, and redeeming needs the same permission as changing a card. These are not the traveller promo codes of the booking site.
{
"redemptionId": "7b0c5c2e-3f7e-4d0a-9b5e-1d2f3a4b5c6d",
"code": "WELCOME25",
"amountCents": 2500,
"currency": "EUR",
"creditBalanceCents": 2500
}| HTTP | code | Meaning |
|---|---|---|
| 400 | coupon_code_required | The body carried no code. |
| 404 | coupon_not_found | No active coupon has that code. An unknown code and a deactivated one get the same answer. |
| 409 | coupon_already_redeemed | You have already redeemed this coupon. |
| 410 | coupon_expired | The coupon has expired. |
| 410 | coupon_exhausted | The coupon has no redemptions left. |
| 422 | coupon_currency_mismatch | The coupon is in a different currency from your billing account. |
Redemption attempts are rate-limited, because a code is worth money; too many in a short time answer 429 with Retry-After.
Managing billing from your account
These routes sit under /v1/dev/billing/* in the developer portal — they use your session cookie, not an X-API-Key, so call them from the dashboard’s own origin rather than a server-to-server integration. Every member who can see billing sees the same account — the one the agency is billed on — whoever is signed in. An agency’s owner and its admins can change it: cards, billing details, settling, coupons. The finance role can read it — the summary, invoices and their PDFs, the card list and the fee lists — but not change it. Any other member gets 403. Norba support acting on your account cannot add, promote or remove a card, settle, or change your billing details on your behalf.
An agency can also save the legal name, tax ID and postal address that should appear on its invoices, from the dashboard’s billing page or via GET/PUT /v1/dev/billing/details below. They apply to every invoice raised from then on; past invoices are untouched.
Every invoice has a number and a PDF. Norba numbers each invoice it issues in one sequence per year — NB-2026-000042 — including a zero month, one your credit balance paid and one whose card declined, and renders it as a PDF with the billing details it was issued with. Download it from the billing page or from documentUrl on each invoice. When Stripe collected the invoice, Stripe’s receipt is linked beside it and names the same number.
| Method | Path | Description |
|---|---|---|
| GET | /v1/dev/pricing | Public rate card — no session required. Every plan, its tiers and its volume ladder, with every derived figure (percentage, floor, ceiling) computed server-side. |
| GET | /v1/dev/billing/summary | The current period: plan, usage by category, whether a card is on file, and BookingBlocked / BookingBlockedReason in the same reason vocabulary as the 402 above. |
| GET | /v1/dev/billing/details | The billing details on file: billingName, billingEmail, taxId, country, addressLine1, addressLine2, city, region, postalCode. |
| PUT | /v1/dev/billing/details | Save those same fields. They appear on every invoice raised from then on. |
| GET | /v1/dev/billing/payment-methods | List saved cards as { "paymentMethods": [...] }: id, brand, last4, expMonth, expYear, isDefault. The id is what the routes below take. |
| POST | /v1/dev/billing/setup-intent | Create a Stripe SetupIntent: answers clientSecret, setupIntentId and the publishableKey to initialise Stripe.js with. 503 when card payments are not available. |
| POST | /v1/dev/billing/payment-methods | Finalize attachment of the card confirmed against that SetupIntent — body is { "paymentMethodId": "pm_…" }, never raw card details. |
| POST | /v1/dev/billing/payment-methods/{id}/default | Make this card the one charged at settlement. |
| DELETE | /v1/dev/billing/payment-methods/{id} | Remove a card. Refused with 409 balance_outstanding when it is the only card on file and a balance is owed — the balance comes back in the body. Add ?charge_outstanding=true to settle what's owed and charge this card first; if it declines, the answer is 402 payment_declined and the card stays on file; if the charge's answer was lost, 409 payment_outcome_unknown, the card stays and nothing more is charged. See Settlement above. |
| POST | /v1/dev/billing/settle | Pay everything owed now — the usage of every period not yet invoiced plus every open invoice — with the default card, or with { "paymentMethodId": "<id from the card list>" }. 200 with status settled, chargedCents and balance. 402 payment_declined with the remaining balance if the card refuses; 409 payment_outcome_unknown, with chargedCents and balance, if a charge's answer was lost — try again in a few minutes, nothing is charged twice. This is where the exit doors (removing the last card, deleting an account, closing an agency) send you. |
| GET | /v1/dev/billing/invoices | Up to 100 invoices as { "invoices": [...] }: id, number, status, currency, totalCents, amountPaidCents, periodStart, periodEnd, issuedAt, paidAt and documentUrl; hostedInvoiceUrl and invoicePdfUrl are Stripe's receipt when Stripe collected it. |
| GET | /v1/dev/billing/invoices/{id}/document | The invoice as a PDF (Content-Disposition: attachment; filename="<number>.pdf"). Every issued invoice has one. 404 for another account's invoice or a draft. |
| GET | /v1/dev/billing/commissions | The fees your agency's live bookings and post-sale changes were charged, line by line. ?period=current (default) or YYYY-MM. See below. |
| GET | /v1/dev/billing/simulated | What your sandbox bookings would have been charged in production, in the same shape. Never invoiced or charged. ?period=current (default) or YYYY-MM. |
| POST | /v1/dev/billing/coupons/redeem | Redeem a billing credit coupon: { "code": "…" }. See Credit and coupons above. |
clientSecret from POST /setup-intent, and only the resulting pm_… id — never the card number — is sent to POST /payment-methods.That is a different card from the one you pay a carrier with. The card in
payment.card on a booking does pass through the API, because the carrier has to charge it — it is transmitted and never stored, and the security code is optional so you can leave it out where the carrier does not ask for one. See Payment Flow.GET /v1/dev/billing/summary — response
Field names are exactly as below (PascalCase — this response has no snake_case mapping, unlike the rest of the API):
{
"PlanCode": "payg",
"PlanName": "Pay as you go",
"FreeAllowance": 500,
"PeriodStart": "2026-09-01T00:00:00Z",
"PeriodEnd": "2026-10-01T00:00:00Z",
"TotalRequests": 815,
"FreeRequests": 500,
"BillableRequests": 315,
"AllowanceUsed": 500,
"AllowanceRemaining": 0,
"TotalCents": 801,
"HasPaymentMethod": true,
"PaymentRequired": false,
"Orders": 3,
"OrdersCents": 645,
"ByCategory": [
{ "Category": "orders", "Label": "Completed booking", "TotalRequests": 3, "BillableRequests": 3, "TotalCents": 645, "Model": "percentage", "RatePercent": "1.99" },
{ "Category": "shopping", "Label": "Airline call", "TotalRequests": 812, "BillableRequests": 312, "TotalCents": 156, "Model": "flat", "UnitMillicents": 500 }
],
"DiscountCents": 0,
"CreditAppliedCents": 0,
"CreditBalanceCents": 0,
"OutstandingCents": 801,
"OpenInvoiceCents": 0,
"OpenInvoices": 0,
"Delinquent": false,
"RequiresPaymentMethod": true,
"BookingBlocked": false,
"BookingBlockedReason": "",
"EmailVerified": true,
"EmailVerificationRequired": true
}TotalCents is the sum of every ByCategory.TotalCents, so it can never be smaller than OrdersCents. Orders/OrdersCents count and total completed bookings only — distinct from BillableRequests, which also includes seats, bags and data calls. RatePercent is the plan’s headline rate for that category — 1.99% on pay-as-you-go, before the month-end volume ladder discounts it. The €2.00 floor, the €25.00 cap and the ceiling of 2.5% of the base fare are applied when each fee is raised, and do not appear here. BookingBlockedReason is empty until a booking would actually be refused, so a dashboard can show the same warning before an integration ever sees the 402.
FreeAllowance, AllowanceUsed and AllowanceRemaining count over the whole life of the account; FreeRequests is how many of this period’s calls were free. DiscountCents and CreditAppliedCents are not subtracted from TotalCents; OutstandingCents is what you owe right now across every period — open invoices plus usage not yet invoiced, net of discount and credit — and is the figure POST /v1/dev/billing/settle would charge. When EmailVerificationRequired is true and EmailVerified is false, metered calls answer 403 email_verification_required until the address is verified or a card is added.
GET /v1/dev/billing/commissions — the fees on your bookings
Every fee your agency’s live bookings and post-sale changes were charged in a period, one line per priced item: the fare, each seat and extra, the order ceiling’s credit and any same-day cancellation credit. Metered calls, the look-to-book surcharge and the flat servicing fee are not lines here; they are in the summary. GET /v1/dev/billing/simulated answers in exactly the same shape for your sandbox bookings, with simulated: true: what they would have cost in production, never invoiced and never charged. Both take ?period=current (the default) or ?period=YYYY-MM; anything else is 400.
{
"simulated": false,
"sandbox": false,
"periodStart": "2026-10-01T00:00:00Z",
"periodEnd": "2026-11-01T00:00:00Z",
"currency": "EUR",
"lines": [
{
"id": "3f1c2a9e-8b7d-4c6e-a5f4-0e1d2c3b4a59",
"orderId": "AA001U8DY61A3",
"kind": "booking",
"category": "orders",
"label": "Completed booking",
"basisCents": 30000,
"basisCurrency": "EUR",
"basisSource": "airline_offer",
"rateBps": 199,
"ratePercent": "1.99",
"amountMillicents": 597000,
"amountCents": 597,
"createdAt": "2026-10-12T09:41:07Z"
}
],
"totalMillicents": 597000,
"totalCents": 597
}kind is booking or servicing (a sale made after the booking). basisCents is the airline figure the fee was worked out from and basisSource says which one it was; when the airline priced in another currency, basisOriginalCents, basisOriginalCurrency and fxRate carry the original figure and the rate used. bound, when present, says which floor or ceiling decided the amount. A line with a heldReason is not yet billable — typically waiting for an exchange rate. sandbox says whether your agency is still in the sandbox today. Up to 500 lines per response.
Things worth knowing before you integrate
- Every call counts against the 500-request allowance — search, booking, servicing, order reads, reference data, monitoring. The 500 are granted ONCE, for the life of the account: nothing renews them each month, and once they are spent an airline call is €0.005 and a data call €0.0025 from then on, either needing a card on file.
- The allowance is per account — every account has its own 500 and none is shared with any other, even one the same person or business also holds. Transferring an agency's ownership moves the free requests it has left to the new owner; it never starts a new 500.
- The €2.00 servicing fee is charged once for each post-sale operation that succeeds — adding seats or extras, a change or reissue, a cancellation, a refund. Paying a hold raises none: a booking is charged once. Quoting one — POST …/quote, …/reshop, …/change or …/cancel — raises no servicing fee, however often you ask, and neither does a retry answered with Idempotent-Replayed: true or a repeat that changes nothing; the call itself is metered like any other.
- The booking fee is computed once the airline has confirmed the order, from the airline's own numbers, and runs detached from the request — it can delay when a charge appears in your usage, but it can never fail or delay the booking itself.
- Every usage line records which airline figure it was computed from — useful when a fee looks off and you want to know which number is behind it.
- Fee amounts you read back are exact — there is no floating-point rounding to account for.
- The monthly volume ladder is a discount applied once at month-end, not a live rate — an order's own usage line never changes after the fact, only the close's own credit line does.