curl --request POST 'https://api.integrateledger.com/v1/customers/balance-adjustments?customerId=user_123' \
-H "Authorization: Bearer $LEDGER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"credits":500,"reason":"Goodwill for the outage","idempotencyKey":"goodwill-user_123"}'import ledger from "integrateledger";
const client = new ledger({ apiKey: process.env.LEDGER_API_KEY ?? "" });
await client.adjustCustomerBalance({ customerId: "user_123", body: { credits: 500, reason: "Goodwill for the outage", idempotencyKey: "goodwill-user_123" } });import os
from integrateledger import ledger
client = ledger(api_key=os.environ["LEDGER_API_KEY"])
client.adjust_customer_balance(customer_id="user_123", credits=500, reason="Goodwill for the outage", idempotency_key="goodwill-user_123"){
"customerId": "<string>",
"adjustment": {
"id": 123,
"credits": 123,
"reason": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"stripeInvoiceId": "<string>",
"stripeCheckoutSessionId": "<string>",
"createdBy": "<string>",
"createdAt": "2023-11-07T05:31:56Z"
},
"availableCredits": 1
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "payload_too_large"
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "rate_limited"
}{
"error": "<string>",
"availableCredits": 1
}Adjust a customer's balance
Add or remove a customer’s credits by hand, with a reason.
curl --request POST 'https://api.integrateledger.com/v1/customers/balance-adjustments?customerId=user_123' \
-H "Authorization: Bearer $LEDGER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"credits":500,"reason":"Goodwill for the outage","idempotencyKey":"goodwill-user_123"}'import ledger from "integrateledger";
const client = new ledger({ apiKey: process.env.LEDGER_API_KEY ?? "" });
await client.adjustCustomerBalance({ customerId: "user_123", body: { credits: 500, reason: "Goodwill for the outage", idempotencyKey: "goodwill-user_123" } });import os
from integrateledger import ledger
client = ledger(api_key=os.environ["LEDGER_API_KEY"])
client.adjust_customer_balance(customer_id="user_123", credits=500, reason="Goodwill for the outage", idempotency_key="goodwill-user_123"){
"customerId": "<string>",
"adjustment": {
"id": 123,
"credits": 123,
"reason": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"stripeInvoiceId": "<string>",
"stripeCheckoutSessionId": "<string>",
"createdBy": "<string>",
"createdAt": "2023-11-07T05:31:56Z"
},
"availableCredits": 1
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "payload_too_large"
}{
"error": "<string>",
"availableCredits": 1
}{
"error": "rate_limited"
}{
"error": "<string>",
"availableCredits": 1
}customerId as a URL-encoded query value. Use an adjustment for credits outside a plan or credit pack: a goodwill credit, a support fix, or taking back credits after a refund. The key needs the billing:write permission.
- Positive
creditsadd a new grant. They never expire unless you setexpiresAt, which must be after the customer’s current time. - Negative
creditsremove credits from the customer’s grants in the order requests spend them (plan credits first, then the rest by expiry). An adjustment never takes the balance below zero: asking for more than the balance returns409 insufficient_creditswith theavailableCreditsyou can remove. reasonis required and appears on the customer’s page and in billing history, with who made the change (createdBy).idempotencyKeyis required, per customer. Retrying with the same key and the same body returns the first adjustment, with theIdempotent-Replayed: trueheader, and changes nothing. The same key with a different body returns409 idempotency_key_reused.
Grant a payment Ledger didn’t
If a customer paid and Ledger granted no credits (for example, an invoice with tax that Ledger refused), add the credits withstripeInvoiceId, or stripeCheckoutSessionId for a credit pack:
{
"credits": 1000,
"reason": "Invoice paid with tax; Ledger refused it",
"idempotencyKey": "grant-in_1QxYzExample",
"stripeInvoiceId": "in_1QxYzExample",
"expiresAt": "2026-12-07T00:00:00Z"
}
404 stripe_object_not_found otherwise), is paid (409 stripe_object_not_paid) and belongs to this customer (409 stripe_object_not_this_customers). The grant then counts as that payment’s, so the payment can’t be granted again (409 stripe_object_already_granted). Linking needs Stripe connected in this environment (422 stripe_not_ready); if Stripe doesn’t answer, the request returns 502 stripe_unavailable and you can retry with the same key.
Unknown customers return 404 customer_not_found.Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Query Parameters
1 - 255Body
Credits to add (positive) or remove (negative). A removal never takes the balance below zero.
-1000000000 <= x <= 1000000000500
Why the balance changed, for your team. Shown on the customer page and in billing history.
1 - 500"Goodwill credit for the Nov 6 outage"
Your unique key for this adjustment. Retrying with the same key and body returns the first adjustment; the same key with a different body is refused.
1 - 255"goodwill-2026-11-06-user_123"
Added credits only: when they stop counting. Must be in the future. Omit for credits that never expire.
Added credits only: the paid Stripe invoice these credits stand in for, when Ledger didn't grant it. Ledger checks it's paid and this customer's, and marks it granted.
^in_[A-Za-z0-9]{1,250}$"in_1QxYzExample"
Added credits only: the paid credit-pack Checkout Session these credits stand in for. Not with stripeInvoiceId.
^cs_[A-Za-z0-9_]{1,250}$Response
Credits added or removed. A retry with the same idempotencyKey returns the first adjustment, with the Idempotent-Replayed: true header.