> ## 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.

# Downgrade a paid plan

> Move a customer to a cheaper paid plan at the end of the period, or now.

Moves a customer with an active Stripe-backed monthly subscription to a **cheaper paid plan**. Send `customerId`, the target `planId` and a client-generated `Idempotency-Key` header. Moving to a free plan isn't a downgrade: cancel at period end instead.

`timing` says when the cheaper plan starts:

* **`period_end`** (the default): the customer keeps the current plan and its credits until the period ends. Ledger puts a Stripe subscription schedule on the subscription; the renewal bills the cheaper price, Ledger grants its credits and then releases the schedule. Overage from the ending period bills on that renewal at the old plan's rate. The response is `scheduled`, with `effectiveAt` set to the renewal.
* **`immediately`**: the cheaper plan starts now and the renewal date stays. The old plan's remaining credits end (`oldPlanCredits: "keep"` leaves them), and the new plan's credits are prorated to the rest of the period, rounded down. With `refund: "prorated"`, Ledger refunds (old price − new price) × the unused share of the period to the card, plus the tax charged on that amount, as a Stripe credit note on the invoice that paid for it; `refundAmountCents` (before tax) and `creditNoteId` report it. The old plan's unbilled overage bills at its own rate on the next renewal (`overageOnPlanChange: "carry_to_renewal"`) or on an invoice now (`"bill_now"`). The response is `applied`.

`refund` and `overageOnPlanChange: "bill_now"` only apply now: with `period_end` they return `400 refund_not_available` or `400 overage_option_not_available`. Options you leave out come from the workspace's defaults, then Ledger's.

One change at a time: a pending upgrade returns `409 upgrade_pending`, a scheduled cancellation `409 cancellation_scheduled`, and another downgrade `409 downgrade_scheduled`. While a downgrade is scheduled, upgrades and cancellation return `409 downgrade_scheduled`; [undo it](https://docs.integrateledger.com/api-reference/billing/cancel-downgrade.md) first. A target that isn't a cheaper, synced paid plan returns `400 ineligible_downgrade`.

Reuse the same idempotency key to retry: it returns the same downgrade and never changes Stripe or refunds twice. The same key with a different plan or option returns `409 idempotency_key_mismatch`. If a Stripe request fails (`502 stripe_downgrade_failed`), retry with the **same** key.


## OpenAPI

````yaml openapi.json POST /v1/billing/downgrade
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/billing/downgrade:
    post:
      tags:
        - Billing
      summary: Downgrade a paid plan
      operationId: downgradePaidPlan
      parameters:
        - schema:
            type: string
            minLength: 1
            maxLength: 255
          required: true
          name: idempotency-key
          in: header
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DowngradeInput'
      responses:
        '200':
          description: 'The downgrade: `scheduled` until the period ends, or `applied`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Downgrade'
        '400':
          description: >-
            Invalid or ineligible downgrade (ineligible_downgrade,
            refund_not_available, overage_option_not_available,
            invalid_idempotency_key)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  downgradeId:
                    type: integer
                required:
                  - error
        '401':
          description: Invalid Ledger API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  downgradeId:
                    type: integer
                required:
                  - error
        '404':
          description: Unknown customer or target plan
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  downgradeId:
                    type: integer
                required:
                  - error
        '409':
          description: >-
            Idempotency key reused with a different request, or another change
            pending (upgrade_pending, downgrade_scheduled,
            cancellation_scheduled, no_payment_to_refund)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  downgradeId:
                    type: integer
                required:
                  - error
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: >-
            Unexpected error in Ledger. It has been logged with the request ID;
            retry, or quote request_id when reporting it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InternalError'
        '502':
          description: >-
            Stripe unavailable or refused the request (stripe_downgrade_failed;
            retry with the same key), or Stripe not set up
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  downgradeId:
                    type: integer
                required:
                  - error
        '503':
          description: >-
            Live charges disabled, or the old plan's overage invoice still being
            created (overage_billing_pending; retry with the same key)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  downgradeId:
                    type: integer
                required:
                  - error
      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.downgrade_paid_plan(customer_id="user_123",
            plan_id="starter", timing="period_end",
            idempotency_key="downgrade_123")
        - lang: bash
          label: cURL
          source: >-
            curl --request POST
            'https://api.integrateledger.com/v1/billing/downgrade' \
              -H "Authorization: Bearer $LEDGER_API_KEY" \
              -H 'Idempotency-Key: downgrade_123' \
              -H 'Content-Type: application/json' \
              --data '{"customerId":"user_123","planId":"starter","timing":"period_end"}'
        - lang: typescript
          label: TypeScript SDK
          source: >-
            import ledger from "integrateledger";


            const client = new ledger({ apiKey: process.env.LEDGER_API_KEY ?? ""
            });

            await client.downgradePaidPlan({ idempotencyKey: "downgrade_123",
            body: { customerId: "user_123", planId: "starter", timing:
            "period_end" } });
components:
  schemas:
    DowngradeInput:
      type: object
      properties:
        customerId:
          type: string
          minLength: 1
          maxLength: 255
        planId:
          type: string
          minLength: 1
          maxLength: 255
        timing:
          type: string
          enum:
            - period_end
            - immediately
          description: >-
            `period_end` (default) switches at the next renewal; `immediately`
            switches now and keeps the billing period.
        refund:
          type: string
          enum:
            - none
            - prorated
          description: >-
            Immediate only. `prorated` refunds (old price − new price) × the
            unused share of the period as a Stripe credit note on the invoice
            that paid for it.
        oldPlanCredits:
          type: string
          enum:
            - keep
            - expire
          description: >-
            Immediate only. `expire` (default) ends the old plan's credits now;
            `keep` leaves them until their original expiry.
        overageOnPlanChange:
          type: string
          enum:
            - carry_to_renewal
            - bill_now
          description: >-
            Immediate only. When the old plan's unbilled overage is billed, at
            its own rate: on the next renewal (default) or on an invoice now.
      required:
        - customerId
        - planId
      additionalProperties: false
    Downgrade:
      type: object
      properties:
        downgradeId:
          type: integer
        status:
          type: string
          enum:
            - pending
            - scheduled
            - applied
            - closed
        fromPlanId:
          type: string
        planId:
          type: string
        timing:
          type: string
          enum:
            - period_end
            - immediately
        refund:
          type: string
          enum:
            - none
            - prorated
        oldPlanCredits:
          type: string
          enum:
            - keep
            - expire
        overageOnPlanChange:
          type: string
          enum:
            - carry_to_renewal
            - bill_now
        effectiveAt:
          type: string
          nullable: true
          format: date-time
        refundAmountCents:
          type: integer
          nullable: true
        creditNoteId:
          type: string
          nullable: true
      required:
        - downgradeId
        - status
        - fromPlanId
        - planId
        - timing
        - refund
        - oldPlanCredits
        - overageOnPlanChange
        - effectiveAt
        - refundAmountCents
        - creditNoteId
    InternalError:
      type: object
      properties:
        error:
          type: string
          example: internal_error
        request_id:
          type: string
          nullable: true
      required:
        - error
        - request_id
    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.