Billing API

An agent holding a Rove API key can buy credits for its account, within limits the account owner sets. There are two ways a purchase is paid:

  • Saved card, off-session. The owner saved a card and allowed agents to charge it. The agent's purchase is charged at once, with nobody present.
  • Payment link for a person. Otherwise the purchase returns a Stripe Checkout link (next_action.url). The agent gives it to a person, who pays in their own browser.

Both go through one endpoint, POST /v1/billing/purchases. Rove checks the owner's limits before any money moves, and the card number never reaches Rove or the agent.

Machine-readable reference: OpenAPI document. Base URL: https://api.roveapi.com.

Owner setup

Agent purchasing is off by default. Nothing below is possible with an API key; the owner signs in to Billing.

  1. Turn on agent purchasing (Billing → Agent purchasing). Set the most per purchase, a period (calendar day, week from Monday, or month, in UTC) and the most per period. The form starts at 25 dollars per purchase and 100 dollars a month; the highest you can set without contacting Rove is 500 dollars per purchase and 2,000 dollars per period. Purchases you make yourself on the billing page do not count toward this limit.
  2. Allow a key to buy (API keys, "Agent may buy credits"). Each key needs its own grant. A grant may carry lower limits of its own; they can never exceed the account's. Test keys are granted the same way.
  3. Save a card (optional, Billing → Payment method). The card is entered on a Stripe-hosted page. Then choose Allow charging to let agents charge it without asking you. Without a saved card, or without that permission, every agent purchase becomes a payment link for you.
  4. Auto top-up (optional, Billing → Auto top-up). When the balance falls below a threshold, Rove buys the pack you chose on the saved card. It needs a saved card with charging allowed, spends within the same period limit as agents, and pauses (and emails you) when the bank asks you to confirm a payment, the card is declined or removed, or a limit no longer fits the pack.

You are emailed when an agent creates a payment link (with the link), when auto top-up pauses or is blocked by the period limit, and when any purchase is paid. Every change to these settings is recorded and shown under Agent purchasing.

Owner routes use the signed-in browser session, not an API key, and accept changes only from the Rove web app (30 changes per account per hour):

RouteWhat it does
GET, PUT /v1/account/billing/agent-purchasingRead or set state (off, on), per_purchase_max_cents, period, period_max_cents, off_session_allowed
PUT, DELETE /v1/account/billing/keys/{keyId}/purchase-grantAllow a key to buy, with optional per_purchase_max_cents and period_max_cents; or revoke it
POST /v1/account/billing/payment-method/setup-sessionsReturns a Stripe-hosted url for saving a card
DELETE /v1/account/billing/payment-methodRemove the saved card (pauses auto top-up)
GET, PUT /v1/account/billing/auto-topupRead, arm (state: "armed", threshold_credits, item_id) or turn off auto top-up
GET /v1/account/billing/balanceThe account's balance and limits

Prices

Credits never expire. The packs on sale:

PackPriceCredits
1,000 credits$101,000
5,000 credits$495,000
10,000 credits$8910,000

GET /v1/billing/catalogue (public, no key) returns the same packs with their id (for example pack_1k), the subscription tiers, what each operation costs and the supervised-minute rate. See Credits & Pricing.

Endpoints

Apart from the public catalogue, all take Authorization: Bearer rvp_live_... (or rvp_test_...), cost no credits, and answer with Cache-Control: no-store.

Method and pathReturnsNotes
GET /v1/billing/cataloguepacks, tiers, costspublic; no key needed
GET /v1/account/balanceBalanceViewthe balance, exact to the millicredit, and whether this key may buy (purchasing)
POST /v1/billing/purchases201 PurchaseView; replay 200body item_id, optional payment; Idempotency-Key header required
GET /v1/billing/purchasesdata, next_cursorpurchases this key made, newest first; ?limit=1..100 (default 20) &cursor=
GET /v1/billing/purchases/{id}PurchaseViewonly purchases this key made
POST /v1/billing/purchases/{id}/cancelPurchaseViewcancels an unpaid purchase; canceling a canceled one returns it
POST /v1/billing/purchases/{id}/simulatePurchaseViewtest keys only, see Test purchases

POST /v1/billing/purchases is limited to 10 purchases per key and 20 per account per hour, counted separately for live and test keys.

Check what this key may buy

GET /v1/account/balance answers, for this key:

  • credits_remaining (whole credits) and credits_remaining_millicredits (exact; 1 credit = 1,000 millicredits).
  • purchasing.allowed, and purchasing.reason when not: agent_purchasing_off or key_not_granted, with owner_action_url for the owner.
  • purchasing.packs_within_limits: the pack ids this key may buy right now without exceeding any limit.
  • purchasing.payment_modes and purchasing.limits (per purchase, the period, what is already committed, and period_resets_at).
  • supervised_millicredits_per_minute: the rate a supervised session started now would use.
  • auto_topup: its state, or null.

Buy a pack

curl -X POST https://api.roveapi.com/v1/billing/purchases \
  -H "Authorization: Bearer rvp_live_YOUR_KEY" \
  -H "Idempotency-Key: 6f1c2b7e-buy-pack-1k" \
  -H "Content-Type: application/json" \
  -d '{ "item_id": "pack_1k", "payment": "auto" }'

payment chooses how it is paid:

paymentBehaviour
auto (default)Charge the saved card when the owner allowed it; if there is none, or the bank asks for confirmation, or the card is declined, return a payment link instead
checkoutAlways return a payment link
saved_methodOnly the saved card. Refused with 403 purchase-not-permitted (suggested_payment: "auto") when there is no card or charging is not allowed; a decline is 402 payment-failed

A purchase that needs a person comes back open with a payment link:

{
  "id": "0b9b5b8e-6a3b-4f4e-9d8c-2f1e4c3b2a10",
  "object": "purchase",
  "status": "open",
  "livemode": true,
  "actor": "api_key",
  "api_key_id": "6d0f3c1e-2a4b-4c5d-8e9f-0a1b2c3d4e5f",
  "api_key_name": "ci-runner",
  "replayed": false,
  "channel": "checkout",
  "item": { "id": "pack_1k", "version": 1, "name": "1,000 credits" },
  "amount_cents": 1000,
  "currency": "usd",
  "credits": 1000,
  "amount_refunded_cents": 0,
  "millicredits_reversed": 0,
  "millicredits_held": 0,
  "next_action": {
    "type": "open_checkout_url",
    "url": "https://checkout.stripe.com/c/pay/cs_live_example",
    "expires_at": "2026-10-10T15:40:00.000Z",
    "reason": "no_saved_method",
    "instruction": "Give this link to the account owner to pay $10.00 before 15:40 UTC."
  },
  "retry": null,
  "failure_code": null,
  "created_at": "2026-10-10T14:40:00.000Z",
  "paid_at": null
}

What each status means for the agent:

statusNext step
paidCredits were added.
processingThe card payment is going through. Poll GET /v1/billing/purchases/{id} every 5 seconds or slower.
open, requires_actionGive next_action.url to a person (see The payment link), then poll. next_action.reason says why: checkout_requested, no_saved_method, off_session_not_allowed, authentication_required or card_declined.
createdBilling has not confirmed it yet. Repeat the same request with the same Idempotency-Key after 30 seconds; it is never charged twice.
failed, expired, canceledNothing was charged. A new attempt needs a new Idempotency-Key.
refunded, disputed, reversedChanged after payment by the owner or the bank.

Idempotency and retries

Idempotency-Key is required on POST /v1/billing/purchases: 1 to 128 characters of A-Z a-z 0-9 _ . : -, unique within the account. Use a new key for each purchase you mean to make, and the same key when repeating a request whose answer you did not get. A repeat with the same key and body returns the purchase as it is now, with status 200, replayed: true and the header Idempotent-Replayed: true; it never charges again. The same key with a different body is 422 idempotency-key-reused.

Every billing problem carries retry, which says whether to repeat the request and with which key:

  • same_key: repeat it with the same Idempotency-Key, after Retry-After when present.
  • new_key: this attempt is over; a new attempt needs a new key.
  • none: repeating it fails again until something changes (a limit, a grant, the request).
OutcomePurchase rowretryWhat to do
400, 401, 403 purchase-not-permitted, 403 spend-limit-exceeded, 404 pack-not-foundnonenone (but same_key for purchase-not-permitted with suggested_payment)Fix the cause, or give a person owner_action_url. The key is unused.
429 purchase-rate-limitednonesame_keyWait Retry-After, repeat.
503 billing-unavailablenone, or kept created (purchase_id present)same_keyWait Retry-After, repeat with the same key. A kept purchase is reconciled with Stripe and never charged twice.
409 idempotency-in-progressbeing createdsame_keyWait Retry-After: 1, repeat.
201, or 200 replaylivenull on the purchaseFollow next_action, or poll the purchase.
402 payment-failedfailednew_keyTry again with a new key, for example with payment: "checkout".
replay of a failed, expired or canceled purchaseterminalnew_keyStart again with a new key.
422 idempotency-key-reusedthe other request'snew_keyUse a new key for a different request.

Both SDKs generate a key when you give none, reuse it on their own retries (network errors, 409 idempotency-in-progress, 429, 503; at most 3 attempts, honouring Retry-After), and attach it to every error they raise, so you can retry at your level with the same key.

Problems

Errors are RFC 7807 application/problem+json. Match a problem by the last path segment of type (insufficient-credits, payment-failed), never by host: older problems use https://api.noncelogic.com/errors/... and billing problems use https://api.roveapi.com/errors/.... Status 402 has two types, insufficient-credits (your balance is too low) and payment-failed (a purchase's payment failed).

TypeStatusMembersretry
idempotency-key-required400none
invalid-params400field errorsnone
invalid-cursor400none
purchase-not-permitted403reason: agent_purchasing_off or key_not_granted, with owner_action_urlnone
purchase-not-permitted403reason: no_saved_method or off_session_not_allowed (only for payment: "saved_method"), suggested_payment: "auto"same_key
spend-limit-exceeded403limit (per_purchase, key_period, account_period), limit_cents, committed_cents, requested_cents, resets_at, packs_within_limits, owner_action_urlnone
test-key-required403none
pack-not-found404item_idnone
purchase-not-found404none
payment-failed402failure_code, purchase_idnew_key
purchase-not-cancelable409purchase_statusnone
invalid-transition409purchase_statusnone
idempotency-in-progress409Retry-Aftersame_key
idempotency-key-reused422new_key
purchase-rate-limited429Retry-Aftersame_key
billing-unavailable503Retry-After, purchase_id when a purchase was keptsame_key

Problems on a purchase request also carry idempotency_key. The full list, including the owner routes, is in the Error Reference.

Out of credits

When credits run out an agent meets one of two answers. Both keep every member they always had and add what the agent needs to act:

  • 402 insufficient-credits on session create, /v1/browser/action, /v1/browser/screenshot, /v1/browser/extract and a supervised start.
  • 410 session-expired with reason: "credit_exhausted" on /v1/browser/action and GET /v1/browser/session/{session_id}, when a supervised session ended because the account ran out. The browser profile is kept; after adding credits, create a new session.
MemberMeaning
credits_remaining, credits_remaining_millicreditsThe balance, whole and exact
required_millicredits402 only: what the refused operation needed
packs_within_limitsPack ids this key may buy now within every limit; empty when it may not buy
purchasingallowed, and reason (agent_purchasing_off, key_not_granted) when not
next_stepbuy_credits: buy a pack from packs_within_limits. ask_owner: give a person owner_action_url
linksbalance, purchase, catalogue and docs
owner_action_urlThe billing page where the owner adds credits
retryAlways none: the same request fails until credits are added

next_step is buy_credits only when the key may buy and at least one pack fits every limit. The extensions are left out when the balance cannot be read, so treat them as optional. Test keys never run out of credits, so they never get these answers; rehearse with the bodies below. They are the fixtures the Node and Python SDK tests use.

A key that may buy:

{
  "type": "https://api.noncelogic.com/errors/insufficient-credits",
  "title": "Insufficient credits",
  "status": 402,
  "detail": "Not enough credits for requested operation.",
  "instance": "/v1/browser/action",
  "credits_remaining": 0,
  "credits_remaining_millicredits": 250,
  "required_millicredits": 1000,
  "packs_within_limits": ["pack_1k", "pack_5k"],
  "purchasing": { "allowed": true, "reason": null },
  "next_step": "buy_credits",
  "links": {
    "balance": "/v1/account/balance",
    "purchase": "/v1/billing/purchases",
    "catalogue": "/v1/billing/catalogue",
    "docs": "https://roveapi.com/docs/billing-api"
  },
  "owner_action_url": "https://roveapi.com/dashboard/billing",
  "retry": "none"
}

A supervised session that ran out, on a key the owner has not allowed to buy:

{
  "type": "https://api.noncelogic.com/errors/session-expired",
  "title": "Session expired",
  "status": 410,
  "detail": "This supervised session ended because the account ran out of credits. Add credits, then create a new session; the browser profile is kept.",
  "instance": "/v1/browser/action",
  "reason": "credit_exhausted",
  "controller": "agent",
  "credits_remaining": 0,
  "credits_remaining_millicredits": 0,
  "packs_within_limits": [],
  "purchasing": { "allowed": false, "reason": "key_not_granted" },
  "next_step": "ask_owner",
  "links": {
    "balance": "/v1/account/balance",
    "purchase": "/v1/billing/purchases",
    "catalogue": "/v1/billing/catalogue",
    "docs": "https://roveapi.com/docs/billing-api"
  },
  "owner_action_url": "https://roveapi.com/dashboard/billing",
  "retry": "none"
}

When a purchase has next_action.type: "open_checkout_url", give next_action.url to a person; next_action.instruction is a sentence you can pass on as it is. The link is a Stripe Checkout page for the account's Stripe customer:

  • It expires 60 minutes after it was created (next_action.expires_at). Unpaid, the purchase becomes expired and nothing is charged.
  • It names the key that asked ("Requested by API key ci-runner ..."), so the person can see who wants to buy.
  • The owner is emailed the same link.
  • POST /v1/billing/purchases/{id}/cancel makes it stop working.

Have the person open the link in their own browser. Do not open it inside a Rove supervised session: the card would be typed into a browser Rove streams and the agent can read.

After paying, poll GET /v1/billing/purchases/{id} until it is paid. If a supervised session ended for lack of credits, create a new one; the browser profile is kept.

Test purchases

A test key (rvp_test_...) buys through the same rules as a live key: agent purchasing must be on, the key granted, and the pack within the limits. Nothing reaches Stripe, no card is charged and no credits are added. Test purchases have livemode: false and their own hourly rate window.

  • If the account has a saved card with charging allowed and payment is not checkout, the purchase starts processing, as an off-session charge would.
  • Otherwise it starts open with a next_action.url that points to this section, since there is no payment page to open.

Then drive it with POST /v1/billing/purchases/{id}/simulate and an outcome:

outcomeFromTo
paidopen, requires_action, processingpaid (no credits are added)
expiredopen, requires_actionexpired
failedprocessingfailed, failure_code: "card_declined"
authentication_requiredprocessingrequires_action, as when the bank asks the owner to confirm

Repeating an outcome the purchase already has returns it unchanged. Any other move is 409 invalid-transition with purchase_status. A live key gets 403 test-key-required.

SDK examples

Node (@roveapi/browser 1.3.0 or later). This example is type-checked against the SDK in CI:

import { BrowserClient, InsufficientCreditsError, PaymentFailedError, SpendLimitExceededError } from '@roveapi/browser'

const rove = new BrowserClient({ apiKey: process.env.ROVE_API_KEY! })

/** Buys the cheapest pack this key may buy, or tells a person what to do. */
async function topUp(): Promise<void> {
  const balance = await rove.balance()
  const itemId = balance.purchasing.packs_within_limits[0]
  if (!balance.purchasing.allowed || !itemId) {
    console.log(`Ask the account owner to add credits: ${balance.purchasing.owner_action_url}`)
    return
  }
  try {
    const purchase = await rove.purchaseCredits({ itemId })
    if (purchase.next_action) console.log(purchase.next_action.instruction, purchase.next_action.url)
    let current = await rove.getPurchase(purchase.id)
    while (current.status === 'open' || current.status === 'requires_action' || current.status === 'processing') {
      await new Promise((resolve) => setTimeout(resolve, 5_000))
      current = await rove.getPurchase(purchase.id)
    }
    console.log(`Purchase ${current.id} is ${current.status}`)
  } catch (err) {
    if (err instanceof SpendLimitExceededError) console.log(`Limit reached until ${err.resetsAt}; packs that fit: ${err.packsWithinLimits?.join(', ')}`)
    else if (err instanceof PaymentFailedError) console.log(`Declined (${err.failureCode}); retry with a new key and payment 'checkout'`)
    else throw err
  }
}

try {
  const session = await rove.session()
  await session.navigate('https://example.com')
} catch (err) {
  if (err instanceof InsufficientCreditsError && err.nextStep === 'buy_credits') await topUp()
  else if (err instanceof InsufficientCreditsError) console.log(`Ask the account owner to add credits: ${err.ownerActionUrl}`)
  else throw err
}

With a test key, finish a test purchase:

import { BrowserClient } from '@roveapi/browser'

const rove = new BrowserClient({ apiKey: process.env.ROVE_TEST_KEY! })
const purchase = await rove.purchaseCredits({ itemId: 'pack_1k', payment: 'checkout' })
const paid = await rove.simulatePurchase(purchase.id, 'paid')
console.log(paid.status, paid.livemode)

Python (roveapi 1.3.0 or later), type-checked with mypy in CI:

import os
import time

from roveapi import BrowserClient, InsufficientCreditsError, PaymentFailedError, SpendLimitExceededError

rove = BrowserClient(api_key=os.environ["ROVE_API_KEY"])


def top_up() -> None:
    """Buys the cheapest pack this key may buy, or tells a person what to do."""
    purchasing = rove.balance()["purchasing"]
    packs = purchasing["packs_within_limits"]
    if not purchasing["allowed"] or not packs:
        print("Ask the account owner to add credits:", purchasing["owner_action_url"])
        return
    try:
        purchase = rove.purchase_credits(packs[0])
    except SpendLimitExceededError as err:
        print("Limit reached until", err.resets_at, "; packs that fit:", err.packs_within_limits)
        return
    except PaymentFailedError as err:
        print("Declined", err.failure_code, "; retry with a new key and payment='checkout'")
        return
    if purchase["next_action"]:
        print(purchase["next_action"]["instruction"], purchase["next_action"]["url"])
    while purchase["status"] in ("open", "requires_action", "processing"):
        time.sleep(5)
        purchase = rove.get_purchase(purchase["id"])
    print("Purchase", purchase["id"], "is", purchase["status"])


try:
    session = rove.session()
    session.navigate("https://example.com")
except InsufficientCreditsError as err:
    if err.next_step == "buy_credits":
        top_up()
    else:
        print("Ask the account owner to add credits:", err.owner_action_url)
finally:
    rove.close()

MCP examples

The Rove MCP server (@roveapi/mcp 1.10.0 or later, and the hosted server) has seven credit tools; see MCP Setup. These calls are checked against the tools' input schemas in CI.

See prices and whether this key may buy (never buys):

{ "name": "buy_credits", "arguments": {} }

Buy a pack. Without idempotency_key the server derives one, so the same call again in the same MCP session returns the first purchase for 10 to 20 minutes; the result carries the idempotency_key to reuse:

{ "name": "buy_credits", "arguments": { "item_id": "pack_1k", "payment": "auto" } }

Follow it, and with a test key, finish it:

{ "name": "get_purchase", "arguments": { "purchase_id": "0b9b5b8e-6a3b-4f4e-9d8c-2f1e4c3b2a10" } }
{ "name": "simulate_purchase", "arguments": { "purchase_id": "0b9b5b8e-6a3b-4f4e-9d8c-2f1e4c3b2a10", "outcome": "paid" } }

A tool that hits an out-of-credits answer returns it as a tool error carrying the members above and ending with the next step: "Call get_credit_balance, then buy_credits with an item_id from top_ups." or "Ask the account owner to add credits at ...".

Rove — the supervised browser for agents