---
title: "2026-07-25 · Legacy customer import"
description: "Legacy customer import (Phase 1) — mid-plan cutover for existing customers, with your period clocks, remaining credit seeds, and a quiet-by-default webhook story."
order: 7
date: "2026-07-25"
---

# Legacy customer import

Additive release. Nothing existing changes behaviour — but if you are migrating customers from another system, the recommended path has changed.

## Added — `POST /v1/imports/customers`

Mid-plan cutover for customers who already exist somewhere else. Previously the only route in was `POST /v1/subscriptions`, which restarts the billing period at *now* and fires the plan's full activation grants — so migrating a customer halfway through an annual plan both reset their renewal date and handed them a second year of credit.

Import takes your period clocks and seeds only what is actually left.

```bash
POST /v1/imports/customers
X-API-Key: qs_live_...
Idempotency-Key: import:user_abc
```

| Behaviour | `POST /v1/subscriptions` | `POST /v1/imports/customers` |
|---|---|---|
| Period clocks | Start at **now** | Your `current_period_start` / `current_period_end` |
| Activation grants | Always fire | Default `grant_policy: none` — no re-grant |
| Webhooks | `subscription.created`, `credit.granted`, … | Quiet by default; opt into `*.imported` only |
| Credits | Issued by plan grants | Seed **remaining** balance via `credit_blocks[]` |

Scope required: `subscriptions:write`. Returns `409` when a non-terminal subscription already exists or an `Idempotency-Key` is reused with a different body, and `422` on validation failure.

**Atomicity.** Import pre-validates, then writes. On failure it voids the seed and activation blocks and soft-deletes the subscription it created, so a retry with the same `Idempotency-Key` succeeds cleanly rather than leaving orphaned credit behind.

Run your whole export through `options.dry_run: true` first — identical validation, no writes.

See [Importing Existing Customers](/docs/cookbook/import-existing-customers) for the full cutover recipe.

## Added — credit source `migration`

`source` accepts `migration`, for credit blocks that represent a balance carried over from your previous system rather than a purchase, plan grant, or promotion. Amounts are millicredits, as everywhere else.

## Added — subscription `origin`

Subscriptions now carry `origin`, either `api` (created through the normal endpoints) or `import`. Use it to tell migrated customers apart from natively-created ones when reconciling.

## Added — `customer.imported` and `subscription.imported` webhooks

Both fire **only** when a request sets `options.emit_webhooks: true`. Import is silent by default, precisely so a large cutover does not flood your endpoint with thousands of `subscription.created` and `credit.granted` deliveries.

Import never emits `subscription.created` or `credit.granted`, regardless of this flag.

## Changed — guidance for migrations

Creating a subscription and then clawing back the over-granted credit was the previous workaround. It is no longer the recommended approach — use import, which never over-grants in the first place.
