Preview. Usage Billing v1 is in development, and demos run on sample data.Read the guide

API reference

The Waffo Usage Billing API manages customers, contracts, seats, balances, and invoices, and decides whether each model call may run. This page is generated from the contract, so the fields and errors below match it.

Draft contract, version 1.0.0-rc.1. Field names and error codes can still change before the contract freezes.

Base URL
https://api.example.invalid
Authentication

Send Authorization: Bearer sk_test_xxx. Tenant secret key, or a portal token from POST /v1/portal_sessions. The tenant comes from the credential.

Key scopes nest: admin includes read, and read includes ingest. A portal token is bound to one customer and can call only the endpoints marked as allowed. Anywhere else it gets 403 insufficient_scope, and another customer's resource is 404 portal_scope_violation.

Idempotency

Every POST, PATCH, and DELETE needs an Idempotency-Key header. Unique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict. Replaying a key with the same body returns the stored response.

  • POST /v1/authorize: request_id in the body, keyed on (tenant, customer_id, request_id). No Idempotency-Key header.
  • POST /v1/ingest: transaction_id per event, deduped on (tenant, transaction_id) for 34 days. No Idempotency-Key header.
Response envelope

Success is {data, meta}, and lists add meta.next_page. Every success is HTTP 200, including creates and idempotent replays. Optional fields are omitted when unset. The invoice PDF is the one response without an envelope.

Errors are {error} with a code, message, retryable flag, request id, and the offending field when one applies.

Money
No JSON number carries money. Usage and credit amounts are decimal strings with at most 9 places, paired with credit_type_id. USD amounts are integer *_cents fields. A float in a money field, including 1999.0, is 400 validation_failed.
Enums
All enums are extensible. Clients must tolerate unknown values.

Customers

List customers

GET/v1/customers

Scope
read
Portal tokens
Not allowed
Returns
A page of customers.
Parameters
NameTypeDescription
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
external_idstringin queryFilter by exact external_id.
statusenumin query

Values: activearchived

Response fields 7
Errors 5

Create a customer

POST/v1/customers

Scope
admin
Portal tokens
Not allowed
Returns
Customer created, or the stored response on a replay.
Parameters
NameTypeDescription
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 3
Response fields 7
Errors 7

Get a customer

GET/v1/customers/{id}

Scope
read
Portal tokens
Not allowed
Returns
The customer.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Response fields 7
Errors 5

Archive a customer

POST/v1/customers/{id}/archive

Scope
admin
Portal tokens
Not allowed
Returns
The archived customer.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Response fields 7
Errors 7

Get the aggregate state of a customer

GET/v1/customers/{id}/state

Scope
read
Portal tokens
Not allowed
Returns
Active contract, seats, balances, budgets, and whether the customer is blocked.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Response fields 8
Errors 5

Products

List products

GET/v1/products

Scope
read
Portal tokens
Not allowed
Returns
A page of products.
Parameters
NameTypeDescription
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
Response fields 6
Errors 5

Create a product

POST/v1/products

Scope
admin
Portal tokens
Not allowed
Returns
Product created, or the stored response on a replay.
Parameters
NameTypeDescription
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 4
Response fields 6
Errors 6

Credit types

Create a credit type

POST/v1/credit_types

Defines a custom pricing unit. The credit-to-USD rate lives on the contract, not here.

Scope
admin
Portal tokens
Not allowed
Returns
Credit type created, or the stored response on a replay.
Parameters
NameTypeDescription
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 2
Response fields 4
Errors 6

Rate cards

Create a rate card

POST/v1/rate_cards

Scope
admin
Portal tokens
Not allowed
Returns
Rate card created, or the stored response on a replay.
Parameters
NameTypeDescription
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 2
Response fields 4
Errors 6

List the rates of a rate card

GET/v1/rate_cards/{id}/rates

Scope
read
Portal tokens
Not allowed
Returns
A page of rates.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
product_idstringin query
attimestampin queryReturn rates in effect at this time.
Response fields 9
Errors 6

Add a rate to a rate card

POST/v1/rate_cards/{id}/rates

Rates are versioned by their time window. A new rate never rewrites history.

Scope
admin
Portal tokens
Not allowed
Returns
Rate added, or the stored response on a replay.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 7
Response fields 9
Errors 9

Contracts

Create a contract

POST/v1/contracts

Scope
admin
Portal tokens
Not allowed
Returns
Contract created, or the stored response on a replay.
Parameters
NameTypeDescription
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 20
Response fields 24
Errors 15

List the edit history of a contract

GET/v1/contracts/{id}/edits

Scope
read
Portal tokens
Not allowed
Returns
A page of contract edits, oldest first.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
Response fields 6
Errors 6

Append an edit to a contract

POST/v1/contracts/{id}/edits

Edits append a new version and never rewrite historic prices. A portal token may send only the fields listed under Portal tokens.

Scope
admin
Portal tokens
Allowed. May write only: starting_atupdate_prepaid_balance_threshold_configurationadd_budgetsupdate_budgets
Returns
Edit recorded, or the stored response on a replay.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 10
Response fields 6
Errors 20

Get a contract

GET/v1/contracts/{id}

Scope
read
Portal tokens
Not allowed
Returns
The contract at its latest version.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Response fields 24
Errors 5

List the contracts of a customer

GET/v1/customers/{id}/contracts

Scope
read
Portal tokens
Not allowed
Returns
A page of contracts.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
Response fields 24
Errors 6

End a contract

POST/v1/contracts/{id}/end

Scope
admin
Portal tokens
Not allowed
Returns
The contract with its new ending_before.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 1
Response fields 24
Errors 8

Archive a contract

POST/v1/contracts/{id}/archive

Scope
admin
Portal tokens
Not allowed
Returns
The archived contract.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Response fields 24
Errors 7

Seats

Add seats to a contract

POST/v1/contracts/{id}/seats

Convenience wrapper for POST /v1/contracts/{id}/edits seat_updates.add_seat_ids. Proration and credit grant follow the contract's subscription and recurring credit settings.

Scope
admin
Portal tokens
Not allowed
Returns
The added seats, or the stored response on a replay.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 3
Response fields 8
Errors 11

Change the tier of a seat

PATCH/v1/contracts/{id}/seats/{seat_id}

An upgrade adds the credit difference at once. A downgrade takes effect next period.

Scope
admin
Portal tokens
Not allowed
Returns
The updated seat.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
seat_idrequiredstringin pathTenant-side seat id, URL encoded.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 1
Response fields 8
Errors 9

Remove a seat

DELETE/v1/contracts/{id}/seats/{seat_id}

Follows the contract's seat_removal_timing. Removal voids the seat's unused credit, and a refund happens only when refund_on_seat_removal is true.

Scope
admin
Portal tokens
Not allowed
Returns
The seat, with status pending_removal or removed.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
seat_idrequiredstringin pathTenant-side seat id, URL encoded.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Response fields 8
Errors 8

Balances

List the balance of each seat

GET/v1/contracts/{id}/seat_balances

Scope
read
Portal tokens
Not allowed
Returns
A page of seat balances for the current period.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
seat_idstringin queryFilter to one seat.
Response fields 10
Errors 6

Get the net balance of a customer

GET/v1/customers/{id}/balance

Scope
read
Portal tokens
Allowed
Returns
Posted, reserved, and available amounts.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
include_seatsbooleanin queryAlso return per-seat balances.

Default false

Response fields 7
Errors 8

Commits

Create a prepaid commit

POST/v1/contracts/{id}/commits

Prepaid money lives in the customer's one wallet. A gated commit stays pending_payment until collection succeeds, and funds the wallet only then.

Scope
admin
Portal tokens
Not allowed
Returns
Commit created, or the stored response on a replay.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 10
Response fields 16
Errors 10

Create a free credit

POST/v1/contracts/{id}/credits

Scope
admin
Portal tokens
Not allowed
Returns
Credit created, or the stored response on a replay.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 7
Response fields 12
Errors 9

Record a manual entry on a commit

POST/v1/commits/{id}/manual_entries

Records an offline payment or a positive adjustment.

Scope
admin
Portal tokens
Not allowed
Returns
Entry recorded, or the stored response on a replay.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 3
Response fields 6
Errors 7

Usage

Authorize a call before it runs

POST/v1/authorize

Decides admission in one database transaction. Business denial is HTTP 200 with allowed false and a reason. Only auth, parameter, and server failures use an error envelope.

The request carries the hold inputs: customer_id, seat_id, request_id, and the estimate. When allowed, the response carries hold_id, hold_status, hold_expires_at, and the legs held. Pass hold_id to POST /v1/ingest to settle the hold. A hold not settled by hold_expires_at expires, and ingest arriving later is billed without it.

The drawdown order is seat bucket, pool buckets by priority, prepaid, then overage when the contract sets allow_overage. Member, pool, and customer hard limits count the non-seat legs only. The first reason that applies is returned.

Replay on (tenant, customer_id, request_id) returns the first decision with replayed true. Retrying after a denial therefore needs a new request_id.

Overshoot bound: settled spend on a hard limit L stays under L + Σ max(actual - estimate, 0) over holds admitted before L was reached.

Scope
ingest
Portal tokens
Not allowed
Idempotency
request_id in the body, keyed on (tenant, customer_id, request_id). No Idempotency-Key header.
Returns
Decision, including business denial.
Request body 7
Response fields 11
Errors 16

Ingest a batch of usage events

POST/v1/ingest

One transaction per event. Rating happens once. A named hold is settled, then the drawdown re-walks with the actual amount. Usage is never refused for lack of funds, because the work already happened. The prepaid leg is queued for the wallet after commit.

Per-event results, in request order:

  • accepted: stored, rated, and billed.
  • duplicate: same transaction_id and same body within 34 days. Not billed again.
  • conflict: same transaction_id as an event from an earlier batch with a different body. Not stored or billed, and the batch still returns 200.
  • rejected: failed a per-event check. Does not occupy the transaction_id.

The same transaction_id with different bodies inside one batch refuses the whole batch with 409 idempotency_conflict_in_batch and takes priority over everything else in the batch. The same transaction_id with the same body inside one batch is duplicate.

Late events inside the 34-day window whose period is closed are accepted and carried forward, with warning billed_via_carry_forward_adjustment and adjustment_invoice_id when known. The closed invoice never changes.

Scope
ingest
Portal tokens
Not allowed
Idempotency
transaction_id per event, deduped on (tenant, transaction_id) for 34 days. No Idempotency-Key header.
Returns
Per-event results. Conflicts and rejections are inside this body.
Request body 1
Response fields 1
Errors 15

Get grouped usage for a customer

GET/v1/customers/{id}/usage

Scope
read
Portal tokens
Allowed
Returns
A page of usage rows.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
starting_atrequiredtimestampin query
ending_beforerequiredtimestampin query
group_byarray of enumin queryGrouping dimensions.

Values: product_idseat_id

window_sizeenumin query

Values: HOURDAYNONE

Response fields 7
Errors 8

Alerts

List alerts

GET/v1/alerts

Scope
read
Portal tokens
Allowed
Returns
A page of alerts.
Parameters
NameTypeDescription
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
customer_idstringin queryFilter by customer. A portal token sees only its own customer.
Response fields 8
Errors 7

Create a threshold alert

POST/v1/alerts

Scope
admin
Portal tokens
Allowed
Returns
Alert created, or the stored response on a replay.
Parameters
NameTypeDescription
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 5
Response fields 8
Errors 9

Archive an alert

POST/v1/alerts/{id}/archive

Scope
admin
Portal tokens
Allowed
Returns
The archived alert.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Response fields 8
Errors 9

Invoices

List the invoices of a customer

GET/v1/customers/{id}/invoices

Scope
read
Portal tokens
Allowed
Returns
A page of invoices.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
statusenumin query

Values: draftfinalizedvoid

Response fields 15
Errors 8

Preview the draft invoice of a period

GET/v1/customers/{id}/invoices/preview

Scope
read
Portal tokens
Allowed
Returns
A live draft invoice. Previewing writes nothing.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
starting_attimestampin queryStart of the billing period. Defaults to the current period.
Response fields 15
Errors 9

Get an invoice

GET/v1/invoices/{id}

Scope
read
Portal tokens
Allowed
Returns
The invoice.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Response fields 15
Errors 7

Download an invoice as PDF

GET/v1/invoices/{id}/pdf

The one binary response in this API, so it has no JSON envelope. The request id is in the X-Request-Id header.

Scope
read
Portal tokens
Allowed
Returns
The PDF.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Errors 7

Void a finalized invoice

POST/v1/invoices/{id}/void

Only a finalized invoice can be voided. Void and collection race on the same invoice, and the first commit wins.

Voidable collection statuses: none, not_required, collection_queued (also cancels the queued request with no payment call), and failed (also cancels the payment).

Voiding a commit purchase invoice before payment cancels the commit.

Scope
admin
Portal tokens
Not allowed
Returns
The voided invoice.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Response fields 15
Errors 10

Issue a credit note on a finalized invoice

POST/v1/invoices/{id}/credit_notes

Finalized invoices never change, so a correction is a credit note. Collection and refunds stay with the payment provider or you.

Scope
admin
Portal tokens
Not allowed
Returns
Credit note issued, or the stored response on a replay.
Parameters
NameTypeDescription
idrequiredstringin pathId of the resource named in the path.
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 2
Response fields 6
Errors 9

Webhooks

Register a webhook endpoint

POST/v1/webhook_endpoints

Deliveries are at-least-once and HMAC signed. The signing secret is returned only in this response.

Scope
admin
Portal tokens
Not allowed
Returns
Endpoint created, or the stored response on a replay.
Parameters
NameTypeDescription
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 2
Response fields 5
Errors 6

List events

GET/v1/events

Pull view of the events sent to webhooks. Async failures with no synchronous error appear here.

Scope
read
Portal tokens
Not allowed
Returns
A page of events, newest first.
Parameters
NameTypeDescription
limitintegerin queryPage size.

Default 25Minimum 1Maximum 100

next_pagestringin queryOpaque cursor from meta.next_page.
typeenumin query

Values: balance.lowtopup.succeededtopup.failedtopup.limit_reachedbudget.threshold_reachedpool.quota_reachedmember.limit_reachedcommit.activatedcustomer.state_changed

customer_idstringin query
Response fields 6
Errors 7

Portal

Issue a portal token for a customer

POST/v1/portal_sessions

The tenant's server calls this and hands the short-lived token to the customer's browser. The tenant secret key is never exposed.

Scope
admin
Portal tokens
Not allowed
Returns
Token issued, or the stored response on a replay.
Parameters
NameTypeDescription
Idempotency-Keyrequiredstringin headerUnique per logical operation. Lives 48 hours. The same key with a different body is 409 idempotency_conflict.

Min length 1Max length 255

Request body 1
Response fields 4
Errors 7