voepy

Billing

How voepy charges you, and how you fund the account that gets charged.

All paths under /v1/billing/. Authorization: Bearer <api-key> required.

How pricing works

Three things determine what you pay:

  1. Per-second voice charges — every answered call is rated after hangup at carrier cost × your markup. The markup is fixed by your plan (or a per-tenant override). Rated charges are debited from your prepaid balance as LedgerEntry rows.
  2. Per-number monthly fee (MRC) — each provisioned DID has a monthly recurring charge. The first month is prorated; subsequent months are billed on the 1st.
  3. One-off activation fee (NRC) — debited up-front when you order a new DID, lookup, or trigger another billable action like gather, streaming, transcription, AMD.

Everything happens against your balance, which you fund via top-ups. If the balance hits zero we reject new outbound calls (402 balance_insufficient); calls already in flight finish normally.

Read your balance

GET /v1/account
{
  "tenant": { "id": "...", "name": "Yourco Voice" },
  "balance": {
    "amount_cents": 12450,         // your remaining credit
    "currency": "USD",
    "credit_limit_cents": 0
  }
}

For live spend and call counts use GET /v1/usage (filterable by date range). Aggregated reports live under /v1/analytics/....

The funding flow

1. Create a Stripe SetupIntent          POST /v1/billing/setup-intent
2. Confirm it in your frontend          (Stripe.js / Elements)
3. Top up                               POST /v1/billing/topup
4. (Optional) Auto-recharge             POST /v1/billing/auto-recharge

Stripe.js handles the card data; your server never touches it. The publishable Stripe key (pk_…) is safe in the browser; the secret one (sk_…) lives only on our side — you never see it.

1 · Create a SetupIntent

POST /v1/billing/setup-intent

Response:

{
  "client_secret": "seti_xxx_secret_yyy",
  "setup_intent_id": "seti_xxx",
  "stripe_customer_id": "cus_xxx"
}

Pass client_secret to Stripe Elements in your frontend. The user enters their card; Stripe attaches it to our Stripe Customer and runs SCA / 3DS if required.

2 · List saved payment methods

GET /v1/billing/payment-methods
[
  {
    "id": "pm_abc123",
    "type": "card",
    "card": {
      "brand": "visa",
      "last4": "4242",
      "exp_month": 12,
      "exp_year": 2028
    },
    "is_default": true
  }
]

Set default: POST /v1/billing/payment-methods/{pm_id}. Remove: DELETE /v1/billing/payment-methods/{pm_id}.

3 · Top up

POST /v1/billing/topup
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "amount_cents": 5000,
  "payment_method_id": "pm_abc123"
}

payment_method_id is optional — omit to use the default. Response:

{
  "payment_id": "...",
  "status": "requires_action",      // or "succeeded" if no 3DS
  "client_secret": "pi_xxx_secret_yyy",
  "amount_cents": 5000,
  "currency": "USD"
}

If status == "requires_action", hand client_secret to stripe.confirmCardPayment() in your frontend to clear 3DS. We credit your balance when Stripe fires payment_intent.succeeded — no extra call needed on your side.

// Frontend
const { error } = await stripe.confirmCardPayment(clientSecret);
if (error) showError(error.message);
else showSuccess("Top-up complete!");

Poll GET /v1/account (or listen for a future balance.credited webhook) to confirm the balance bumped.

4 · Auto-recharge

Optional but recommended for production. We automatically top up when your balance drops below a threshold.

POST /v1/billing/auto-recharge

{
  "threshold_cents": 1000,         // recharge when balance ≤ $10
  "amount_cents": 5000,            // top up by $50
  "payment_method_id": "pm_abc123"
}

We charge the saved card when the threshold is crossed and credit your balance. If the charge fails we email you and disable auto-recharge until you re-enable it.

Disable: DELETE /v1/billing/auto-recharge.

Plans (optional)

Plans bundle a markup tier, included voice minutes, and a monthly fee into a single SKU. You can run on the default markup with no plan, but attaching a plan locks in volume discounts.

List public plans

GET /v1/billing/plans
[
  {
    "slug": "starter",
    "name": "Starter",
    "monthly_fee_cents": 0,
    "included_minutes": 0,
    "markup_percent_bps": 4000,
    "line_items": [
      { "kind": "voice_minute", "unit_amount_cents": 0, "unit_label": "minute", "is_metered": true },
      { "kind": "phone_number", "unit_amount_cents": 150, "unit_label": "number", "is_metered": false }
    ]
  },
  {
    "slug": "growth",
    "name": "Growth",
    "monthly_fee_cents": 9900,
    "included_minutes": 5000,
    "markup_percent_bps": 3000,
    "line_items": [ ... ]
  }
]

Attach a plan

POST /v1/billing/subscription
{ "plan_slug": "growth" }

GET /v1/billing/subscription returns the current subscription; DELETE /v1/billing/subscription cancels it (the cancellation takes effect at the end of the current billing period).

Payment history

GET /v1/billing/payments?status=succeeded&cursor=...&limit=50

Each row:

{
  "id": "9f8c…",
  "amount_cents": 5000,
  "amount_refunded_cents": 0,
  "currency": "USD",
  "status": "succeeded",          // pending | requires_action | succeeded | failed
  "stripe_payment_intent_id": "pi_…",
  "created_at": "2026-05-12T14:32:18.000Z"
}

Invoices

A monthly invoice is generated on the 1st covering MRC charges + overage minutes (anything past the plan-included quota). Top-ups and per-call rated charges are not re-billed on the invoice — they're already debited from your balance.

GET /v1/billing/invoices?status=paid&cursor=...&limit=50

Each row links to a Stripe-hosted PDF you can download or forward to your accounting team.

Refunds

Refunds are admin-initiated. Email support with the payment_id. The refund credits back to the original card and we issue a balancing ledger entry so your balance doesn't double-count.

Quotas

/v1/quota shows your hard limits — daily spend cap, concurrent call cap, allow/deny country lists, and the auto-recharge config in one place.

GET /v1/quota
{
  "daily_spend_cap_cents": 50000,
  "concurrent_call_cap": 25,
  "countries_allowed": ["US", "CA"],
  "countries_blocked": [],
  "prefixes_blocked": [],
  "allow_caller_id_forwarding": false
}

If you hit a cap, calls return 403 daily_spend_cap_exceeded or 403 concurrent_call_cap_exceeded. Daily caps reset at UTC midnight.

Per-tenant cap changes are admin-only — talk to support if you need a ceiling raised.

Next