> ## Documentation Index
> Fetch the complete documentation index at: https://docs.integrateledger.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Adjust a customer's balance

> Add or remove a customer's credits by hand, with a reason.

Pass the exact `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 `credits` add a new grant. They never expire unless you set `expiresAt`, which must be after the customer's current time.
* Negative `credits` remove 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 returns `409 insufficient_credits` with the `availableCredits` you can remove.
* `reason` is required and appears on the customer's page and in billing history, with who made the change (`createdBy`).
* `idempotencyKey` is required, per customer. Retrying with the same key and the same body returns the first adjustment, with the `Idempotent-Replayed: true` header, and changes nothing. The same key with a different body returns `409 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 with `stripeInvoiceId`, or `stripeCheckoutSessionId` for a credit pack:

```json theme={"system"}
{
  "credits": 1000,
  "reason": "Invoice paid with tax; Ledger refused it",
  "idempotencyKey": "grant-in_1QxYzExample",
  "stripeInvoiceId": "in_1QxYzExample",
  "expiresAt": "2026-12-07T00:00:00Z"
}
```

Ledger asks Stripe that the payment exists in this environment (`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`.


## OpenAPI

````yaml openapi.json POST /v1/customers/balance-adjustments
openapi: 3.1.0
info:
  title: Ledger API
  version: 0.1.0
servers:
  - url: https://api.integrateledger.com
    description: Hosted Ledger API
security: []
paths:
  /v1/customers/balance-adjustments:
    post:
      tags:
        - Customers
      summary: Add or remove a customer's credits by hand
      description: >-
        Negative credits remove credits, never below a zero balance. Pass
        stripeInvoiceId (or stripeCheckoutSessionId) to grant a paid Stripe
        payment that Ledger didn't grant.
      operationId: adjustCustomerBalance
      parameters:
        - schema:
            type: string
            minLength: 1
            maxLength: 255
          required: true
          name: customerId
          in: query
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BalanceAdjustmentInput'
      responses:
        '200':
          description: >-
            Credits added or removed. A retry with the same idempotencyKey
            returns the first adjustment, with the Idempotent-Replayed: true
            header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceAdjustmentResult'
        '400':
          description: >-
            Invalid request, or expiresAt isn't after the customer's current
            time
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceAdjustmentError'
        '401':
          description: Missing or invalid Ledger API key, or a key without billing:write
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceAdjustmentError'
        '404':
          description: >-
            Customer not found, or Stripe has no such invoice or Checkout
            Session in this environment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceAdjustmentError'
        '409':
          description: >-
            The removal is more than the balance (insufficient_credits), the
            idempotencyKey was used for a different adjustment, or the Stripe
            payment is already granted, unpaid, or another customer's
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceAdjustmentError'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          description: >-
            A Stripe payment was linked, but Stripe isn't connected in this
            environment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceAdjustmentError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          description: Stripe didn't answer while Ledger checked the payment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceAdjustmentError'
      security:
        - ApiKey: []
      x-codeSamples:
        - lang: python
          label: Python SDK
          source: >-
            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")
        - lang: bash
          label: cURL
          source: >-
            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"}'
        - lang: typescript
          label: TypeScript SDK
          source: >-
            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" } });
components:
  schemas:
    BalanceAdjustmentInput:
      type: object
      properties:
        credits:
          type: integer
          minimum: -1000000000
          maximum: 1000000000
          description: >-
            Credits to add (positive) or remove (negative). A removal never
            takes the balance below zero.
          example: 500
        reason:
          type: string
          minLength: 1
          maxLength: 500
          description: >-
            Why the balance changed, for your team. Shown on the customer page
            and in billing history.
          example: Goodwill credit for the Nov 6 outage
        idempotencyKey:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            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.
          example: goodwill-2026-11-06-user_123
        expiresAt:
          type: string
          format: date-time
          description: >-
            Added credits only: when they stop counting. Must be in the future.
            Omit for credits that never expire.
        stripeInvoiceId:
          type: string
          pattern: ^in_[A-Za-z0-9]{1,250}$
          description: >-
            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.
          example: in_1QxYzExample
        stripeCheckoutSessionId:
          type: string
          pattern: ^cs_[A-Za-z0-9_]{1,250}$
          description: >-
            Added credits only: the paid credit-pack Checkout Session these
            credits stand in for. Not with stripeInvoiceId.
      required:
        - credits
        - reason
        - idempotencyKey
      additionalProperties: false
    BalanceAdjustmentResult:
      type: object
      properties:
        customerId:
          type: string
        adjustment:
          $ref: '#/components/schemas/BalanceAdjustment'
        availableCredits:
          type: integer
          minimum: 0
          description: The customer's balance after the adjustment.
      required:
        - customerId
        - adjustment
        - availableCredits
    BalanceAdjustmentError:
      type: object
      properties:
        error:
          type: string
        availableCredits:
          type: integer
          minimum: 0
          description: 'With insufficient_credits: the most this adjustment can remove.'
      required:
        - error
    BalanceAdjustment:
      type: object
      properties:
        id:
          type: integer
          exclusiveMinimum: 0
        credits:
          type: integer
          description: 'Positive: credits added. Negative: credits removed.'
        reason:
          type: string
        expiresAt:
          type: string
          nullable: true
          format: date-time
          description: >-
            When added credits stop counting. Null: they never expire, or
            credits were removed.
        stripeInvoiceId:
          type: string
          nullable: true
        stripeCheckoutSessionId:
          type: string
          nullable: true
        createdBy:
          type: string
          description: >-
            Who made it: api_key:<key ID>, api_key:workspace or dashboard:<user
            ID>.
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - credits
        - reason
        - expiresAt
        - stripeInvoiceId
        - stripeCheckoutSessionId
        - createdBy
        - createdAt
    PayloadTooLargeError:
      type: object
      properties:
        error:
          type: string
          example: payload_too_large
      required:
        - error
    RateLimitError:
      type: object
      properties:
        error:
          type: string
          example: rate_limited
      required:
        - error
  responses:
    PayloadTooLarge:
      description: 'The request body is over the limit: 1 MiB, or 5 MiB for a webhook.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PayloadTooLargeError'
    RateLimited:
      description: >-
        Too many requests. Wait Retry-After seconds, then retry. See the rate
        limits guide.
      headers:
        Retry-After:
          description: Seconds until a request is allowed again
          schema:
            type: integer
        RateLimit-Limit:
          description: 'The burst: requests allowed at once'
          schema:
            type: integer
        RateLimit-Remaining:
          description: Requests left right now
          schema:
            type: integer
        RateLimit-Reset:
          description: Seconds until the full burst is back
          schema:
            type: integer
        RateLimit-Policy:
          description: >-
            The sustained rate per second and the burst, such as
            200;w=1;burst=1000
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.