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

# Import a Stripe subscription

> Move a customer's existing Stripe subscription onto a Ledger plan, with no charge.

Imports a subscription you already bill in Stripe onto a paid Ledger plan. Link the customer's Stripe customer first with [Link a Stripe customer](https://docs.integrateledger.com/api-reference/customers/stripe-customer.md). The plan's monthly price must equal the subscription's. The full walkthrough is [Migrate from Stripe Billing](https://docs.integrateledger.com/migrate-from-stripe.md).

Ledger moves the subscription's item to the plan's own Stripe Price, with no proration: nothing is charged now, and the renewal date and amount stay the same. It then grants the plan's credits for Stripe's current period and treats the subscription like one it sold: each paid renewal grants the next period's credits, and upgrades and cancellations work through Ledger. Send `startingBalance` to carry over the credits the customer has left this period; it defaults to the plan's monthly credits.

An active subscription, one set to cancel at the period end, and a trialing one can be imported; a trial becomes the plan's first period. Ledger refuses, without changing anything:

* `400 subscription_past_due`, `subscription_incomplete`, `subscription_ended`, `subscription_not_billable` or `latest_invoice_unpaid`: collect or void the open invoice in Stripe first, or don't import an ended subscription.
* `400 unsupported_subscription` with a `reason`: something Ledger can't bill yet: `multiple_items`, `quantity`, `price_mismatch`, `not_monthly`, `metered_price`, `send_invoice`, `discount`, `schedule`, `pending_update`, `pause_collection`, `billing_thresholds`, `pending_invoice_items` or `customer_balance`.
* `409 stripe_customer_not_linked` or `stripe_customer_mismatch`: link the subscription's own Stripe customer first.
* `409 multiple_subscriptions`: the Stripe customer has another subscription that isn't canceled.
* `409 subscription_exists`: the customer already has a paid plan or a pending Checkout. An active free plan is replaced by the import.

Ledger checks all of this before it changes Stripe. If something changes in Ledger while it switches the Price (a Checkout starts for the same customer, say), the refusal has a `message`: the subscription is already on the plan's Price, with nothing charged, but not imported. Fix what it names and import again.

Importing the same subscription onto the same plan again returns it with `imported: false` and changes nothing; onto another plan, it returns `409 subscription_already_imported`. Like attach, the import needs a healthy signed webhook endpoint (`502 stripe_not_configured` or `stripe_webhook_unhealthy`), because the credits for every later renewal arrive by webhook. It works in Live and doesn't need Ledger's live-charge approval, since it starts no charge. The key needs the `billing:write` permission.


## OpenAPI

````yaml openapi.json POST /v1/billing/import
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/import:
    post:
      tags:
        - Billing
      summary: Import an existing Stripe subscription
      description: >-
        Link the Stripe customer first. Add startingBalance to carry over the
        credits the customer has left this period.
      operationId: importSubscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportSubscriptionInput'
      responses:
        '200':
          description: The subscription is the customer's Ledger plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSubscriptionResponse'
        '400':
          description: >-
            Invalid request, a free plan, a startingBalance above the plan's
            credits, or a subscription Ledger can't bill: past_due, incomplete,
            ended or not billable, an unpaid latest invoice, or
            unsupported_subscription with a reason
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportError'
        '401':
          description: Invalid Ledger API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportError'
        '404':
          description: Customer, plan or Stripe subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportError'
        '409':
          description: >-
            The Stripe customer isn't linked or isn't the subscription's, it has
            another open subscription, the Ledger customer already has a plan,
            or the subscription was imported onto another plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportError'
        '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_import_failed),
            the Stripe connection is incomplete or its key is invalid
            (stripe_not_configured), or the signed webhook endpoint is unhealthy
            (stripe_webhook_unhealthy)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportError'
      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.import_subscription(customer_id="user_123",
            stripe_subscription_id="sub_123", plan_id="pro")
        - lang: bash
          label: cURL
          source: >-
            curl --request POST
            'https://api.integrateledger.com/v1/billing/import' \
              -H "Authorization: Bearer $LEDGER_API_KEY" \
              -H 'Content-Type: application/json' \
              --data '{"customerId":"user_123","stripeSubscriptionId":"sub_123","planId":"pro"}'
        - lang: typescript
          label: TypeScript SDK
          source: >-
            import ledger from "integrateledger";


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

            await client.importSubscription({ customerId: "user_123",
            stripeSubscriptionId: "sub_123", planId: "pro" });
components:
  schemas:
    ImportSubscriptionInput:
      type: object
      properties:
        customerId:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            The Ledger customer. Link its Stripe customer first with POST
            /v1/customers/stripe-customer.
        stripeSubscriptionId:
          type: string
          maxLength: 255
          description: The customer's existing subscription in your Stripe account.
        planId:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            The paid Ledger plan it becomes. Its monthly price must equal the
            subscription's.
        startingBalance:
          type: integer
          minimum: 0
          description: >-
            The plan credits the customer has left this period, to carry over
            their usage. Defaults to the plan's monthly credits.
      required:
        - customerId
        - stripeSubscriptionId
        - planId
      additionalProperties: false
    ImportSubscriptionResponse:
      type: object
      properties:
        subscriptionId:
          type: integer
        planId:
          type: string
        status:
          type: string
          enum:
            - active
        currentPeriodStart:
          type: string
          format: date-time
        currentPeriodEnd:
          type: string
          format: date-time
        cancelAtPeriodEnd:
          type: boolean
          description: >-
            The subscription ends at the period end in Stripe; Ledger ends the
            plan then.
        grantedCredits:
          type: integer
          minimum: 0
          description: Credits granted for the current period.
        imported:
          type: boolean
          description: False when the subscription had already been imported.
      required:
        - subscriptionId
        - planId
        - status
        - currentPeriodStart
        - currentPeriodEnd
        - cancelAtPeriodEnd
        - grantedCredits
        - imported
      additionalProperties: false
    ImportError:
      type: object
      properties:
        error:
          type: string
        reason:
          type: string
          description: >-
            For unsupported_subscription: what Ledger can't bill yet, such as
            discount or multiple_items.
        message:
          type: string
          description: >-
            Set when Ledger had already moved the subscription to the plan's
            Stripe Price (no charge) before refusing: what to fix before
            importing again.
      required:
        - error
    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.