Reduce Customer Credit Balance
Remove a stated amount of credit from a customer’s balance and return the ledger entry recording it, plus balance_after_atom — the balance the write left behind, read in the same transaction.
Say how much to take off, not what the balance should end up as. A delta means the same thing no matter what else moved the balance in the meantime, so it is safe to send without first proving the balance is unchanged.
Refuses rather than clamps. A reduction larger than the balance is rejected with 409 credit_balance_insufficient, not clamped to zero. Balances here can legitimately go negative — a negative balance is a debit added to the customer’s next invoice — so reducing past zero would overcharge them. The check is made against the balance as read when the request is handled; it is not serialized against a renewal finalizing for the same customer at that instant, which is a pre-existing gap this endpoint does not change.
Optional precondition. Send expected_balance_atom with the balance you based the decision on and the reduction is refused with 409 credit_balance_precondition_failed if it no longer matches. The body carries current_balance_atom so you can decide again without another read.
Errors are an RFC 7807-shaped JSON body. Both refusals put error_code and the balance figures at the top level, next to title and detail — the same shape as collection_in_progress on invoices. Branch on error_code, not on the status.
Idempotency: send a stable Idempotency-Key header of at least 8 characters (shorter is rejected with 422). A duplicate that arrives while the first request is still in flight is refused (409, below) for up to five minutes. Once the first request has completed, its response is replayed for the same (key, account, endpoint) — and on this endpoint the endpoint half carries the customer id, see below — instead of running again; the replay record is written in the same transaction as the ledger entry, so once the reduction is committed a retry gets the stored result, never a second reduction. If the process dies before that commit nothing was written and a retry applies for the first time, which is what you want. Replay is guaranteed for 24 hours; do not rely on it beyond that, and never reuse a key for a genuinely new reduction — note that a refusal (4xx) is replayed too. Reconcile against GET /customers/{customer_id}/credit-notes when in doubt.
Keys are scoped to the customer on this endpoint. The replay scope is (key, account, customer): the same key sent for two different customers is two reductions, one each, so a batch expiry sweep can key by run id. Note this is specific to this endpoint — on POST /customers/{customer_id}/credit-notes the customer id is not part of the scope, and a key reused across customers replays the first one.
On an ambiguous response without an idempotency key, do not blind-retry. Read GET /customers/{customer_id}/credit-notes — the ledger, not the customer’s cached balance — to see whether the entry landed.
Authentication
Bearer authentication of the form Bearer <token>, where token is your auth token.
Path parameters
Headers
Request
How much credit to remove, in the smallest currency unit. Always positive — this endpoint only reduces. A request for more than the customer holds is rejected rather than clamped, judged against the balance as read while handling the request; see expected_balance_atom for what that check does and does not cover.
Currency of the balance to reduce. Balances are tracked per currency; this reduces only the named one.
Machine-readable reason, stored on the ledger entry. Use expiry_clawback when reclaiming credit that has gone unused past its useful life, manual_adjustment for anything else.
Optional human-readable note, shown to the merchant in the credit history. A good place for the grant or campaign this reclaims.
Optional optimistic-concurrency check. When set, the reduction is applied only if the customer’s balance in this currency still reads as exactly this amount; otherwise the request is rejected with 409 and nothing is written. Send the balance you based your decision on, so a reduction computed against a stale figure is refused rather than applied. This is a check at read time, not a lock: it closes the gap between when you read the balance and when you call, but a change landing during the call itself is not excluded.
Response
Positive = issued, negative = applied/voided
Machine-readable reason
The customer’s balance in this currency immediately after the reduction was written, read from the ledger in the same transaction. Saves a re-read when reconciling; like every balance here it is the figure at that moment, not a lock on it.
Human-readable note
When this credit stops being spendable. Null = never.
When PaymentKit reclaimed the unused remainder of this credit. Null = not (yet) expired. Only ever set on entries that carry an expires_at.
How much of this credit is still unused, in the smallest currency unit. Reported only for entries whose consumption has been tracked from the moment they were issued (every entry with an expires_at, and every entry issued after the customer first held expiring credit in that currency), excluding debit settlements. Null otherwise. Credit past its expires_at is no longer applicable even while this still reports a value; it becomes 0 once PaymentKit closes the entry out, whatever the reclaim was able to take.