quotastack Docs
Docs / API / Imports

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-planPOST /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 itPOST /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 todaycreateSubscription
1 of 1 operations documented in full.

Import a customer

POST/v1/imports/customers
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne 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.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

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
FieldTypeMeaning
customerrequiredobject
customer.external_idrequiredstring

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_policyoptionalstring

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
subscriptionoptionalobject
subscription.plan_variant_idrequiredstring (uuid)

Which plan variant the customer was on.

If you leave it out: The call fails if you sent a subscription block.

subscription.statusoptionalstring

Where the subscription stood when you moved it.

If you leave it out: The status is active.

  • activeThe plan is running.
  • trialingStill in a free trial.
  • pausedOn hold, and not billing.
  • cancellingEnding when this period runs out.
subscription.current_period_startrequiredstring (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_endrequiredstring (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_policyoptionalstring

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.

  • noneGrant nothing. Send what the customer had left as credit_blocks.
  • seed_onlyGrant only the blocks you sent, and none from the plan.
  • plan_activationGrant the full plan amount, as a fresh signup would get.
credit_blocksoptionalarray
credit_blocks.creditsrequiredinteger (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.sourcerequiredstring

Why these credits exist. Use migration for a straight carry-over.

If you leave it out: QuotaStack rejects the block. This field is required.

  • migrationYou moved these from an old system. Use this one at cutover.
  • manualA person added these by hand.
  • promotionalA free gift.
  • compensationYou are making something right.
  • referralA customer sent a friend your way.
credit_blocks.idempotency_keyoptionalstring

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_overridesoptionalarray
gauge_statesoptionalarray
gauge_states.currentrequiredinteger (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.

optionsoptionalobject
options.emit_webhooksoptionalboolean

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_runoptionalboolean

Whether to work the import out and throw the result away.

If you leave it out: QuotaStack does the import for real.

Request
{
  "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
{
  "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
FieldTypeMeaning
customerrequiredobject
subscriptionoptionalunknown
credit_blocksrequiredarray
credit_blocks.original_amountrequiredinteger (int64)

How many credits this block started with.

Unit: millicredits, where 1 credit is 1000 millicredits.

credit_blocks.remaining_amountrequiredinteger (int64)

How many credits are left in this block.

Unit: millicredits, where 1 credit is 1000 millicredits.

credits_granted_totalrequiredinteger (int64)

How many credits the import gave out, over all the blocks.

Unit: millicredits, where 1 credit is 1000 millicredits.

entitlement_overridesoptionalarray
gauge_statesoptionalarray
dry_runrequiredboolean

Whether this was a rehearsal. true means QuotaStack wrote nothing.

idempotent_replayoptionalboolean

Whether QuotaStack had seen this Idempotency-Key before. true means you are reading the first reply again, and this call changed nothing.

Errors

  • 400QuotaStack could not read the body at all. bad-request
  • 401Missing or invalid authentication.
  • 409This customer already has a subscription 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. conflict
  • 422A 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. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.