Skip to main content

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).
curl https://api.orbit.devotel.io/api/v1/billing/status \
  -H "X-API-Key: dv_live_sk_your_key_here"
{
  "data": {
    "stripe_configured": true,
    "has_customer": true,
    "can_manage": true,
    "can_checkout": true
  },
  "meta": {
    "request_id": "req_status_001",
    "timestamp": "2026-05-16T12:00:00Z"
  }
}

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).
curl https://api.orbit.devotel.io/api/v1/billing/balance \
  -H "X-API-Key: dv_live_sk_your_key_here"
{
  "data": {
    "balance_usd": 150.00,
    "balance_cents": 15000,
    "credits": 15000,
    "outbound_paused": false,
    "outbound_block_reason": null
  },
  "meta": {
    "request_id": "req_balance_001",
    "timestamp": "2026-05-16T12:00:00Z"
  }
}
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 BALANCE_TOO_LOW until a successful top-up clears the flag.

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.
Idempotency-Key
string
required
Stable client-generated value, 8–255 characters. Re-using the key for a different (amount, currency) returns 409 IDEMPOTENCY_KEY_REUSED.
amount
integer
required
Amount in minor units (cents). Min 1, max 1000000 ($10,000).
currency
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.
successUrl
string
required
Redirect URL on successful payment. Must be on a trusted origin (orbit.devotel.io).
cancelUrl
string
required
Redirect URL if the user cancels. Must be on a trusted origin.
curl -X POST https://api.orbit.devotel.io/api/v1/billing/balance/top-up \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: topup_2026-05-16_user_42_abc123" \
  -d '{
    "amount": 10000,
    "currency": "usd",
    "successUrl": "https://orbit.devotel.io/billing?topup=ok",
    "cancelUrl": "https://orbit.devotel.io/billing?topup=cancelled"
  }'
{
  "data": {
    "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_live_abc123..."
  },
  "meta": {
    "request_id": "req_topup_001",
    "timestamp": "2026-05-16T12:00:00Z"
  }
}

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.
enabled
boolean
required
Enable or disable auto-top-up.
threshold_minor
integer
Balance threshold (in minor units, e.g. cents) that triggers a top-up.
recharge_amount_minor
integer
Amount (in minor units, e.g. cents) to charge when triggered. Stripe rejects amounts below the per-currency minimum charge.
curl -X PUT https://api.orbit.devotel.io/api/v1/billing/auto-topup \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "threshold_minor": 5000,
    "recharge_amount_minor": 10000
  }'

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.
returnUrl
string
required
URL to redirect to after the portal session ends. Must be on a trusted origin (orbit.devotel.io).
curl -X POST https://api.orbit.devotel.io/api/v1/billing/portal \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "returnUrl": "https://orbit.devotel.io/billing" }'

Transactions ledger

List Transactions

GET /api/v1/billing/transactions?limit=30&type=spend&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.
limit
integer
default:"30"
Page size, max 100.
type
string
Optional filter: topup, spend, refund, bonus.
cursor_ts
string
ISO-8601 timestamp from the previous page’s next_cursor.cursor_ts.
cursor_id
string
Row id from the previous page’s next_cursor.cursor_id.
curl "https://api.orbit.devotel.io/api/v1/billing/transactions?limit=30" \
  -H "X-API-Key: dv_live_sk_your_key_here"
{
  "data": {
    "transactions": [
      {
        "id": "ct_2vXq7p9yzAbCdEf",
        "type": "topup",
        "amount_minor": 10000,
        "reference": "stripe_session:cs_live_abc123",
        "metadata": { "currency": "usd", "fx_rate": 1.0 },
        "created_at": "2026-05-16T11:22:33.444Z",
        "expires_at": null,
        "refunded_at": null
      },
      {
        "id": "ct_2vXq7p9yzAbCdEe",
        "type": "spend",
        "amount_minor": -42,
        "reference": "charge:sms_msg_01J9KP3R5W8YEQ4DXG6N7H2VKZ",
        "metadata": { "channel": "sms", "country": "1" },
        "created_at": "2026-05-16T11:21:18.901Z",
        "expires_at": null,
        "refunded_at": null
      }
    ],
    "next_cursor": {
      "cursor_ts": "2026-05-16T11:21:18.901Z",
      "cursor_id": "ct_2vXq7p9yzAbCdEe"
    },
    "has_more": true
  },
  "meta": {
    "request_id": "req_tx_001",
    "timestamp": "2026-05-16T12:00:00Z"
  }
}

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 for slicing the same spend window.
days
integer
default:"30"
Lookback window, 1–90.
group_by
string
default:"channel"
channel, country, campaign, or sender.
channel
string
Optional filter narrowing to a single channel (e.g. sms, voice).
curl "https://api.orbit.devotel.io/api/v1/billing/usage-by-channel?days=30" \
  -H "X-API-Key: dv_live_sk_your_key_here"
{
  "data": [
    {
      "key": "sms",
      "channel": "sms",
      "total_messages": 3420,
      "delivered": 3380,
      "failed": 12,
      "total_cost_minor": 27360,
      "total_cost": 273.6000
    },
    {
      "key": "voice",
      "channel": "voice",
      "total_messages": 84,
      "delivered": 78,
      "failed": 6,
      "total_cost_minor": 11280,
      "total_cost": 112.8000
    }
  ],
  "meta": {
    "request_id": "req_usage_001",
    "timestamp": "2026-05-16T12:00:00Z"
  }
}
Per-channel billing rules
  • SMS / MMS — charged on submitted (carrier accepted handoff). DLR delivered updates the row’s status but doesn’t double-bill.
  • Voice — billed in 60s increments after answered; busy / no-answer is 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 DLR delivered (Viber state=5); submit-time and rejected messages are free.
See Pricing for the per-channel rate card.

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.
days
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.

Invoices

List Invoices

GET /api/v1/billing/invoices Retrieve past Stripe invoices and one-time payment receipts for the org.
curl https://api.orbit.devotel.io/api/v1/billing/invoices \
  -H "X-API-Key: dv_live_sk_your_key_here"
{
  "data": [
    {
      "id": "inv_abc123",
      "number": "INV-2026-003",
      "amount": 10000,
      "currency": "usd",
      "status": "paid",
      "period_start": "2026-05-01T00:00:00Z",
      "period_end": "2026-05-31T23:59:59Z",
      "pdf_url": "https://pay.stripe.com/invoice/acct_xxx/inv_abc123/pdf",
      "created_at": "2026-05-16T11:22:33Z"
    }
  ],
  "meta": {
    "request_id": "req_inv_001",
    "timestamp": "2026-05-16T12:00:00Z",
    "pagination": {
      "cursor": "cur_inv_abc",
      "has_more": true,
      "total": 12
    }
  }
}
Invoice amounts are in the smallest currency unit (cents for USD). Divide by 100 for display.

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.

Examples

Node.js

import { Orbit } from '@devotel/orbit-sdk'

const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! })

// Check balance
const balance = await orbit.billing.getBalance()
console.log(`Wallet: $${balance.data.balance_usd}`)

// Mint a top-up Checkout session
const session = await orbit.billing.topUp({
  amount: 10000,           // $100.00 in cents
  currency: 'usd',
  successUrl: 'https://yourapp.com/billing?topup=ok',
  cancelUrl: 'https://yourapp.com/billing?topup=cancelled',
  idempotencyKey: `topup_${Date.now()}_${userId}`,
})
// Redirect the user to session.data.checkoutUrl
A first-party Python SDK is on the roadmap but not yet shipped. Call the REST endpoints above with requests / httpx / any HTTP client.