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, their leftover 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 endpoint.
Which call do I want?
- You are moving a customer off your old system, mid-plan →
POST /v1/imports/customersThis call is the one to reach for at cutover.createSubscriptionstarts 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/customersSendoptions.dry_run: true. QuotaStack works out the whole result and writes nothing. - The customer is new, and starts a plan today →
createSubscription
Import a customer
/v1/imports/customers | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | One customer, plus the subscription, credit blocks, 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, subscription.imported |
This call moves one customer over from your old system, in a single step. QuotaStack makes the customer, the subscription, 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 returns 409.
| Field | Type | Meaning |
|---|---|---|
Idempotency-Keyrequired | string | 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 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 Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example |
| Field | Type | Meaning |
|---|---|---|
customerrequired | object | |
customer.external_idrequired | string | 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_policyoptional | string | 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.
|
subscriptionoptional | object | |
subscription.plan_variant_idrequired | string (uuid) | Which plan variant the customer was on. If you leave it out: The call fails if you sent a |
subscription.statusoptional | string | Where the subscription stood when you moved it. If you leave it out: The status is
|
subscription.current_period_startrequired | string (date-time) | 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_endrequired | string (date-time) | 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_policyoptional | string | Whether QuotaStack hands out plan credits as part of the import. If you leave it out: By default, it is
|
credit_blocksoptional | array | |
credit_blocks.creditsrequired | integer (int64)1–∞ | 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. |
credit_blocks.sourcerequired | string | Why these credits exist. Use If you leave it out: QuotaStack rejects the block. This field is required.
|
credit_blocks.idempotency_keyoptional | string | 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_overridesoptional | array | |
gauge_statesoptional | array | |
gauge_states.currentrequired | integer (int64)0–∞ | What the meter read at cutover, such as seats used up. If you leave it out: The entry fails. Send this on every gauge. |
optionsoptional | object | |
options.emit_webhooksoptional | boolean | Whether QuotaStack tells your endpoint about this import. Turning it on sends If you leave it out: QuotaStack sends nothing. That is the opposite of the rest of this API, and it is on purpose. |
options.dry_runoptional | boolean | Whether to work the import out and throw the result away. If you leave it out: QuotaStack does the import for real. |
{
"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
}
} {
"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
} | Field | Type | Meaning |
|---|---|---|
customerrequired | object | |
subscriptionoptional | unknown | |
credit_blocksrequired | array | |
credit_blocks.original_amountrequired | integer (int64) | How many credits this block started with. Unit: millicredits, where 1 credit is 1000 millicredits. |
credit_blocks.remaining_amountrequired | integer (int64) | How many credits are left in this block. Unit: millicredits, where 1 credit is 1000 millicredits. |
credits_granted_totalrequired | integer (int64) | How many credits the import gave out, over all the blocks. Unit: millicredits, where 1 credit is 1000 millicredits. |
entitlement_overridesoptional | array | |
gauge_statesoptional | array | |
dry_runrequired | boolean | Whether this was a rehearsal. |
idempotent_replayoptional | boolean | Whether QuotaStack had seen this |
Errors
400— QuotaStack could not read the body at all. bad-request401— Missing or invalid authentication.409— This customer already has a subscription that has not ended. QuotaStack imports one subscription per customer today. You get the same status when you reuse anIdempotency-Keywith a different body. conflict422— A field is wrong, and the reply names it. Astatusoutside active, trialing, paused, and cancelling lands here. So doesgrant_policy: plan_prorated, which QuotaStack does not support yet. validation-error429— Rate limit exceeded.500— Unexpected server error.
Loading…