Billing
Read the plan, credit balance, and ledger history — so automation can check it can afford to run before it tries.
Billing over the API is read-only. There is a billing:read scope and no billing:write,
because nothing a token can do should be able to move money.
The reason to reach for these endpoints is the one that matters in CI: checking you can afford
a run before starting one, so a pipeline fails with a clear message instead of a
422 credits_exhausted halfway through.
Plans
GET /billing/plans · scope billing:read · no minimum role
Every plan available, not just yours.
{
"plans": [
{
"id": "…",
"name": "…",
"monthly_price_usd": 0,
"monthly_credits": 0,
"is_self_serve": true
}
]
}Prop
Type
Current status
GET /billing/status · scope billing:read · role viewer
The endpoint to poll before spending. Two shapes — check is_exempt first.
An exempt organization returns exactly this and nothing else:
{ "is_exempt": true }Everyone else:
Prop
Type
`is_exempt: true` means the other fields are absent, not zero
Demo and internal organizations return only { "is_exempt": true }. Code that reads
credit_balance_usd unconditionally will throw on them. Check is_exempt first and treat it
as "spending is not limited here".
Monthly credits are spent before top-up credits
monthly_balance_usd resets at current_period_end and does not carry over.
topup_balance_usd persists until spent. So a healthy-looking credit_balance_usd can still
be about to shrink at the period boundary — if you are scheduling work near it, read the two
components, not just the total.
Ledger history
GET /billing/history · scope billing:read · role admin
Every credit movement, for reconciliation and per-team cost attribution.
{
"history": [
{
"id": "01M2NP…",
"delta_usd": -5.0,
"balance_after_usd": 95.0,
"reason": "run_debit",
"source_type": "run",
"source_id": "01M2NP4K8QW3ZR7YT2XVB9CDEF",
"created_at": "2026-09-16T18:04:11.229104+00:00"
}
]
}Prop
Type
Because source_id carries the idempotency_key you chose, deriving that key from something
meaningful — a deploy id, a service name, a team prefix — turns this ledger into a cost
breakdown you can group without any extra bookkeeping.
Console-only billing endpoints
Anything that moves money is unreachable with a token, whatever scopes it carries. All return
403 api_token_route_not_allowed:
Prop
Type
Starting a pentest does spend credits and is available with runs:write — that is the
automation people actually need, and its cost is bounded and predictable. Changing what you owe
every month is not. See
what tokens cannot do.
Checking affordability before a run
BALANCE=$(curl -fsS https://api.vulnix.dev/billing/status \
-H "Authorization: Bearer $VULNIX_TOKEN" \
| jq 'if .is_exempt then 1e9 else .credit_balance_usd end')
ESTIMATE=$(curl -fsS "https://api.vulnix.dev/scopes/$SCOPE_ID" \
-H "Authorization: Bearer $VULNIX_TOKEN" | jq '.estimated_cost_usd')
if (( $(echo "$BALANCE < $ESTIMATE" | bc -l) )); then
echo "Not enough credits: have $BALANCE, need about $ESTIMATE" >&2
exit 1
fiestimated_cost_usd on the scope is an estimate, not the exact
charge — leave headroom rather than comparing to the cent.