---
title: "Imports"
description: "Move a customer off your old system in one call, keeping their billing clock and their leftover credits."
---

# Imports

Move customers off your old system.

An import brings a customer over from the system you are leaving. QuotaStack
makes the customer, their [subscription](/docs/concepts/subscriptions), their
leftover [credits](/docs/concepts/credits), and their overrides in one step.

Either all of it lands, or none of it does. A half-imported customer never
exists.

This call is not `createSubscription`. That one starts a plan today and grants
its credits today. An import keeps the dates the customer was already on, and
grants nothing unless you ask.

You supply the period. QuotaStack does not work it out from today. A customer
17 days into a monthly period stays 17 days in, and renews on the day they
always would have.

Send what the customer had left as `credit_blocks`. Give each block its own
`idempotency_key`, so a retry never doubles a balance.

Rehearse first. `options.dry_run: true` returns the whole result and writes
nothing.

An import is quiet on purpose. QuotaStack sends no `credit.granted` and no
`subscription.created`, so moving 10,000 customers cannot flood your
[webhook](/docs/concepts/webhooks) endpoint.

## Which call do I want?

- You are moving a customer off your old system, mid-plan → `POST /v1/imports/customers`
  This call is the one to reach for at cutover. `createSubscription` starts a plan today and grants its credits. An import keeps the clock and the balance the customer already had.
- You want to see what an import would do, before it does it → `POST /v1/imports/customers`
  Send `options.dry_run: true`. QuotaStack works out the whole result and writes nothing.
- The customer is new, and starts a plan today → `createSubscription`

1 of 1 operations documented in full.

## Import a customer

`POST /v1/imports/customers`

- **Idempotency-Key:** Required. Returns 422 without it.
- **Environment:** Sandbox and Live
- **Auth:** Tenant API key
- **Writes:** One customer, plus the [subscription](/docs/concepts/subscriptions), [credit blocks](/docs/concepts/credits), overrides, and gauge readings you sent. Almost nothing is announced. QuotaStack never sends `credit.granted` or `subscription.created` from an import, whatever you set. Both events are switched off in the code. So moving 10,000 customers cannot flood your endpoint. Set `options.emit_webhooks: true` and you get `customer.imported` and `subscription.imported`, and nothing else.
- **Fires:** [`customer.imported`](/docs/api/events/customer.imported), [`subscription.imported`](/docs/api/events/subscription.imported)

This call moves one customer over from your old system, in a single step. QuotaStack makes the customer, the [subscription](/docs/concepts/subscriptions), the leftover credits, the overrides, and the gauge readings together. Either all of it lands, or none of it does. You supply the dates. QuotaStack does not work the period out from today, the way `createSubscription` does. That is the point of this call: the customer keeps the clock they were already on. `grant_policy` is `none` unless you say otherwise, so an import hands out no plan credits. Send the credits the customer had left as `credit_blocks` instead. Send `options.dry_run: true` for a rehearsal. QuotaStack returns the whole result, marked `dry_run: true`, and writes nothing at all. A customer who already has a live [subscription](/docs/concepts/subscriptions) returns `409`.

### Header parameters

| Field | Type | Required | Meaning |
|---|---|---|---|
| `Idempotency-Key` | string | required | A unique string you choose, up to 256 characters. It stops a retry from doing the work twice. This header is required on every POST and PATCH. Leave it out and the request fails with `422`, before any change is made. Send the same key twice and QuotaStack replays the first response. The second call's status and body match the first, and the response carries `X-Idempotent-Replayed: true`. Replays are available for 24 hours. Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example `topup:{payment_intent_id}` or `renewal:{subscription_id}:{period_start}`. |

### Body parameters

| Field | Type | Required | Meaning |
|---|---|---|---|
| `customer` | object | required |  |
| `customer.external_id` | string | required | Your own ID for this customer. QuotaStack makes the customer when it has never seen this ID. If you leave it out: QuotaStack rejects the call. This field is required. |
| `customer.overage_policy` | string | optional | What you want to happen when this customer runs out. Overrides your account-wide setting when set. Leave it unset and the account-wide setting applies. If you leave it out: Nothing is stored. The account-wide setting applies either way. `block` `allow` `notify` `null` |
| `subscription` | object | optional |  |
| `subscription.plan_variant_id` | string (uuid) | required | Which [plan variant](/docs/concepts/plan-variant-entitlements) the customer was on. If you leave it out: The call fails if you sent a `subscription` block. |
| `subscription.status` | string | optional | Where the [subscription](/docs/concepts/subscriptions) stood when you moved it. If you leave it out: The status is `active`. `active` — The plan is running. `trialing` — Still in a free trial. `paused` — On hold, and not billing. `cancelling` — Ending when this period runs out. |
| `subscription.current_period_start` | string (date-time) | required | When the period the customer is in now began. You set this, and QuotaStack keeps it. If you leave it out: QuotaStack works one out. Send it, or the clock you kept is lost. |
| `subscription.current_period_end` | string (date-time) | required | When that period runs out. The first renewal QuotaStack runs falls on this date. If you leave it out: QuotaStack works one out from the plan. |
| `subscription.grant_policy` | string | optional | Whether QuotaStack hands out plan credits as part of the import. If you leave it out: By default, it is `none`. An import grants no plan credits at all. `none` — Grant nothing. Send what the customer had left as `credit_blocks`. `seed_only` — Grant only the blocks you sent, and none from the plan. `plan_activation` — Grant the full plan amount, as a fresh signup would get. |
| `credit_blocks` | array | optional |  |
| `credit_blocks.credits` | integer (int64) | required | How many credits the customer had left when you moved them. Unit: millicredits, where 1 credit is 1000 millicredits. If you leave it out: QuotaStack rejects the block. This field is required on every block you send. Range: 1–∞. |
| `credit_blocks.source` | string | required | Why these credits exist. Use `migration` for a straight carry-over. If you leave it out: QuotaStack rejects the block. This field is required. `migration` — You moved these from an old system. Use this one at cutover. `manual` — A person added these by hand. `promotional` — A free gift. `compensation` — You are making something right. `referral` — A customer sent a friend your way. |
| `credit_blocks.idempotency_key` | string | optional | Your own key for this one block. QuotaStack skips a block it has already seen under the same key, so a retried import never doubles a balance. If you leave it out: A retry of the same import can grant the block twice. Send one key per block. |
| `entitlement_overrides` | array | optional |  |
| `gauge_states` | array | optional |  |
| `gauge_states.current` | integer (int64) | required | What the meter read at cutover, such as seats used up. If you leave it out: The entry fails. Send this on every gauge. Range: 0–∞. |
| `options` | object | optional |  |
| `options.emit_webhooks` | boolean | optional | Whether QuotaStack tells your endpoint about this import. Turning it on sends `customer.imported` and `subscription.imported`, and nothing more. If you leave it out: QuotaStack sends nothing. That is the opposite of the rest of this API, and it is on purpose. `credit.granted` and `subscription.created` never fire here, even when you turn this on. |
| `options.dry_run` | boolean | optional | Whether to work the import out and throw the result away. If you leave it out: QuotaStack does the import for real. |

### Request

```json
{
  "customer": {
    "external_id": "user_42",
    "display_name": "Ada Lovelace"
  },
  "subscription": {
    "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
    "status": "active",
    "started_at": "2026-01-01T00:00:00Z",
    "current_period_start": "2026-07-01T00:00:00Z",
    "current_period_end": "2026-08-01T00:00:00Z",
    "grant_policy": "none"
  },
  "credit_blocks": [
    {
      "credits": 42000,
      "source": "migration",
      "reason": "Remaining balance at cutover",
      "expires_at": "2026-08-01T00:00:00Z",
      "idempotency_key": "migrate-block:user_42:remaining"
    }
  ],
  "options": {
    "emit_webhooks": false,
    "dry_run": false
  }
}
```

### Response 200

```json
{
  "customer": {
    "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
    "external_id": "user_42",
    "display_name": "Ada Lovelace",
    "environment": "live",
    "created_at": "2026-07-28T09:00:00Z"
  },
  "subscription": {
    "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
    "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
    "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
    "status": "active",
    "current_period_start": "2026-07-01T00:00:00Z",
    "current_period_end": "2026-08-01T00:00:00Z"
  },
  "credit_blocks": [
    {
      "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a31",
      "original_amount": 42000,
      "remaining_amount": 42000,
      "expires_at": "2026-08-01T00:00:00Z"
    }
  ],
  "credits_granted_total": 42000,
  "entitlement_overrides": [],
  "gauge_states": [],
  "dry_run": false,
  "idempotent_replay": false
}
```

### Response fields

| Field | Type | Required | Meaning |
|---|---|---|---|
| `customer` | object | required |  |
| `subscription` | unknown | optional |  |
| `credit_blocks` | array | required |  |
| `credit_blocks.original_amount` | integer (int64) | required | How many credits this block started with. Unit: millicredits, where 1 credit is 1000 millicredits. |
| `credit_blocks.remaining_amount` | integer (int64) | required | How many credits are left in this block. Unit: millicredits, where 1 credit is 1000 millicredits. |
| `credits_granted_total` | integer (int64) | required | How many credits the import gave out, over all the blocks. Unit: millicredits, where 1 credit is 1000 millicredits. |
| `entitlement_overrides` | array | optional |  |
| `gauge_states` | array | optional |  |
| `dry_run` | boolean | required | Whether this was a rehearsal. `true` means QuotaStack wrote nothing. |
| `idempotent_replay` | boolean | optional | Whether QuotaStack had seen this `Idempotency-Key` before. `true` means you are reading the first reply again, and this call changed nothing. |

### Errors

- `400` — QuotaStack could not read the body at all. See [bad-request](/docs/api/errors/bad-request).
- `401` — Missing or invalid authentication.
- `409` — This customer already has a [subscription](/docs/concepts/subscriptions) that has not ended. QuotaStack imports one subscription per customer today. You get the same status when you reuse an `Idempotency-Key` with a different body. See [conflict](/docs/api/errors/conflict).
- `422` — A field is wrong, and the reply names it. A `status` outside active, trialing, paused, and cancelling lands here. So does `grant_policy: plan_prorated`, which QuotaStack does not support yet. See [validation-error](/docs/api/errors/validation-error).
- `429` — Rate limit exceeded.
- `500` — Unexpected server error.

### See also

- [Legacy customer import](/docs/cookbook/import-existing-customers)
- [Subscriptions](/docs/api/subscriptions)
- [Credits](/docs/concepts/credits)
- [Webhooks](/docs/concepts/webhooks)
