> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Importing subscriptions

> Adopt the subscriptions your gateway is already collecting on — and the four things the adoption refuses to guess.

Your customers have been paying somewhere for a while. Connecting the gateway
does not move them here on its own, and this page is why, and what to do about
it.

## Why they do not arrive by themselves

Every subscription this system holds is a **mirror** of one at your gateway, and
the ordinary way a row comes to exist is a checkout started here: the row is
created first, its id travels to the gateway as the checkout's client reference,
and the completed checkout is what ties the two together.

A subscription created before any of that — years ago, or last week in Stripe's
own dashboard — never went through that door. Its webhooks do arrive, and they
are stored, and they settle with a note saying nothing here is linked to
`sub_…`. Which is correct: a gateway's answer names a payer the gateway
invented, and **which of your teams that is** is not something it can tell us.

That is the only missing piece, and this is where you supply it.

## Do the catalogue first

An adopted subscription needs a plan, and the plan is resolved from the price it
is billed on — so the gateway's price has to be mapped into your catalogue
before the subscription can be adopted. If you have not done that yet:

```bash theme={null}
curl -X POST "$API/v1/organization/environments/$ENV/billing-providers/stripe/catalogue-import" \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: $ORG"
```

That reads your gateway's products and prices into plans and prices here, with
the mapping. If you priced your product here first instead, publish your prices
onto the gateway — same mapping, opposite direction.

Also make sure the connection's **health check** has run at least once. A price
mapping is keyed on the merchant account, and the account is only known after
something has asked the gateway who the keys belong to.

## Then give those plans their features

**This is the step that gets skipped**, and it fails quietly. The import brings
over plans and prices, and nothing else — no features, no matrix. A gateway
knows what it charges; it has no idea what your product lets somebody do.

So a subscription adopted onto an imported plan resolves to a plan with **no
features**: the customer is on Pro, pays for Pro, and is entitled to nothing.
Nothing errors, and the first report of it is a support ticket.

Set the features on each plan before you adopt anything — in the panel under
Catálogo, or through the catalogue API. If you already adopted, fixing the plan
fixes every subscription pointing at it; entitlements resolve through the plan,
not through a copy taken at adoption time.

## Adopting one

```bash theme={null}
curl -X POST "$API/v1/organization/subscriptions/link?environment=live" \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: $ORG" \
  -H "Content-Type: application/json" \
  -d '{
        "customer_external_id": "acme-inc",
        "provider_subscription_id": "sub_1P9x…"
      }'
```

```json theme={null}
{
  "id": "3d90…",
  "customer_id": "6f1c…",
  "plan_id": "b271…",
  "status": "active",
  "current_period_start": "2026-08-30T00:00:00Z",
  "current_period_end": "2026-09-30T00:00:00Z",
  "provider": "stripe",
  "provider_subscription_id": "sub_1P9x…",
  "provider_synced_at": "2026-09-09T15:04:05Z",
  "mirror_refreshed": true
}
```

**201** means a row now exists that did not.

Nothing in that answer came from your request except the customer. The status,
the period, the trial, a scheduled cancellation and the line's quantity are all
read from the gateway; the plan comes from the price mapping. Nothing was
charged and nothing changed at the gateway — what changed is that this system
now holds the subscription.

From then on it behaves like any other: webhooks find it, the nightly
reconciliation re-reads it, entitlements resolve through its plan, and it can be
changed or cancelled from here.

## Naming the customer

By `customer_id`, or by `customer_external_id` — **your** id for the team, which
is what makes this scriptable: your backend already knows which account maps to
which `cus_`, and it should not have to hold a map of our uuids too. Send one or
the other; a request carrying both holds two beliefs about who is paying.

The customer has to exist. This endpoint will not create one, because a team
conjured to hang money off would be a team with no members — use
`POST /v1/customers` on the machine surface, which is the call that knows the
team's name.

## Running it over a list

A migration of any size is a script, so the same act is on the machine surface
under a `uk_sk_` key — where the environment is the **key's** and no field can
move it:

```bash theme={null}
curl -X POST "$API/v1/subscriptions/link" \
  -H "Authorization: Bearer $UK_SECRET_KEY" \
  -H "Idempotency-Key: backfill-$EXTERNAL_ID" \
  -H "Content-Type: application/json" \
  -d "{\"customer_external_id\": \"$EXTERNAL_ID\", \"provider_subscription_id\": \"$SUB\"}"
```

Two layers answer two different retries, and it is worth knowing which is which:

* the **`Idempotency-Key`** replays the same response, bytes and status, for a
  repeated request — that is what a client's backoff needs, and it is what stops
  a lost answer from becoming a second call;
* adopting the **same subscription for the same customer** again, under a new
  key, answers **200** with the row and writes nothing — that is what a batch
  re-run a week later needs.

The same subscription for a **different** customer is refused either way: it is
the one repeat that means somebody made a mistake.

So a script that dies halfway can simply be run again. Read your own map of
account to `cus_`, resolve each one's subscriptions at the gateway, and call
this once per row — the refusals below tell you which rows to look at, and the
rest are done.

## What it refuses to guess

There is no un-adopt. A subscription is never deleted here, and its gateway
reference is only ever written into an empty column — so a wrong link is
permanent, and anything that would have to be assumed is refused instead.

| Code                           | What happened                                                                                                   | The repair                                                                                         |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `price_not_mapped_to_provider` | The price it is billed on is not in your catalogue, so there is no plan to record it against                    | Import the catalogue. The response carries `unmapped_provider_price_id` — that is the price to map |
| `subscription_has_extra_items` | The gateway holds more than one priced line, so which line names the plan is not a question any gateway answers | Mirror this one by hand, or simplify it at the gateway                                             |
| `subscription_already_linked`  | It is already mirrored here, under a different customer                                                         | Check which customer you meant                                                                     |
| `customer_mismatch`            | The gateway says a different payer holds it than the one this customer has been paying as                       | Almost always a transposed line in a script                                                        |
| `provider_account_unknown`     | The connection has never been verified, so the merchant account is unknown                                      | Run the connection's health check                                                                  |

`customer_mismatch` only fires when there is something to compare against. We
store no `cus_` of our own, so a customer's payer reference is discovered by
reading a subscription they already hold — and a customer you are about to
migrate usually has none. Silence is not a mismatch, and refusing on it would
refuse the whole migration.

## What it will not do for you

**It will not find the subscriptions.** There is no "import everything" here,
and that is deliberate: which team each `sub_` belongs to is the one fact this
system cannot derive, and a bulk endpoint would have to guess it for every row
at once. Your backend has the map; this is the call that records it.

**It will not backfill the invoices.** What is adopted is the subscription and
its current state. Payments made before it existed here stayed at the gateway,
which is still where that history lives; invoices from the next payment onward
arrive by webhook as usual.

## Last: point the gateway's webhook here

Adoption records the subscription **as it is right now**. Everything after that
— a renewal, a failed payment, a cancellation, a plan change made at the gateway
— arrives as a webhook, and only if the gateway knows where to send it.

The address is on the payment provider's screen in the panel, under **URL do
webhook**, once the connection exists. Paste it into your gateway's dashboard
and store the signing secret back here.

Skip this and everything you just migrated freezes in the state it arrived in,
with nothing to indicate it. A subscription that was cancelled last month still
reads as active, and the customer keeps their access.
