> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.paymentkit.com/guides/billing/credit-notes-credit-balance/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paymentkit.com/_mcp/server. # Credit notes & credit balance A customer's **credit balance** is money you owe them, held against their account. PaymentKit applies it automatically when their next invoice is collected. Each adjustment to the balance is recorded as a **credit note** — an immutable ledger entry — so the balance is always the running sum of every note. Balances are tracked **per currency**. A customer can hold credit in USD and EUR independently. # Credit balance vs. credit notes | Concept | What it is | | ------------------ | ----------------------------------------------------------------------------- | | **Credit note** | A single ledger entry: credit `issued`, `applied` to an invoice, or `voided`. | | **Credit balance** | The sum of all credit notes for a customer in a given currency. | You can work at either level: * **Raise a balance to an absolute target** with `PATCH credit-balance` — PaymentKit computes the difference against the current balance and posts it to the ledger. * **Lower a balance** with `POST credit-balance/reduce` — you state how much to take off rather than what the balance should end up as. This is the endpoint to use for reductions. * **Append a specific credit note** with `POST credit-notes` — useful when you want a reason and memo on record, or to link the credit to a specific invoice. This endpoint only ever *adds* credit; it cannot reduce a balance. # Set a customer's credit balance `PATCH credit-balance` sets the balance to an **absolute target**, not a delta. PaymentKit compares your target against the current balance and issues credit (if higher) or voids credit (if lower) to match. > **Warning** > > **Deprecated for reductions — use `POST credit-balance/reduce` instead.** An absolute target is > worked out against a balance you read earlier, so anything that moves the ledger in between — an > invoice finalizing and applying credit, a grant landing from elsewhere — is absorbed into the > difference and written as a real change. `PATCH` is also not covered by the idempotency > middleware at all (it intercepts `POST` and `PUT` only), so a retry after a timeout re-derives > the difference against whatever the balance is by then. > > This endpoint is not going away, and it still works for raising a balance. #### Dashboard 1. Open the **Customer** 2. Open the actions menu and choose **Change invoice balance** 3. Pick the currency, choose **Credit** or **Debit**, and enter the amount 4. Click **Apply balance adjustment** The amount comes pre-filled with the customer's current balance. What you enter replaces it. #### API ```bash curl -X PATCH https://app.paymentkit.com/api/{account_id}/customers/{customer_id}/credit-balance \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "amount_atom": 5000, "currency": "USD" }' ``` | Field | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `amount_atom` | `integer` | The **target** balance in the smallest currency unit (e.g. cents). A positive value is credit owed to the customer. A negative value is a debit: PaymentKit adds it to the amount due when the customer's next invoice is finalized. | | `currency` | `string` | Currency code (e.g. `USD`), accepted in any case. Each currency's balance is independent. | The response is the resulting balance. Currency codes come back lowercase: ```json { "amount_atom": 5000, "currency": "usd" } ``` > **Note** > > The target is compared with the customer's **ledger** balance — the same number returned by > the customer object. Expiring credit that has passed its `expires_at` but has not yet been > reclaimed still counts toward it until PaymentKit reclaims it; see > [Expiring credit](#expiring-credit). > > Lowering the balance draws from the customer's **unexpired** credit first — soonest-expiring > first, credit with no expiry last — and only then from credit already past its `expires_at` that > has not been reclaimed. So a reduction takes expiring credit before permanent credit. Read > [Expiring credit](#expiring-credit) before reducing the balance of a customer who holds any. > > `POST credit-balance/reduce` below is the better endpoint for taking credit away, and the same > draw order applies to it. # Reduce a customer's credit balance `POST credit-balance/reduce` removes a stated amount of credit and returns the ledger entry that records it. You say how much to take off, not what the balance should end up as, so the request means the same thing no matter what else moved the balance in the meantime. #### API ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/customers/{customer_id}/credit-balance/reduce \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: expiry-sweep-2026-06" \ -d '{ "amount_atom": 2500, "currency": "USD", "reason": "expiry_clawback", "memo": "Unused promo credit from the May campaign" }' ``` | Field | Type | Description | | ----------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `amount_atom` | `integer` | How much credit to remove, in the smallest currency unit. Must be greater than `0` — this endpoint only reduces. | | `currency` | `string` | Currency of the balance to reduce (e.g. `USD`). Only the named currency is touched. | | `reason` | `string` | One of `expiry_clawback`, `manual_adjustment`. | | `memo` | `string` | Optional human-readable note (max 500 chars). | | `expected_balance_atom` | `integer` | Optional precondition. The reduction is applied only if the balance in this currency still reads as exactly this amount; otherwise the request is refused with `409` and nothing is written. | The response is `201` with the credit note that was written — a `voided` entry with a **negative** `amount_atom` — plus `balance_after_atom`, the balance the write left behind, read in the same transaction so a sweep need not re-read: ```json { "id": "cn_live_1a2b3c4d5e6f7g8h", "customer_id": "cus_live_...", "invoice_id": null, "amount_atom": -2500, "currency": "usd", "type": "voided", "reason": "expiry_clawback", "memo": "Unused promo credit from the May campaign", "created_at": "2026-06-17T14:00:00Z", "updated_at": "2026-06-17T14:00:00Z", "balance_after_atom": 7500 } ``` Store that `cn_` id to correlate the reduction later. No companion invoice is created — reclaiming unused credit is a ledger correction, not a document the customer receives — so `invoice_id` is `null`. ## It refuses rather than clamps A reduction larger than the balance is rejected with `409 credit_balance_insufficient`, not trimmed 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. > **Warning** > > Both that check and `expected_balance_atom` are evaluated against the balance as read while the > request is handled. Nothing here takes a lock and the balance is a sum over the ledger rather > than a stored column, so a renewal finalizing for the same customer at that instant can still > take the balance negative. Read these as true of the balance at the moment it was read, not as a > serialized guarantee. ## Errors The two business refusals and the validation `422` use the [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) field set (served as `application/json`): the human-readable text is `detail`, and the machine-readable fields — `error_code` and the balance figures — sit **at the top level** of the body next to it, the same shape as `collection_in_progress` on invoices. The one exception is the in-flight duplicate `409` below, which is a bare `{"detail": "..."}`. | Status | `error_code` | What happened | | ------ | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `409` | `credit_balance_precondition_failed` | `expected_balance_atom` no longer matches. The body also carries `current_balance_atom`, `expected_balance_atom` and `currency`. Nothing was written — decide again from the current figure and retry with a **new** `Idempotency-Key`. | | `409` | `credit_balance_insufficient` | The reduction would take the balance below zero. The body carries `current_balance_atom`, `requested_amount_atom` and `currency`; nothing was written. | | `409` | *none* | A request with the same `Idempotency-Key` is still in flight (`detail` is a plain string). The original may yet succeed, so do **not** retry with a new key; poll `GET credit-notes` instead. | | `404` | *none* | No customer with this id in this account. | | `422` | *none* | The request itself was malformed — including an `Idempotency-Key` shorter than 8 characters. `detail` is the string `Validation failed` and the field errors sit in a top-level `errors` array. | ```json { "type": "about:blank", "title": "Credit Balance Insufficient", "status": 409, "detail": "Cannot reduce a credit balance below zero. The customer holds 1000 and you asked to remove 2500.", "instance": "https://app.paymentkit.com/api/acc_live_.../customers/cus_live_.../credit-balance/reduce", "request_id": "be_prod_...", "error_code": "credit_balance_insufficient", "current_balance_atom": 1000, "requested_amount_atom": 2500, "currency": "usd" } ``` Three conditions share `409` and their remedies differ, so 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 with the `409` above, 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 the note 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, though an early retry sees the in-flight `409` until that five-minute claim lapses. Replay is guaranteed for 24 hours; don't 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 credit-notes` when in doubt. > **Note** > > **Keys are scoped to the customer on this endpoint.** The replay scope is (key, account, > customer), so the same key sent for two different customers is two reductions, one each — a > batch expiry sweep can key by run id. This is specific to `credit-balance/reduce`: on > `POST credit-notes` the customer id is *not* part of the scope, and a key reused across > customers replays the first customer's note for the second, so key per customer there. If a response is ambiguous and you sent no idempotency key, don't blind-retry. Read `GET credit-notes` — the ledger, not the customer's cached balance — to see whether the entry landed. # Issue a credit note `POST credit-notes` appends a credit to the customer's balance with a reason and optional memo. > **Warning** > > **This endpoint cannot reduce a balance — use `POST credit-balance/reduce` for that.** > > `amount_atom` is validated as greater than zero, so a negative amount is rejected with `422` > rather than posting a debit. There is no flag or reason that changes this: issuing and reducing > are separate endpoints on purpose, so a reduction gets the idempotency, the balance precondition > and the overdraw refusal that this one does not have. #### API ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/customers/{customer_id}/credit-notes \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: credit-overcharge-918" \ -d '{ "amount_atom": 2500, "currency": "USD", "reason": "manual_adjustment", "memo": "Goodwill credit for billing error" }' ``` | Field | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `amount_atom` | `integer` | Credit amount in the smallest currency unit. Must be greater than `0`. | | `currency` | `string` | Currency code (e.g. `USD`). | | `reason` | `string` | One of `proration_excess`, `manual_adjustment`. Any other reason is rejected with `422`. | | `memo` | `string` | Optional human-readable note (max 500 chars). | | `invoice_id` | `string` | Optionally link the note to an existing invoice. The invoice must exist and belong to this customer, or the request is refused with a `404`. If omitted, a fully-paid companion invoice is created to back the credit. | | `expires_at` | `string` | Optional. An ISO 8601 timestamp **with a timezone offset**, in the future. From that instant the credit can no longer be applied to invoices, and PaymentKit reclaims whatever is still unused. Omit for credit that never expires — the default. See [Expiring credit](#expiring-credit). | > **Tip** > > Send an `Idempotency-Key` header so a retried request doesn't issue the credit twice. > **Note** > > Expiring credit has to be switched on for your environment. Two separate `422`s can come back, > and the order matters when you're reading the error: the timestamp is validated first, so a > missing timezone offset or a date in the past is refused for *that* reason whether or not the > feature is on. Only a well-formed future timestamp reaches the check that reports the feature > is not enabled yet. ## The credit note object ```json { "id": "cn_live_1a2b3c4d5e6f7a8b", "customer_id": "cus_live_...", "invoice_id": "in_live_...", "amount_atom": 2500, "currency": "USD", "type": "issued", "reason": "manual_adjustment", "memo": "Goodwill credit for billing error", "created_at": "2026-06-17T14:00:00Z", "updated_at": "2026-06-17T14:00:00Z", "expires_at": null, "expired_at": null, "remaining_atom": null } ``` | Field | Description | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Credit note identifier, prefixed `cn_`. | | `expires_at` | When this credit stops being spendable. `null` for credit that never expires. | | `expired_at` | When PaymentKit closed this credit out after its `expires_at` passed — whether or not there was anything left to reclaim. `null` until then, and always `null` for credit with no `expires_at`. | | `remaining_atom` | How much of this credit is still **unused**. Not the same as spendable: between an `expires_at` passing and PaymentKit reclaiming the credit, this still reports a value the customer can no longer apply. It becomes `0` once PaymentKit closes the note out, whatever the reclaim was able to take. Reported only on entries that *add* credit and whose consumption PaymentKit has tracked since they were issued — every note with an `expires_at`, and every note issued after the customer first held expiring credit **in that currency**. It is `null` on everything else: `applied` and `voided` entries, `debit_settlement` entries (debt bookkeeping, not credit the customer can spend), and credit issued before PaymentKit began tracking for that customer and currency. | | `type` | `issued` (credit added), `applied` (consumed by an invoice), or `voided` (the balance was reduced). | | `amount_atom` | Positive when credit is issued; negative when applied or voided. | | `invoice_id` | The invoice the note is linked to, if any. | | `reason` | Why the note exists. Not every reason a read can return is one you can set — see the table below. Treat this field as an open set; new values may be added. | Reasons split three ways: some you set when **issuing** credit, some you set when **reducing** a balance, and some PaymentKit writes for itself. | Reason | Set on | What it means | | ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `proration_excess` | `POST credit-notes` | Credit for a paid period the customer no longer gets. PaymentKit also writes it itself for the proration excess on a subscription downgrade. | | `manual_adjustment` | `POST credit-notes` and `POST credit-balance/reduce` | A merchant-driven adjustment. The only reason you can set in either direction. | | `expiry_clawback` | `POST credit-balance/reduce` only | Credit you granted, reclaimed because it went unused past its useful life — an expired referral bonus, a promotional credit pulled back. It describes credit being taken away, so it cannot be set when issuing a credit note. | | `auto_apply` | PaymentKit only | An invoice drew credit down from the balance. | | `debit_settlement` | PaymentKit only | A negative (debit) balance was settled onto an invoice. | | `void_reversal` | PaymentKit only | An invoice that had consumed balance was voided, so the ledger was compensated. | | `credit_expired` | PaymentKit only | An expiring grant reached its `expires_at` and PaymentKit reclaimed what was left. Distinct from `expiry_clawback`, which is you reclaiming credit by hand — so you can tell your own action apart from the platform's. | > **Note** > > `auto_apply`, `debit_settlement`, `void_reversal` and `credit_expired` are written by > PaymentKit for its own ledger bookkeeping, and `expiry_clawback` belongs to the reduce > endpoint. All five are rejected with `422` if you send them when creating a credit note. ## When an invoice is voided If an invoice that had credit applied to it is voided **because the amount was never genuinely owed** — a plan-change proration, or a merchant voiding an invoice raised in error — that credit returns to the customer's balance automatically as a `void_reversal` note. You don't need to issue a compensating credit note yourself in that case; doing so would double-credit the customer. Credit is **not** returned when the write-off ends a collection that was attempted and failed. An invoice that exhausted dunning keeps the credit consumed whether it ends up `void` or `uncollectible` — the customer owed that money and did not pay it, so the merchant absorbs the net rather than the gross. Which of the two labels dunning applies is your `invoice_status_on_failure` setting, and it affects your books only, never the customer's balance. The same holds for `uncollectible`: writing a debt off never returns the credit it consumed. If you then void that same invoice yourself, the credit does come back — once, from the void. If the invoice instead settled a negative (debit) balance, voiding it re-instates that debit as a `void_reversal` note with a **negative** amount, since the customer never paid it. So a `void_reversal` can move the balance in either direction — read its `amount_atom` rather than assuming credit was added. The returned credit can arrive as more than one note: * If the customer holds no expiring credit in that currency, it is a single `void_reversal` note with no `expires_at`. * Otherwise there is one note per expiring credit note the invoice drew on, each carrying that note's own `expires_at`, plus at most one further note with no `expires_at` for the remainder. Voiding never turns expiring credit into permanent credit, and if a date has already passed the restored credit is already expired — PaymentKit reclaims it on the next pass. However they are split, the notes always sum to exactly what the ledger says the invoice consumed. > **Note** > > Don't infer the balance change from the invoice's status. Check whether a `void_reversal` note > exists for the invoice, or read the customer's balance after the void. # List a customer's credit notes ```bash curl "https://app.paymentkit.com/api/{account_id}/customers/{customer_id}/credit-notes?currency=USD" \ -H "Authorization: Bearer sk_live_..." ``` Pass an optional `currency` filter; results are paginated and sorted newest-first. Two more filters help with expiring credit: | Query parameter | Effect | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unused_only=true` | Only entries that **added** credit and still have unused value: positive entries, whose consumption PaymentKit has tracked since they were issued (see `remaining_atom` in [Expiring credit](#expiring-credit)), that have not been closed out, and that have had less drawn from them than they were issued for. `applied` and `voided` entries never appear. Credit whose `expires_at` has passed but which PaymentKit has not yet reclaimed is still included. | | `expires_before=` | Only entries carrying an `expires_at` earlier than the timestamp. Entries with no `expires_at` are never returned, and credit that already expired **and was reclaimed** still is — pair with `unused_only=true` for what is still live. The timestamp must include a timezone offset, like `2026-10-01T00:00:00Z`; without one the request is rejected with a `422`. | Combine them to see what is about to be reclaimed: `?unused_only=true&expires_before=2026-10-01T00:00:00Z`. > **Note** > > `debit_settlement` entries are never returned by `unused_only=true`, and they report > `remaining_atom: null`. They are debt bookkeeping — a negative balance zeroed onto an invoice — > not credit the customer can spend. The two answers agree on purpose: no entry reports unused > value while being invisible under `unused_only=true`. # How credit is applied When an invoice is finalized, PaymentKit automatically draws down any available credit in the invoice's currency. Each application writes an `applied` credit note with a negative amount, and the balance falls accordingly. You don't need to apply credit manually — issuing it is enough. Credit issued *after* an invoice was finalized can still reach that invoice, but each invoice draws at most once. While nothing has been applied to it, a payment retry on an open or past-due invoice applies whatever balance is available at that point. Once credit has been applied or a debit settled, later retries leave that invoice alone and the credit stays on the customer's balance for the next one. An invoice only ever draws on credit that has **not** expired, and it takes the soonest-expiring credit first — so expiring credit is spent ahead of credit that never expires, and ahead of the reclaim. Credit whose `expires_at` has already passed is skipped even in the window before PaymentKit reclaims it, so what an invoice can draw is sometimes less than the balance below. See [Expiring credit](#expiring-credit). Only credit still on the balance can be drawn down, so a reduction posted before an invoice collects lowers what that invoice can take. A reduction never rewrites an application that already happened — it appends a `voided` note of its own, and invoices already collected are untouched. The current balances are also exposed on the customer object: ```json { "id": "cus_live_...", "credit_balances": [ { "amount_atom": 5000, "currency": "USD" }, { "amount_atom": 1200, "currency": "EUR" } ] } ``` > **Note** > > `credit_balances` lists every non-zero balance by currency and is the field to read. The legacy > singular `credit_balance` is retained for backward compatibility and reflects only the largest > balance. > > Both report the **ledger** balance, which still counts credit that has expired but has not been > reclaimed yet. An invoice cannot draw on that part. # Expiring credit Set `expires_at` when you issue a credit note and PaymentKit handles the rest: 1. **Invoice spending stops at the instant** `expires_at` passes. An invoice finalized one second later will not draw on that credit, even before it has been reclaimed. Credit issued without an `expires_at` is unaffected. 2. **The unused remainder is reclaimed** shortly after, by a sweep that runs every 15 minutes by default. Treat the reclaim as prompt, not as a deadline — what is guaranteed is that the credit stops being spendable at `expires_at`. The sweep sets `expired_at` on the original note, writes a `voided` credit note with reason `credit_expired` for whatever it reclaims, and emits a `credit_note.expired` event carrying `issued_atom`, `remaining_before_atom` (what the note still held), `clawed_back_atom` (what was actually taken), `expires_at`, `expired_at` and `currency`. 3. **The reclaim never takes more than the customer's balance**, and never takes credit that has not expired. So `clawed_back_atom` can be less than `remaining_before_atom` — including `0`, for a customer whose balance is at or below zero, or whose balance is only positive because of other credit that is still valid. The event still fires in those cases; reconcile on `clawed_back_atom`, not on `remaining_before_atom`. When the reclaim takes `0` no `credit_expired` note is written at all — only `expired_at` is set. 4. **A note that was fully used before its date** is closed out silently: `expired_at` is set, nothing is reclaimed, and no `credit_note.expired` event is emitted. 5. **`remaining_atom` tells you what is still unused** on each expiring credit note. It is not the same as spendable: between `expires_at` passing and the reclaim it still reports a value the customer can no longer apply, and it reads `0` once the note is closed out, whatever the reclaim was able to take. Everything above is per currency. A customer's balance, their expiring credit and each reclaim are tracked separately for each currency they hold credit in. > **Note** > > `credit_note.expired` is subscribable directly, as is the `credit_note.*` prefix. An endpoint > subscribed to all events (`*`) receives it too. See [Webhook events](#webhook-events). When an invoice that drew on expiring credit is voided, the credit comes back **with the same `expires_at`** it had. Voiding an invoice never turns expiring credit into permanent credit — and if that date has already passed, the restored credit is already expired: it cannot be applied, and PaymentKit closes it out on a following sweep, reclaiming whatever point 3 allows. > **Note** > > `expires_at` applies to credit issued **with** it. It cannot be added to a credit note after > the fact, and credit issued before this feature carries no `expires_at` and reports > `remaining_atom: null`. > > To put an expiry on credit a customer already holds, reduce the balance by that amount with > `POST credit-balance/reduce` and re-issue it with `POST credit-notes` and an `expires_at`. > Reduce with `reason: "expiry_clawback"`, which is what that reason is for, and send an > `Idempotency-Key` so a retry cannot take the credit twice. Use > `POST credit-balance/reduce` rather than `PATCH credit-balance` here: it takes the amount to > remove rather than the balance to end at, so the request means the same thing even if the > balance moved in between. > > **Do this only while the customer holds no unused expiring credit in that currency.** A > reduction is drawn from the customer's unexpired credit first — soonest-expiring first, credit > with no expiry last — and only then from credit already past its `expires_at` that has not been > reclaimed. So if they already hold expiring credit it is that grant the reduction consumes, not > the permanent credit you meant to convert, and you will have silently moved one expiry date > rather than added a new one. > > Check first with a list call that returns only unused credit carrying an expiry: > `?unused_only=true¤cy=USD&expires_before=2100-01-01T00:00:00Z`. `expires_before` matches > only entries that carry an `expires_at`, so a timestamp later than any date you issue returns all > of them, including credit already past its date that has not been reclaimed yet. If that comes > back empty, no expiry date is at risk and the recipe is safe. > **Tip** > > To try it on a sandbox account: pick a customer holding no other credit in that currency, issue a > credit with an `expires_at` a couple of minutes ahead, finalize an invoice after that time and > see it not apply, then check the customer's credit notes a little later for the `credit_expired` > entry. If `expires_at` is refused with a 422 reading "expires\_at is not enabled on this > environment yet", it has not been switched on for your environment. # Webhook events The subscribable webhook for credit activity is `customer.updated`. It fires whenever a customer's cached credit balance is refreshed: after credit is issued, applied, voided, reduced, or reclaimed at expiry. | Event | Trigger | | ------------------ | --------------------------------------------------------------------------------------- | | `customer.updated` | Customer details changed. A credit-balance refresh is one of the changes that fires it. | To track credit changes, subscribe to `customer.updated` and read `credit_balances` from the customer object on the API. The webhook payload is the stored customer row, so `data.object` carries the raw `credit_balance` cache (`{currency: amount_atom}`), not the `credit_balances` array. A `customer.updated` on its own does not mean the balance moved: the reclaim pass refreshes the cache for every credit note it closes out, including one that was already fully used. Compare the balance against what you last stored. > **Note** > > Credit-note ledger writes also emit `credit_note.created`. A credit note that reaches its > `expires_at` still holding unspent value emits `credit_note.expired`, whether or not there was > a balance left to reclaim; one that was already fully used is closed out silently (see > [Expiring credit](#expiring-credit)). Both events are individually subscribable, as is the > prefix `credit_note.*`. An endpoint subscribed to all events (`*`) receives them too. ## `credit_note.created` fires in both directions Every ledger write emits `credit_note.created` — including the writes that take credit *away*. Subscribe to it and you receive both directions on the same event name. It is also visible in the account's event log (`GET /api/{account_id}/events/`), and the same rule applies to what you read back from `GET credit-notes`: reducing a balance appends an entry just as issuing one does, so the event name alone never tells you which happened. Read the entry before assuming credit was granted. | Signal | Credit added | Credit removed | | ------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | `amount_atom` | Positive. | Negative. | | `type` | `issued`. | `voided` for a reduction or a reversal, `applied` when an invoice consumed the credit. | | `reason` | `proration_excess`, `manual_adjustment`, `debit_settlement`, `void_reversal`. | `expiry_clawback`, `manual_adjustment`, `auto_apply`, `void_reversal`. | The sign of `amount_atom` is the reliable signal. `reason` narrows it but does not settle it: `manual_adjustment` is written in both directions, and a `void_reversal` can go either way. Only `expiry_clawback` is exclusive to a reduction — the reduce endpoint is the only place that accepts it, and issuing a credit note with that reason is rejected. A reduction writes only the ledger entry. Unlike issuing a credit note with no `invoice_id`, it creates no companion invoice, so no `invoice.created` accompanies it. > Hold a credit balance for a customer and apply it automatically to future invoices.