Billing API
Orbit is pay-as-you-go. There are no plan tiers or recurring subscriptions — every organization has a single prepaid USD wallet (organizations.credits,
stored in minor units / cents) that funds messaging, voice, and AI spend.
Top up the wallet through a one-time Stripe Checkout session; per-send charges
are deducted in real time and recorded in an append-only ledger
(credit_transactions).
Base path: /api/v1/billing
Status
Get Billing Status
GET /api/v1/billing/status
Returns whether Stripe is configured for this organization and whether a
customer record exists yet. The dashboard uses this to gate the “Open Stripe
Portal” CTA — top-up itself does not require a pre-existing customer (Checkout
auto-provisions one on first payment).
Wallet balance
Get Balance
GET /api/v1/billing/balance (alias: GET /api/v1/billing/credits)
Retrieve the current wallet balance in USD cents plus the outbound-pause
flag (set when the balance hits zero or a payment-method issue parks the
account).
balance_cents and credits are the same minor-unit (cents) value —
credits is preserved for backwards compatibility. balance_usd is the
same number divided by 100. When outbound_paused: true is set, all
outbound sends (SMS, voice, AI compose) return SENDING_PAUSED
(HTTP 402) until the billing alert that tripped the pause is reset in
the billing alerts dashboard.Top up the wallet
Create Top-Up Session
POST /api/v1/billing/balance/top-up (alias: POST /api/v1/billing/credits/purchase)
Creates a Stripe Checkout session (mode: payment — one-time charge, NOT a
subscription) for adding USD credits to the wallet. Stripe webhook
checkout.session.completed credits the wallet asynchronously after payment
clears.
The Idempotency-Key header is required. Re-submitting the same key
within 60 seconds (Redis hot-path) or within the durable replay window
(Postgres topup_idempotency_records) returns the original Checkout URL
instead of creating a duplicate session.
string
required
Stable client-generated value, 8–255 characters. Re-using the key for a
different
(amount, currency) returns 409 IDEMPOTENCY_KEY_REUSED.integer
required
Amount in minor units (cents). Min
1, max 1000000 ($10,000).string
default:"usd"
usd or eur. The wallet stores values in the org’s default currency;
Stripe webhook handlers apply FX conversion when the charge currency
differs.string
required
Redirect URL on successful payment. Must be on a trusted origin
(
orbit.devotel.io).string
required
Redirect URL if the user cancels. Must be on a trusted origin.
Configure Auto-Top-Up
PUT /api/v1/billing/auto-topup
Set up automatic top-ups when the wallet balance falls below a threshold.
Companion routes: GET /api/v1/billing/auto-topup reads the current
config, DELETE /api/v1/billing/auto-topup disables and clears it. The
scheduled charge fires from the webhook-worker on a 15-minute tick once
the threshold is crossed and uses the org’s default off-session payment
method.
boolean
required
Enable or disable auto-top-up.
integer
required
Balance threshold (in minor units, e.g. cents) that triggers a top-up.
Required when
enabled is true; ignored when enabled is false. Must be
a positive integer of at least 100 minor units ($1.00, or the equivalent in
your wallet currency).integer
required
Amount (in minor units, e.g. cents) to charge when triggered. Required when
enabled is true; ignored when enabled is false. Must be a positive
integer between 100 minor units (10,000),
and it must be greater than threshold_minor so each top-up lifts the balance
back above the trigger. Stripe also rejects amounts below the per-currency
minimum charge.integer
default:"null"
Optional monthly spend cap (in minor units). Once the calendar month’s
auto-charges would exceed this value, the scheduler stops topping up until the
cap resets at the start of the next UTC month. Must be at least
recharge_amount_minor, otherwise the first charge would already breach it.
Omitting this field — or sending null — leaves any previously saved cap in
place; to remove a cap you must send clear_cap (see below).integer
default:"5"
Optional cap on how many auto-top-ups may fire per UTC day, counting both
scheduled and interactive top-ups. Must be between
1 and 100. Omit the
field or send null to use the default of 5 per day.boolean
default:"false"
Set to
true to remove a previously saved max_monthly_minor cap. Because
omitting max_monthly_minor (or sending it as null) preserves the stored
cap rather than clearing it, removing a cap is an explicit action. Only the
literal boolean true clears the cap; any other value is ignored.When
enabled is true, both threshold_minor and recharge_amount_minor
are required. Sending { "enabled": true } on its own returns a 422.Clearing the monthly cap. Leaving
max_monthly_minor out of the request
body — or sending it as null without clear_cap — keeps any previously
saved cap in place. This is deliberate: a partial form submit that blanks the
cap field can’t silently drop a limit another admin configured. To actually
remove the cap, send "clear_cap": true; the cap is then set to null.Open Stripe Customer Portal
POST /api/v1/billing/portal
Mints a short-lived Stripe Customer Portal URL where the end user can manage
saved payment methods, download tax receipts, and view past charges. This
is NOT a subscription portal — Orbit has no recurring subscription state to
manage.
string
required
URL to redirect to after the portal session ends. Must be on a trusted
origin (
orbit.devotel.io).Crypto top-up (NOWPayments)
Fund the wallet with cryptocurrency through a NOWPayments hosted invoice, as an alternative to Stripe Checkout. The wallet is credited asynchronously once the on-chain payment confirms. These endpoints require theowner, admin,
or billing role.
Crypto top-up must be enabled by your operator. If it is not yet available
for your account, these endpoints return
503 SERVICE_UNAVAILABLE — use
Stripe Checkout in the meantime.Create Crypto Top-Up Session
POST /api/v1/billing/balance/top-up-crypto
Mints a NOWPayments hosted invoice and returns its URL. Like the Stripe
top-up, the Idempotency-Key header is required — re-submitting the same
key returns the original invoice instead of minting a new one.
string
required
Stable client-generated value, 8–255 characters. Re-using the key for a
different
(amount, currency) returns 409 IDEMPOTENCY_KEY_REUSED.integer
required
Amount in minor units (cents). Min
100 (10,000).string
default:"usd"
Only
usd is supported today — the wallet ledger is USD-denominated.string
required
Redirect URL after the payment confirms. Must be on a trusted origin
(
orbit.devotel.io).string
required
Redirect URL if the payer closes the invoice. Must be on a trusted origin.
feePaidByUser reflects the network-fee policy. For top-ups of 50 the fee is added on top of amount at checkout, so
feePaidByUser is true and feeCents is 0 — as in the $100 example above,
where the network fee appears on the NOWPayments payment page rather than in
this response.Get Crypto Top-Up Status
GET /api/v1/billing/crypto-topup/{intentId}/status
Poll the status of a crypto top-up intent while the payment settles. Returns
404 when the intent does not exist or belongs to another organization.
string
required
The
intentId returned by the create call.Cancel Crypto Top-Up
POST /api/v1/billing/crypto-topup/{intentId}/cancel
Cancel a crypto top-up that has not yet settled. Succeeds only while the
payment is waiting or confirming; a payment that is already on-chain or
credited returns 409 PAYMENT_IN_FLIGHT with the live status in details. If
funds arrive after a cancel, the wallet is still credited automatically — a
cancel never loses a payment.
Pay by link
Collect a one-off customer payment over any reachable channel. Orbit mints a Stripe-hosted Checkout link and renders a request-to-pay message for the highest-priority channel the contact can receive it on (SMS, RCS, WhatsApp, email, or a voice IVR read-out). Nothing is sent for you — the rendered message and hosted URL are returned for your own send pipeline. These endpoints require theowner, admin, or billing role.
Create a Pay-by-Link
POST /api/v1/billing/pay-by-link
Selects the delivery channel, mints the hosted Checkout Session, and renders
the message body. Returns 422 NO_REACHABLE_CHANNEL when none of the
preferred channels is reachable for the contact.
integer
required
Amount to collect in minor units. Min
1, max 99999999.string
required
3-letter ISO 4217 currency code (e.g.
usd).string
required
What the payment is for, 1–500 characters. Shown on the Checkout page and in
the rendered message.
string[]
required
Delivery channels in descending priority order. Each of
sms, rcs, whatsapp, email, voice.string[]
required
Channels the contact is currently reachable on. The first
preferredChannels
entry that also appears here is chosen.string
required
Redirect URL after payment. Must be on a trusted origin (
orbit.devotel.io).string
required
Redirect URL if the payer cancels. Must be on a trusted origin.
string
Optional conversation to associate the payment with.
string
Optional contact to associate the payment with.
string
Optional merchant name to render in the message.
string
Optional BCP-47 locale for the rendered message (e.g.
en-US).Preview a Pay-by-Link Message
POST /api/v1/billing/pay-by-link/preview
Render the request-to-pay message for a URL you already have, without minting
a new Checkout Session. Takes the same preferredChannels / reachableChannels
selection plus url, amountMinor, currency, description, and the
optional merchantName / locale. Returns the selected channel and rendered
message, or 422 NO_REACHABLE_CHANNEL.
Transactions ledger
List Transactions
GET /api/v1/billing/transactions?limit=30&type=debit&cursor_ts=...&cursor_id=...
Cursor-paginated read of public.credit_transactions. Negative
amount_minor values are spends (sends, voice minutes, AI tokens, etc.);
positive values are top-ups and refunds. Cursor is composite
(created_at, id) so ties on the same nanosecond don’t skip or
duplicate.
integer
default:"30"
Page size, max 500.
string
Optional exact-match filter on a ledger row’s
type. Common credit
types are credit_purchase (wallet top-up), admin_grant, bonus,
refund, and credit (other wallet credits, e.g. trial or sub-account
transfers); common debit types are debit (per-message, per-minute, or
number-lookup usage), credit_refunded and refund_clamp (top-up
refund offsets), and admin_deduct. The filter matches the stored value
exactly, so an unrecognised value returns no rows.string
ISO-8601 timestamp from the previous page’s
next_cursor.cursor_ts.string
Row id from the previous page’s
next_cursor.cursor_id.Usage analytics
Get Usage By Channel
GET /api/v1/billing/usage-by-channel?days=30&group_by=channel
Per-channel spend + volume breakdown sourced from credit_transactions.
Powers the dashboard usage card. Supports group_by axes
channel | country | campaign | sender | cost_center for slicing the same
spend window. cost_center rolls spend up by the chargeback tag set on a
send’s metadata.cost_center, so internal cost centers can attribute spend
to a project, department, or customer (untagged sends bucket under
untagged).
integer
default:"30"
Lookback window, 1–90.
string
default:"channel"
channel, country, campaign, sender, or cost_center.string
Optional filter narrowing to a single channel (e.g.
sms, voice).Per-channel billing rules
- SMS / MMS — charged on
submitted(carrier accepted handoff). DLRdeliveredupdates the row’sstatusbut doesn’t double-bill. - Voice — billed in 60s increments after
answered;busy/no-answeris free. - WhatsApp BYO — Orbit does not charge messaging fees; the tenant’s own WABA cost is settled directly with Meta. Orbit only charges AI compose tokens when an agent generates the message body.
- RCS / Email / Push — charged on
submitted(carrier or Resend accepted). - Viber Tier 1 (SMPP) — charged on
submitted(carrier accepted handoff), same as SMS/MMS. A later DLRdelivered(Viber state=5) updates the row’sstatusbut doesn’t double-bill. Pre-submit rejections (the provider never accepts the handoff) are free.
Get Spend Series
GET /api/v1/billing/spend-series?days=30
Per-day spend (positive minor units) over the lookback window plus a
7-day moving-average forward projection. Anomaly days (spend > mean + 2σ)
are flagged for callout in the dashboard chart.
integer
default:"30"
Lookback window in days, 1–90.
Get Burn Rate
GET /api/v1/billing/burn-rate?days=30
Returns the trailing average daily spend and the projected number of
days remaining before the wallet hits zero (balance_cents / avg_daily_spend).
Powers the dashboard’s BalanceWidget “X days remaining” inline copy.
What-if pricing preview
POST /api/v1/billing/whatif-pricing/preview
Re-price a window of your own recorded usage against a candidate rate card
and get back the exact per-lane and total cost delta versus what you pay
today. The traffic is held fixed — only the rates change — so the result is
a faithful re-pricing of real usage, not a forecast of hypothetical volume.
Use it to back-test a negotiated sheet, a new plan, or a competitor’s
published rates before you commit.
Read-only: it moves no money, writes no record, and can be run as often as
you like. Restricted to the owner, admin, and billing roles. The
dashboard’s interactive What-if simulator
(Billing → Pricing → What-if simulator) covers the quick per-channel case;
this endpoint is the programmatic path when you need per-country, per-direction,
and per-sub-type granularity.
integer
default:"30"
Look-back window in days,
1–90. Usage recorded in this window is
aggregated into billing lanes (channel × country × direction × sub-type).object[]
required
The candidate rate card — at least one row, up to 5000. A row is a partial
override: any lane the card does not price keeps its current cost
(
candidate_matched: false, zero delta), exactly like an org pricing
override layers over base rates. Country-specific rows take precedence over
* wildcards, and finer sub-types (e.g. WhatsApp marketing vs utility)
are matched the same way as on your live bill.string
Optional human label echoed back in the response (e.g.
Plivo Growth 2026),
max 120 characters.All money fields are in USD cents;
*_unit_price_cents carries sub-cent
precision while *_total_cents is a whole-cent, rounded-up (never
under-bill) total. A negative delta_cents / delta_percent means the
candidate card is cheaper for your mix. candidate_matched: false flags a
lane the candidate card does not cover — spot these before you commit.
delta_percent is null when the current cost is 0 but the candidate
cost is not.Budget cap and spend monitoring
Two read-only endpoints for keeping spend under control: a monthly budget cap you can check before an action, and a velocity-anomaly signal that flags a sudden surge before the hard daily caps bite. Both require theowner,
admin, or billing role.
Get Budget Cap
GET /api/v1/billing/budget-cap
Returns the organization’s monthly budget cap (when one is configured), the
current calendar-month period boundaries in UTC, and month-to-date spend. Use
it to warn a user before an action would push spend past the cap — for
example, showing a “would exceed monthly cap” banner before launching a
campaign.
Response fields:
cap_cents— the monthly cap in minor units (cents), ornullwhen no cap is configured.period_start/period_end— ISO-8601 UTC boundaries of the current calendar month.period_endis exclusive (the first instant of next month).current_spend_cents— spend so far this month, in minor units.
404 when the organization record is missing or has been deleted.
Detect Spend Anomalies
GET /api/v1/billing/spend-anomaly
Flags channels whose current spend velocity has surged past the organization’s
trailing baseline. Each channel’s spend so far today is extrapolated to a
full-day rate and compared against the average of the trailing baseline
window; a channel projecting past the baseline by the surge multiple (and
above an absolute floor) is flagged. This catches a compromised account
before the hard daily caps are exhausted. Flagged channels are also recorded
for operator review, deduplicated to one alert per channel per UTC day.
Response fields:
anomalies— array of flagged channels. Each haschannel,baseline_daily_minor,today_spend_minor,projected_daily_minor,surge_ratio(projected ÷ baseline, ornullfor a channel with no prior spend), andseverity(highorcritical).detected—truewhenanomaliesis non-empty.baseline_days— trailing complete days used for the baseline (14).surge_multiplier— the multiple over baseline required to flag (10).min_projected_daily_minor— the absolute projected-spend floor a channel must clear to flag, in minor units (25000 = $250/day).day_fraction_elapsed— fraction of the current UTC day elapsed, used for the extrapolation.
Committed-use drawdown
Get Commitment Drawdown
GET /api/v1/billing/commitment
Read-only drawdown meter for organizations on a negotiated monthly spend
commitment. Reports how much of the commit has been drawn down this period, a
linear run-rate projection, and the projected end-of-period true-up
(minimum-commit shortfall if under pace, overage if exceeded). Requires the
owner, admin, or billing role.
Organizations without a commitment configured receive { "commitment": null }
alongside the current period boundaries and spend, so a dashboard panel can
self-hide. Returns 404 when the organization record is missing.
Pricing previews
Read-only “see what it would cost” estimators. Each runs a pure pricing calculator over the inputs you supply and returns the computed breakdown — no money moves, no rows are written, and no charges fire. All require theowner,
admin, or billing role.
Preview Volume-Tier Discount
GET /api/v1/billing/volume-tier-preview?channel=sms&projected_volume=250000
Given a channel, a hypothetical monthly projected_volume, and an optional
destination country_code, returns the discount ladder your organization
bills against for that channel (your negotiated tiers, else the platform
defaults), the per-unit price the projected volume earns, and the projected
spend and savings versus the undiscounted rack rate.
string
required
Channel to price (e.g.
sms, whatsapp, voice).integer
required
Hypothetical monthly volume, a positive integer.
string
Optional ISO 3166-1 alpha-2 destination (e.g.
US) for a country-specific
ladder.Simulate a Rate Card (What-If Pricing)
POST /api/v1/billing/whatif-pricing/preview
Replays your organization’s own recorded usage over the last N days through a
candidate rate card and returns the exact per-lane and total cost delta versus
today’s live rates. Use it to back-test a proposed rate card, volume tier, or
committed-use plan against real traffic before switching.
integer
default:"30"
Look-back window, 1–90 days.
object[]
required
The candidate rate card, at least one row. Each row:
channel (required),
country_code (ISO alpha-2 or *, default *), direction (mt or mo,
default mt), optional sub_type, rate_per_unit, and optional
markup_bps, markup_fixed_cents, absolute_price_cents.string
Optional label echoed back in the response (e.g.
Growth plan 2026).The response contains
window_days, the echoed label, a per-lane lanes
array (current vs candidate cost), and a totals object with the aggregate
cost delta. Lanes the candidate card does not cover are flagged so you can
spot gaps before committing.Preview AIaaS Usage Pricing
POST /api/v1/billing/aiaas-usage-pricing/preview
Estimates the AIaaS pure-usage charge breakdown across the message,
voice-minute, and resolved-outcome axes for a hypothetical metered period.
Send a usage object (messageCount, voiceMinutes, resolvedOutcomes[])
and an optional config; omit config to use the committed pure-usage
defaults. Returns the computed charge breakdown in micro-cents.
Preview Marketplace Rev-Share
POST /api/v1/billing/marketplace-rev-share/preview
Estimates the marketplace rev-share split and per-builder payout rollup for a
set of metered agent/tool invocations under a pricingByListing map, with an
optional minPayoutMicroCents threshold. Returns the rev-share breakdown
and the aggregated payouts.
Preview Resolution Usage Meter
POST /api/v1/billing/resolution-usage-meter/preview
Projects a set of confirmed AI resolutions into idempotent usage events, a
per-period meter rollup, and pay-per-resolution invoice line descriptors. Send
resolutions[] and an optional outcome-pricing config (omit for the opt-out
defaults, which produce no invoice lines). Returns events, rollup, and
lineItems.
Preview Margin Rollup
POST /api/v1/billing/margin-rollup/preview
Rolls a set of reseller profitability lines up into net margin per
sub-account, per channel, and per region, so you can see which parts of your
book actually make money — not just what you billed. Each line pairs the
revenue you billed a sub-account with the cost of goods sold (the carrier or
wholesale cost) for the same usage:
string
required
The sub-account the usage is attributed to.
string
required
The channel the usage ran on (for example
sms, voice, whatsapp, email).string
Destination region or country bucket. Omitted lines roll up under
unknown.string
required
ISO-4217 currency. Every line must share one currency; mixed currencies
return
422 VALIDATION_ERROR.integer
required
Revenue billed for the line, in minor units (cents). Maps directly onto the
amount_minor a meter rollup returns per sub-account.integer
required
Cost of goods sold for the line, in minor units. Maps onto the meter rollup’s
base_amount_minor (the carrier pass-through), or supply your own carrier
cost figures.number
Billable units the line represents (informational; summed per group).
string
Optionally force the rollup currency; omitted uses the first line’s currency.
ledger_entries from
POST /api/v1/usage/meters/rollup, so you can
feed a rated period straight in. The report moves no money and stores nothing.
Returns totals plus by_subaccount, by_channel, and by_region arrays
(each sorted by net margin, most profitable first). Every group carries
revenue_minor, cost_minor, margin_minor (negative when a line sold below
cost), margin_pct_bps (margin as basis points of its own revenue), and
revenue_share_bps (its share of total revenue).
Outcome-based pricing
Configure per-resolved-outcome AI-agent billing (a fixed charge each time an agent resolves a conversation, instead of per-token). Charges fire downstream when a conversation passes its rubric and the organization has opted in — this config surface moves no money itself. Requires theowner, admin, or
billing role. Rates are in micro-cents (1 cent = 1,000,000 micro-cents; the
$0.005 benchmark is 500000).
Get Outcome Pricing
GET /api/v1/billing/outcome-pricing
Returns the current config, or the opt-out defaults (enabled: false, the
benchmark rate, no per-outcome overrides) when it has never been set.
Update Outcome Pricing
PUT /api/v1/billing/outcome-pricing
Enable or disable outcome billing and set the rates. Returns the persisted
config. Returns 422 VALIDATION_ERROR when a rate is out of range.
boolean
default:"false"
Whether outcome charges are applied for this organization.
integer
default:"500000"
Charge per resolved outcome in micro-cents,
0–1000000000 ($10.00).object
default:"{}"
Optional per-outcome-key rate overrides (each
0–1000000000 micro-cents).Seats
Report and manage agent seat tiers.full seats are billable; light seats
(read-only, KB-authoring, reporting, manager dashboards) are free. Reading the
breakdown is open to owner, admin, and billing; changing a seat tier is
restricted to owner and admin. All amounts are in minor units (cents).
Get Seat Breakdown
GET /api/v1/billing/seats
Returns full vs light seat counts, the per-full-seat monthly rate, the
estimated monthly seat cost, and the estimated savings versus an all-full-seat
plan.
Update a Seat Type
PATCH /api/v1/billing/seats/{userId}
Flip a user between full and light, then return the recomputed breakdown.
Returns 404 when the user is not in your organization. Requires the owner
or admin role.
string
required
ID of a user in your organization.
string
required
full or light.Trial credits
Grant and check the one-time promotional trial credit. Requires theowner or
admin role.
Get Trial Credit Status
GET /api/v1/billing/trial-credits/status
Reports whether the trial credit has already been granted for your
organization, so the “Claim trial credit” control can hide once it has fired.
Always scoped to your own organization.
Grant Trial Credits
POST /api/v1/billing/trial-credits
Grants the trial credit to your organization. The grant is one-time — a repeat
call is a no-op. amount is bounded by a per-tenant cap; a request above the
cap returns 422 TRIAL_CREDIT_CAP_EXCEEDED.
integer
Optional credit amount in minor units (cents). Omit to grant the default
promotional amount.
Invoices
List Invoices
GET /api/v1/billing/invoices
Retrieve the org’s invoices, newest first. Results are cursor-paginated.
string
Filter by lifecycle status. One of
draft, issued, paid, void, synced.string
Opaque pagination cursor. Pass the
next_cursor value from the previous page
to fetch the next one.integer
default:"25"
Maximum number of invoices to return. Minimum
1, maximum 100.Monetary fields (
amount_due, total, subtotal) are decimal strings in the
major currency unit (e.g. "100.00" is $100.00) — no division required.
next_cursor is null and has_more is false on the final page.Refunds (owner only)
POST /api/v1/billing/refunds issues a Stripe refund against a specific
top-up credit_transaction lot (30-day window). GET /api/v1/billing/refunds
lists the org’s refund history. Both endpoints are restricted to the org
owner role — unexpected refunds against a prepaid wallet are high-impact
and hard to reverse.
Create a refund
POST /api/v1/billing/refunds
string
required
ID of the top-up
credit_transaction lot to refund against, 1–128
characters. Must belong to your org and be a credit_purchase lot; the
refund is drawn from that lot’s remaining unused balance.integer
required
Partial refund amount in wallet minor units (USD cents). Integer, min
1,
max 1000000000. Cannot exceed the lot’s remaining refundable balance —
the request returns 422 VALIDATION_ERROR if it does.string
Optional free-text note (max 500 characters) attached to the Stripe refund
metadata and the refund audit record.
Examples
Node.js
A first-party Python SDK is on the roadmap but not yet shipped. Call the REST
endpoints above with
requests / httpx / any HTTP client.