Importing Existing Customers (Legacy Cutover)
How to move mid-plan customers, remaining wallets, trials, and packs into QuotaStack without double-granting or resetting billing periods.
LEGACY CUTOVER · MID-PLAN IMPORT
POST /v1/subscriptions restarts the period at "now" and fires full activation grants; import takes your period clocks and seeds only the credit that is actually left. Quick take
- Use
POST /v1/imports/customersfor mid-plan users — neverPOST /v1/subscriptions - Seed the remaining balance, not the original grant amount, via
credit_blocks[]withsource: migration - Defaults are deliberate:
grant_policy: none(no re-grant) andemit_webhooks: false(quiet cutover) - Run the whole export through
dry_run: truefirst — same validation, no writes
Importing Existing Customers (Legacy Cutover)
When you already have users in another system (homegrown ledger, Stripe + spreadsheet, etc.), do not call normal POST /v1/subscriptions for everyone. That path starts a new period at “now” and fires full plan activation credits.
Use the import API instead.
What import is for
| You have… | Import does… |
|---|---|
| Users mid monthly/annual plan | Creates subscription with your period start/end |
| Remaining credits / packs | Seeds remaining millicredits only |
| Free looks + paid packs | Multiple credit blocks with priority/expiry |
| Trials, cancel-at-period-end | Status + trial_ends_at / cancel_at_period_end |
| Wallet-only customers | Customer + credits, no subscription |
| Gauges (seats used) / custom entitlements | gauge_states / entitlement_overrides |
What import is not for
- Replaying full historical usage or invoices (keep that in your old system).
- Collecting or prorating money (your PSP).
- Open AI reservations (drain them before cutover).
- Auto-importing your plan catalog (create plans/metrics in QuotaStack first).
Prerequisites
- Catalog exists in the target environment (sandbox first): metrics, metering rules, plans, variants, entitlements.
- Map every legacy plan id → a QuotaStack
plan_variant_id. - Export remaining balances, not original grant amounts.
- Finish or release in-flight reservations so held credits are settled.
API
POST /v1/imports/customers
X-API-Key: qs_live_...
Idempotency-Key: import:{your_user_id}
Content-Type: application/json
Mid-plan annual example
Customer paid Pro Annual 2025-11-01 → 2026-11-01, 42 credits left (42,000 millicredits).
curl -s -X POST https://api.quotastack.io/v1/imports/customers \
-H "X-API-Key: $API_KEY" \
-H "Idempotency-Key: import:user_abc" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"external_id": "user_abc",
"display_name": "Ada"
},
"subscription": {
"plan_variant_id": "'"$PRO_ANNUAL_VARIANT"'",
"status": "active",
"started_at": "2025-11-01T00:00:00Z",
"current_period_start": "2025-11-01T00:00:00Z",
"current_period_end": "2026-11-01T00:00:00Z",
"grant_policy": "none"
},
"credit_blocks": [
{
"credits": 42000,
"source": "migration",
"reason": "Remaining balance at cutover",
"expires_at": "2026-11-01T00:00:00Z",
"idempotency_key": "migrate-block:user_abc:remaining"
}
],
"options": {
"emit_webhooks": false,
"dry_run": false
}
}'
Critical: "grant_policy": "none" (default). Do not use plan_activation unless you intentionally want a full new-plan grant.
Wallet-only (no subscription)
{
"customer": { "external_id": "user_xyz" },
"credit_blocks": [
{
"credits": 10000,
"source": "migration",
"reason": "Prepaid wallet remaining",
"idempotency_key": "migrate-block:user_xyz:wallet"
}
]
}
Free looks + pack stack (hybrid)
Model free value as credits, not a second plan, unless you need two different entitlement sets:
{
"customer": { "external_id": "user_pack" },
"subscription": {
"plan_variant_id": "VARIANT_FREE_OR_LABEL",
"status": "active",
"current_period_start": "2026-01-01T00:00:00Z",
"current_period_end": "2027-01-01T00:00:00Z",
"grant_policy": "none"
},
"credit_blocks": [
{
"credits": 3000,
"source": "promotional",
"reason": "Free looks remaining",
"priority": 0,
"idempotency_key": "migrate-block:user_pack:free"
},
{
"credits": 18000,
"source": "migration",
"reason": "Weekly pack remaining",
"priority": 10,
"expires_at": "2026-08-01T00:00:00Z",
"idempotency_key": "migrate-block:user_pack:weekly"
}
]
}
Burn-down uses priority then soonest expiry — packs burn before free looks when priority is higher.
Trial
"subscription": {
"plan_variant_id": "VARIANT_PRO",
"status": "trialing",
"trial_ends_at": "2026-08-15T00:00:00Z",
"current_period_start": "2026-07-15T00:00:00Z",
"current_period_end": "2026-08-15T00:00:00Z",
"grant_policy": "none"
}
Cancel at period end already requested
"subscription": {
"plan_variant_id": "VARIANT_PRO",
"status": "cancelling",
"cancel_at_period_end": true,
"current_period_start": "2026-07-01T00:00:00Z",
"current_period_end": "2026-08-01T00:00:00Z",
"grant_policy": "none"
}
Dry-run first
"options": { "dry_run": true }
Returns the same validation and projected shape without writing. Run your full export through dry-run before live.
Idempotency
| Key | Purpose |
|---|---|
Header Idempotency-Key: import:{user_id} | Whole request safe to retry (24h cache) |
credit_blocks[].idempotency_key | Survives beyond 24h — use forever-stable keys per logical block |
Re-running the same keys must not double balance.
Repair: missing credits only
If the customer already imported with a subscription, and you only forgot a pack:
{
"customer": { "external_id": "user_abc" },
"credit_blocks": [
{
"credits": 5000,
"source": "migration",
"reason": "Missed pack on first pass",
"idempotency_key": "migrate-block:user_abc:missed-pack"
}
]
}
Omit subscription. QuotaStack will not rewrite period clocks. Creating a second subscription while one is live returns 409.
Cutover checklist
- Build catalog in sandbox.
- Export legacy snapshot (remaining balances + period ends).
dry_run=trueover all rows; fix 422s.- Import sandbox; spot-check balance, period end,
entitlements/check,consume. - Drain reservations in production legacy system.
- Import live with the same script.
- Verify sample of live users.
- Flip app config to QuotaStack for metering/entitlements.
- Keep legacy ledger read-only for one cycle (disputes).
- At real period end: normal prepaid charge +
POST /v1/subscriptions/{id}/renew(or postpaid auto-advance).
Postpaid / overage
Invoice anything already over plan before cutover from your old system. After import, QuotaStack only records new usage and overage for the open period.
Common mistakes
| Mistake | Result |
|---|---|
Using POST /v1/subscriptions for mid-plan users | New period + full credit grant |
| Seeding original grant amount, not remaining | Over-credit |
| Omitting period end | 422 or wrong renewals |
| Cutover with open reservations | Balance/holds disagree with reality |
| Expecting import to charge the customer | QS never charges; you already did |
Related
Concepts used
Loading…