Surfil

API · credits

Two endpoints cover the Credit lifecycle: POST /v1/meter/consume spends a Credit atomically, and GET /v1/ledger reads the history. A Credit is one server-signed value-output; execution is unmetered, the signed output is metered.

POST /v1/meter/consume

Auth: device token. Atomic, idempotent decrement. Consumption order is fixed: window → monthly → paid → granted.

  • 200 - signed decrement result.
  • 402 - insufficient credits; zero writes (the balance is untouched).
  • 503 - fail-closed: signing unavailable, so no decrement happens.

Every consume carries an idempotency key. A retry with the same key replays the stored result - a duplicated consume never double-charges. That is the guarantee behind safe client retries.

GET /v1/ledger

Auth: Supabase session. The caller's own credit ledger, newest first. It is not a receipt store - signed savings receipts are device-held (zero-trace).

{
  "transactions": [
    { "delta": -1,   "bucket": "monthly", "reason": "receipt",      "created_at": 1767225300 },
    { "delta": +500, "bucket": "granted", "reason": "credit.grant", "created_at": 1767139200 }
  ]
}

An empty ledger is honest, not an error - a new account genuinely has no transactions yet.

Read the concept behind this in Credits. Balances are surfaced in the Hub on Billing & Credits.