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

# Attach a plan or add-on to a customer

> Assign a free plan immediately, or start Stripe Checkout for a paid plan or credit pack in the selected environment.

Pass `customerId` with exactly one of `planId` or `addOnId`. Use your application's stable customer ID, and create the customer first with [Create or get a customer](https://docs.integrateledger.com/api-reference/customers/create.md). Your API key selects Sandbox or Live. Paid live Checkout requires live activation and Ledger's live-charge gate; see [Sandbox and live environments](https://docs.integrateledger.com/environments.md).

Paid Checkout requires a stored Stripe connection with both a webhook signing secret and endpoint ID. Ledger checks that the endpoint is enabled in Stripe and matches the selected environment, URL, API version, and required events before creating a paid Checkout link. An incomplete setup returns `502 stripe_not_configured`; a missing, disabled, mismatched, or temporarily unverifiable endpoint returns `502 stripe_webhook_unhealthy`. Reconnect or verify the endpoint in the dashboard's Stripe settings, then retry. Free plans do not require Stripe.

```bash cURL theme={null}
curl --fail-with-body --request POST https://api.integrateledger.com/v1/billing/attach \
  --header "Authorization: Bearer $LEDGER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"customerId":"user_123","planId":"pro"}'
```

```python Python SDK source theme={null}
import os
from ledger_sdk import Ledger

ledger = Ledger(api_key=os.environ["LEDGER_API_KEY"])
checkout = ledger.attach(customer_id="user_123", plan_id="pro")
print(checkout["checkoutUrl"])
```

```typescript TypeScript SDK source theme={null}
import { Ledger } from "@ledger/sdk";

const ledger = new Ledger({ apiKey: process.env.LEDGER_API_KEY ?? "" });
const checkout = await ledger.attach({ customerId: "user_123", planId: "pro" });
if ("checkoutUrl" in checkout) console.log(checkout.checkoutUrl);
```

## Response and credit timing

A **free plan** returns `subscriptionId` and `status: "active"` immediately, without Stripe or a Checkout URL. The first monthly allowance is available at once. Repeating the same attach returns the same subscription without granting twice.

A **paid plan** returns `subscriptionId` and `checkoutUrl`. Ledger activates the plan and grants its allowance only after a verified paid Stripe invoice webhook. A **credit pack** returns `addOnPurchaseId` and `checkoutUrl`; Ledger grants its credits after a signed, paid Checkout completion webhook. A browser redirect alone does not grant credits.

An active free plan can start a paid plan Checkout. Its remaining allowance stays available while Checkout is open. After the verified paid invoice arrives, the remaining free plan allowance expires and the paid allowance begins. Previously purchased credit packs stay available. A failed or abandoned Checkout leaves the free plan active.

## Retrying Checkout

Retrying the same pending paid plan or credit pack returns its existing open Checkout URL. If that Stripe session has expired, Ledger closes the pending attempt and creates a new Checkout on the next attach. Returning to your `cancelUrl` does not itself expire the Stripe session; the open link remains resumable until Stripe expires it.

`409 subscription_exists` means this customer already has an incompatible active or pending plan. `409 purchase_pending` means a different credit pack Checkout is pending. See the [billing guide](https://docs.integrateledger.com/billing.md) for the complete lifecycle.


## OpenAPI

````yaml openapi.json POST /v1/billing/attach
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/attach:
    post:
      tags:
        - Billing
      summary: Attach a plan or add-on to a customer
      description: For a credit pack, send addOnId instead of planId.
      operationId: attach
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AttachInput'
      responses:
        '200':
          description: Pending Stripe Checkout for a paid item, or an active free plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachResponse'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachError'
        '401':
          description: Invalid Ledger API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachError'
        '404':
          description: Customer, plan, or add-on not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachError'
        '409':
          description: >-
            Customer already has a subscription, another Checkout is pending, or
            Checkout expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachError'
        '502':
          description: >-
            Stripe Checkout unavailable or signed webhook endpoint unhealthy;
            retry after resolving
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachError'
        '503':
          description: Live billing is not enabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachError'
      security:
        - ApiKey: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >-
            curl --request POST
            'https://api.integrateledger.com/v1/billing/attach' \
              -H "Authorization: Bearer $LEDGER_API_KEY" \
              -H 'Content-Type: application/json' \
              --data '{"customerId":"user_123","planId":"starter"}'
        - lang: python
          label: Python SDK (source only)
          source: |-
            import os
            from ledger_sdk import Ledger

            ledger = Ledger(api_key=os.environ["LEDGER_API_KEY"])
            ledger.attach(customer_id="user_123", plan_id="starter")
        - lang: typescript
          label: TypeScript SDK (source only)
          source: >-
            import { Ledger } from "@ledger/sdk";


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

            await ledger.attach({ customerId: "user_123", planId: "starter" });
components:
  schemas:
    AttachInput:
      anyOf:
        - $ref: '#/components/schemas/PlanAttachInput'
        - $ref: '#/components/schemas/AddOnAttachInput'
    AttachResponse:
      anyOf:
        - $ref: '#/components/schemas/PlanAttachResponse'
        - $ref: '#/components/schemas/AddOnAttachResponse'
        - $ref: '#/components/schemas/FreePlanAttachResponse'
    AttachError:
      type: object
      properties:
        error:
          type: string
      required:
        - error
    PlanAttachInput:
      type: object
      properties:
        customerId:
          type: string
          minLength: 1
          maxLength: 255
        planId:
          type: string
          minLength: 1
          maxLength: 255
      required:
        - customerId
        - planId
      additionalProperties: false
    AddOnAttachInput:
      type: object
      properties:
        customerId:
          type: string
          minLength: 1
          maxLength: 255
        addOnId:
          type: string
          minLength: 1
          maxLength: 255
      required:
        - customerId
        - addOnId
      additionalProperties: false
    PlanAttachResponse:
      type: object
      properties:
        checkoutUrl:
          type: string
          format: uri
        subscriptionId:
          type: integer
      required:
        - checkoutUrl
        - subscriptionId
      additionalProperties: false
    AddOnAttachResponse:
      type: object
      properties:
        checkoutUrl:
          type: string
          format: uri
        addOnPurchaseId:
          type: integer
      required:
        - checkoutUrl
        - addOnPurchaseId
      additionalProperties: false
    FreePlanAttachResponse:
      type: object
      properties:
        subscriptionId:
          type: integer
        status:
          type: string
          enum:
            - active
      required:
        - subscriptionId
        - status
      additionalProperties: false
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer

````

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