Billing & payments
Two separate flows of money exist in Norba, and this page is only about the second one.
| Flow | Who pays whom | Where it’s documented |
|---|---|---|
| The traveller pays the agency | Fare + the agency’s own service fee, settled with the airline by the agency. Carried on payment at POST /v1/orders and settled at POST /v1/orders/{orderID}/commit. | Orders Lifecycle |
| The agency pays Norba | A share of what it sold, plus metered usage, 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. Three 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 Norba snapshotted when the offer was priced (or, failing that, the total the carrier confirmed), and every seat/ancillary fee from the catalogue price Norba itself served for it — nothing else can be the basis of a percentage.
- Currency conversion is dated, or deferred, 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. With no rate available yet, the line is written in the carrier’s currency priced at nothing and marked held — it prices itself the moment a rate is added, and is never converted at a guessed rate or silently dropped.
- A same-day cancellation gets its fee back. An order cancelled or refunded within 24 hours of booking is credited its Norba fees as a mirror line, net of anything already credited — so cancelling twice does not credit twice.
Standing: what stops a booking
Every metered request, and every order, is checked against the account’s standing first:
| Situation | Data lookups | Servicing | Booking |
|---|---|---|---|
| Inside the free allowance | free | €2.00 | allowed |
| Past the allowance, card on file | metered | €2.00 | allowed |
| Past the allowance, no card | 402 payment_required | €2.00 | 402 payment_method_required (pay-as-you-go); enterprise books without a card |
| Delinquent (flagged by staff) | allowance only, then 402 delinquent | €2.00 | 402 delinquent |
| Paused by staff | served, not billed | served, not billed | allowed, not billed |
Shopping (search) is never gated and never consumes the allowance. On a deployment with Stripe not configured, nothing is ever gated on a card.
Every gate answers with the same structured body. Most reasons are 402; a plan code the catalogue does not recognise is 403, because no card fixes a configuration problem:
{
"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 free monthly request allowance is exhausted, and no card is on file to bill past it. 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 | Staff have flagged the account for 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 | 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. |
A sixth value, plan_disabled, is reserved in the response contract but not currently produced by any check — a plan is retired by removing it from the catalogue, which surfaces as unknown_plan for any account still on it.
Settlement: from usage to invoice to card
One function turns metered usage into an invoice, under a per-account lock so two settlement runs can never draft against the same usage. It runs from two places:
- The monthly close — every hour, and at boot, for every period that can no longer grow. It releases any usage line that was held for a missing FX rate and now has one, totals the collectable usage by category, adds the volume-ladder discount and any back-office discount as their own lines, draws down the account’s credit balance, drafts the invoice, claims the usage against it (which is what makes re-running the close a no-op), and charges the default card. 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.
- Removing your last card (
DELETE /v1/dev/billing/payment-methods/{id}?charge_outstanding=true) runs the same settlement mid-period: it invoices what is owed so far at the plan rate, claims it, charges the card being removed, and only then detaches it. The month-end close still bills what comes after, and its volume ladder still counts the orders that were settled early.
metadata.norba_invoice_id. The webhook only ever updates that row’s status, number and hosted link; it never touches its period or discount breakdown, and a Stripe invoice this system voided itself is ignored when the same void comes back on the webhook.A declined card leaves the invoice open with its usage already claimed — nothing is re-billed twice. The card stays on file, a retry runs 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. From the back office, staff can settle, void, or write off an invoice directly; voiding also voids the corresponding Stripe invoice, so a debt cancelled on Norba’s side does not stay open and payable on Stripe’s.
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. A support engineer impersonating your account cannot add, promote, or detach a card on your behalf — every mutating route below is blocked during impersonation.
| 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/payment-methods | List saved cards. |
| POST | /v1/dev/billing/setup-intent | Create a Stripe SetupIntent. 503 stripe not configured on a deployment with no Stripe wired. |
| 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. Add ?charge_outstanding=true to settle what's owed so far and charge it first — see Settlement above. |
| GET | /v1/dev/billing/invoices | Up to 100 invoices for the current user, each with its Stripe hosted link and PDF once issued. |
| POST | /v1/dev/billing/webhook | Stripe webhook receiver. Not for integrators to call — listed for completeness. |
clientSecret from POST /setup-intent, and only the resulting pm_… id — never the card number — is sent to POST /payment-methods.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",
"MonthlyFreeRequests": 5000,
"PeriodStart": "2026-09-01T00:00:00Z",
"PeriodEnd": "2026-10-01T00:00:00Z",
"TotalRequests": 812,
"FreeRequests": 812,
"BillableRequests": 0,
"TotalCents": 0,
"HasPaymentMethod": true,
"PaymentRequired": false,
"Orders": 3,
"OrdersCents": 645,
"ByCategory": [
{ "Category": "booking", "Label": "Completed booking", "TotalRequests": 3, "BillableRequests": 3, "TotalCents": 645, "Model": "percentage", "RatePercent": "2.50" }
],
"Delinquent": false,
"RequiresPaymentMethod": true,
"BookingBlocked": false,
"BookingBlockedReason": ""
}Orders/OrdersCents count and total completed bookings only — distinct from BillableRequests, which also includes seats, bags and data calls. 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.
Things worth knowing before you integrate
- Shopping is never billed and never gated — search as much as you want.
- 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 lands in usage_events, but it can never fail or delay the booking itself.
- Every usage line records where its basis came from (airline_offer, airline_order, airline_catalogue, or client_declared) — useful when a fee looks off and you want to know which number it was computed from.
- Money is integer millicents throughout the billing system, so a fee amount you read back is 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.