Billing
Read the plan, pentests left and PR reviews used — so automation can check it can start a 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 have a
pentest left before starting one, so a pipeline fails with a clear message instead of a
422 credits_exhausted halfway through. (credits_exhausted is the error code for "no pentests
left on the plan", or "the plan is not active".)
Plans
GET /billing/plans · scope billing:read · no minimum role
Every plan available, not just yours.
{
"plans": [
{
"id": "startup",
"name": "Startup",
"monthly_price_usd": 99,
"is_self_serve": true,
"monthly_pentests": 3,
"monthly_pr_reviews": 50
}
]
}Prop
Type
Current status
GET /billing/status · scope billing:read · role viewer
The endpoint to poll before starting a run. 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
pentests.left unconditionally will throw on them. Check is_exempt first and treat it as
"starting a run is not limited here".
A plan's monthly pentests renew at current_period_end and do not carry over, so a healthy-looking
pentests.left can still be about to reset at the period boundary.
History
GET /billing/history · scope billing:read · role admin
Every movement of pentests, for reconciliation and per-team attribution.
{
"history": [
{
"id": "01M2NP…",
"delta_pentests": -1,
"pentests_after": 2,
"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 history into a 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 use one of your pentests 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 before a run
LEFT=$(curl -fsS https://api.vulnix.dev/billing/status \
-H "Authorization: Bearer $VULNIX_TOKEN" \
| jq 'if .is_exempt then 1000000 else .pentests.left end')
if [ "$LEFT" -lt 1 ]; then
echo "No pentests left on the plan" >&2
exit 1
fi