<div id="integration-guide">

<div class="ig-section" id="stored-cards-migration">
<div class="ig-section-header">
<div class="ig-section-icon" style="background:#f0faf0;color:#16a34a;"><i class="fas fa-id-card"></i></div>
<h2>Migrating stored cards &amp; recurring payments</h2>
</div>

<p class="ig-section-desc">For partners whose merchants store customers' cards on the legacy platform and charge them again later — subscriptions, instalments or saved cards. This is the second part of a wider migration: first your integration moves to the new platform using the guide for it, then — once that integration is built and live for a merchant — that merchant's stored cards move too, without any customer re-entering a card, in four phases that you control per merchant.</p>

<!-- Who -->
<div class="ig-subsection">
<h3>Who this guide is for</h3>
<p>Follow this guide if your systems hold token values received when customers saved their cards on the legacy platform, and you charge those tokens again. The stored values look like one of these:</p>
<ul>
<li>a multi-use token: <code>tm_</code> followed by letters and digits, e.g. <code>tm_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345</code></li>
<li>a virtual card number: 16 digits starting with <code>991001</code>, e.g. <code>9910012837465190</code></li>
</ul>
<p>It covers those tokens, the payments you make with them, and how new cards get saved from now on. It does not cover migrating your checkout itself — that is the first part of the project, with its own guide, and it must be finished before the token migration starts.</p>

```admonition
icon: fas fa-route
title: Where this sits in the migration
content: <strong>Step 1</strong> — build and go live with your new-platform integration (Hosted Checkout, Embedded Components or the direct API) using the guide for your legacy integration: <a href="/partners/online-payments/migration/overview">find it here</a>. <strong>Step 2</strong> — this guide: move the stored cards. Everything below assumes step 1 is done; if it isn't yet, start there and come back when your new integration is live.
```
</div>

<!-- Why -->
<div class="ig-subsection">
<h3>Why migrate, and what you gain</h3>
<p>The legacy platform, including its token store, is retiring in early 2027. Tokens minted on it cannot be charged on the new platform directly — but they don't need to be re-collected either: Teya imports your stored cards, and each legacy token upgrades itself the first time you charge it.</p>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>No customer re-enters a card.</strong> The import and the first-use conversion happen behind the scenes.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>No bulk conversion on your side.</strong> Your token database migrates itself, charge by charge, as your normal billing runs.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>No gap.</strong> Both platforms run side by side until you switch, so there is never a moment when a customer could fall through.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Higher authorisation rates.</strong> Converted cards are backed by card-scheme network tokens, which issuers approve more often than stored card numbers and which keep working when a card is reissued — so more recurring charges succeed, with fewer retries and failed renewals.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>One kind of token.</strong> At the end, every customer has a Teya token and one API charges them all — see <a href="/partners/online-payments/saved-payments/overview">Card on File</a>.</span></div>
</div>
</div>

<!-- What it involves -->
<div class="ig-subsection">
<h3>What the migration involves</h3>
<p>Four phases, in this order. The order is what makes it safe: at some point Teya takes a copy of your stored cards from the legacy platform, and any token created <em>after</em> that copy would be missing from it. So the first thing to stop is the <em>creation</em> of legacy tokens; the last thing to stop is their <em>use</em>.</p>

<div class="ig-flow-steps">
<div class="ig-flow-step"><span class="ig-flow-step-num">1</span> New cards go to the new platform</div>
<span class="ig-flow-arrow"><i class="fas fa-arrow-right"></i></span>
<div class="ig-flow-step"><span class="ig-flow-step-num">2</span> Two platforms side by side</div>
<span class="ig-flow-arrow"><i class="fas fa-arrow-right"></i></span>
<div class="ig-flow-step"><span class="ig-flow-step-num">3</span> Teya imports your stored cards</div>
<span class="ig-flow-arrow"><i class="fas fa-arrow-right"></i></span>
<div class="ig-flow-step"><span class="ig-flow-step-num">4</span> Legacy tokens move on first use</div>
</div>

<p><strong>Effort:</strong> a small development project — one new-card path, a one-line routing rule, and one field change for the cut-over. The phases can be weeks apart; there is no pressure on how long Phase 2 lasts.</p>

```admonition
icon: fas fa-paper-plane
title: Planning this across a merchant base
content: The phases run per merchant, and the import in Phase 3 is per merchant too. Agree phase dates with your Teya partner contact in batches, and make sure Phase 1 is complete for a merchant — every store, every code path, every background job — before you confirm it for import.
```

</div>

</div>

<div class="ig-divider"></div>

<div class="ig-section" id="developer-guide">
<div class="ig-section-header">
<div class="ig-section-icon" style="background:#eff6ff;color:#2563eb;"><i class="fas fa-code"></i></div>
<h2>Developer guide</h2>
</div>
<p class="ig-section-desc">The four phases in detail: what to build, the one routing rule, and what happens on the first charge of a legacy token. Token payments themselves are documented on <a href="/partners/online-payments/saved-payments/charge">Charge a Saved Payment Method</a>; this page only covers what is specific to migrating.</p>

<!-- Before you start -->
<div class="ig-subsection">
<h3>Before you start</h3>
<h4>1. Prerequisites</h4>
<ul>
<li>Your integration already takes card payments on the new platform, with credentials per store — see <a href="/partners/online-payments/migration/overview">the migration overview</a> if not.</li>
<li>You can call <code>POST /v3/transactions/online</code> — the direct API used for token payments. Read <a href="/partners/online-payments/saved-payments/overview">Card on File</a> and <a href="/partners/online-payments/saved-payments/charge">Charge a Saved Payment Method</a> first; the requests below are those pages' requests with one different payment method.</li>
</ul>

<h4>2. Agree the phase dates with your Teya contact</h4>
<p>Phase 3 can only start after Phase 1 is genuinely done: a legacy token created after the export will not be in the import, and in Phase 4 it fails with an explicit "not migrated" error rather than falling back to the legacy platform. Your Teya contact also provides test-environment legacy-token fixtures and confirms when the legacy-token payment method is enabled for your environment.</p>

<h4>3. Find the code you'll be touching</h4>
<p>Search your codebase for the places that:</p>
<ul>
<li><strong>create tokens on the legacy platform</strong> (<code>/api/token/single</code>, <code>/api/token/multi</code>, SecurePay tokenisation) — Phase 1's scope</li>
<li><strong>charge stored values</strong> (<code>tm_</code>, <code>991001</code>) — Phase 2's routing and Phase 4's scope</li>
</ul>

<h4>4. Ground rules</h4>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span>Tokens are resolved per merchant and store — charge a migrated token with the <code>store_id</code> it was migrated for.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Send legacy values <strong>exactly as stored</strong>; the format is validated strictly (max 64 characters, the two shapes above).</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Store the returned <code>token_response.token_id</code> on every successful charge — it is the customer's permanent Teya token.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Put the Phase 4 cut-over behind a flag so rollback is a config change.</span></div>
</div>
</div>

<!-- What replaces what -->
<div class="ig-subsection">
<h3>What replaces what</h3>
<div style="overflow-x:auto;">
<table class="ig-comparison">
<thead>
<tr><th>Legacy platform</th><th>New platform</th><th>What it means for you</th></tr>
</thead>
<tbody>
<tr><td>Multi-use token <code>tm_…</code> / virtual card <code>991001…</code></td><td>Teya token — a UUID in <code>token_response.token_id</code></td><td>Charged with payment method <code>TOKEN</code>. Legacy values are accepted once more, as payment method <code>LEGACY_TOKEN</code>, to convert them (Phase 4).</td></tr>
<tr><td><code>POST /api/token/single</code>, <code>/api/token/multi</code></td><td><code>store_payment_method: true</code> on a <code>SALE</code> or a zero-amount <code>ACCOUNT_VERIFICATION</code> transaction</td><td>Tokenisation happens inside the payment; no separate call. See <a href="/partners/online-payments/saved-payments/overview">Card on File</a>.</td></tr>
<tr><td><code>POST /api/payment</code> with a multi-use token</td><td><code>POST /v3/transactions/online</code> with <code>payment_method.type: "TOKEN"</code></td><td>Same endpoint as every other direct-API payment. See <a href="/partners/online-payments/saved-payments/charge">Charge a Saved Payment Method</a>.</td></tr>
<tr><td>Recurring flag on the payment</td><td><code>initiator</code> (<code>MERCHANT</code> / <code>CUSTOMER</code>) + <code>recurring_payment_type</code> (<code>RECURRING</code> / <code>INSTALLMENT</code> / <code>CARD_ON_FILE</code>)</td><td><code>MERCHANT</code> = cardholder not present, no 3DS. <code>CUSTOMER</code> = cardholder present, 3DS applies and may challenge.</td></tr>
<tr><td>Token deletion</td><td><code>DELETE /v1/tokens/{token_id}</code> → <code>204</code></td><td>Delete the token when the customer removes a saved card.</td></tr>
<tr><td>Your own token database</td><td>Your own token database — now holding a Teya token (UUID) or, temporarily, a legacy value</td><td>The routing rule below keys on which one the customer has.</td></tr>
</tbody>
</table>
</div>
</div>

<!-- Phase 1 -->
<div class="ig-subsection">
<h3>Phase 1 — Saving new cards on the new platform</h3>
<p><strong>What:</strong> stop creating tokens on the legacy platform. From this day, when a customer saves a card, save it on the new platform and store the Teya token you get back.</p>
<p><strong>Why:</strong> every card saved from now on is already migrated, and the export in Phase 3 will be complete.</p>
<p>A token is created by setting <code>store_payment_method: true</code> on a transaction — as part of a sale (<code>type: "SALE"</code>), or without charging the customer (a zero-amount <code>type: "ACCOUNT_VERIFICATION"</code>, for an "add a payment method" flow). Either way the response carries a <code>token_response</code>:</p>

```json
{
  "type": "ONLINE_TRANSACTION",
  "online_transaction": {
    "transaction_id": "5f7b1a3a-3b7a-4b1a-9b1a-3a3b7a4b1a9b",
    "status": "SUCCESS",
    "token_response": {
      "token_status": "SUCCESS",
      "token_id": "1e2d4f80-9c3e-4f5b-83aa-9e8d2b6a6c49",
      "brand": "VISA",
      "last4": "1234"
    }
  }
}
```

<p>Store <code>token_id</code> against the customer. <code>brand</code> and <code>last4</code> are for what you show them ("Visa ending 1234").</p>
<p><strong>Things to get right:</strong></p>
<ul>
<li>Token creation can fail while the payment succeeds: <code>token_status: "FAILED"</code> with a <code>failure_reason</code>. Treat it as "saving the card didn't work", not as a payment problem.</li>
<li>A token is only kept when the payment succeeds. Don't store anything from a failed transaction.</li>
<li>Disable legacy token creation <em>everywhere</em> — a forgotten code path undermines Phase 3.</li>
</ul>
<p>Full reference: <a href="/partners/online-payments/saved-payments/overview">Card on File — storing the card</a>.</p>
</div>

<!-- Phase 2 -->
<div class="ig-subsection">
<h3>Phase 2 — Two platforms side by side</h3>
<p><strong>What:</strong> existing legacy tokens keep being charged where they always were — on the legacy platform, unchanged. Customers who saved a card since Phase 1 are charged on the new platform, as an ordinary token payment.</p>
<p><strong>Why:</strong> nothing is at risk while the import is prepared, and every card saved during this phase is already on the new platform.</p>

```bash
curl -X POST https://api.teya.com/v3/transactions/online \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "store_id": "5bca0d99-3f47-4d91-9372-cf12b8ec50db",
    "type": "SALE",
    "initiator": "MERCHANT",
    "amount": { "value": 10000, "currency": "GBP" },
    "merchant_reference_id": "order_ABC123",
    "payment_method": {
      "type": "TOKEN",
      "token": {
        "token_id": "1e2d4f80-9c3e-4f5b-83aa-9e8d2b6a6c49",
        "recurring_payment_type": "RECURRING"
      }
    }
  }'
```

<p>Your billing engine's routing rule for this whole phase is one line:</p>

```
for each due charge:
    customer has a Teya token (UUID)  → new platform, payment method TOKEN
    customer has a legacy value       → legacy platform, unchanged
```

<p>Two fields describe <em>why</em> you are charging a stored card:</p>
<div style="overflow-x:auto;">
<table class="ig-comparison">
<thead>
<tr><th>Field</th><th>Values</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td rowspan="2"><code>initiator</code></td><td><code>MERCHANT</code></td><td>The cardholder is not present — a recurring charge, an instalment, or a merchant-initiated card-on-file payment. No 3DS/SCA is performed.</td></tr>
<tr><td><code>CUSTOMER</code></td><td>The cardholder is present (e.g. clicking "pay with saved card" on your site). 3DS/SCA is performed and may require a challenge. Default if omitted.</td></tr>
<tr><td><code>recurring_payment_type</code></td><td><code>RECURRING</code>, <code>INSTALLMENT</code>, <code>CARD_ON_FILE</code></td><td>The type of stored-card usage. <code>CUSTOMER</code>-initiated payments support only <code>CARD_ON_FILE</code>; <code>MERCHANT</code>-initiated payments support all three.</td></tr>
</tbody>
</table>
</div>
<p><strong>Things to get right:</strong> <code>amount.value</code> is in the smallest currency unit (100 ISK = <code>100</code>, 100 EUR = <code>10000</code>). Send an <code>Idempotency-Key</code> per charge attempt.</p>
<p>Full reference: <a href="/partners/online-payments/saved-payments/charge">Charge a Saved Payment Method</a>.</p>
</div>

<!-- Phase 3 -->
<div class="ig-subsection">
<h3>Phase 3 — Teya imports your stored cards</h3>
<p>Once you've confirmed to your Teya contact that no new legacy tokens are being created, Teya takes the file of your stored cards from the legacy platform and imports it into the new one. Your customers never re-enter a card and you never touch card data. Keep the Phase 2 routing unchanged while the import runs; Teya confirms when it is complete.</p>
</div>

<!-- Phase 4 -->
<div class="ig-subsection">
<h3>Phase 4 — Moving legacy tokens to the new platform</h3>
<p><strong>What:</strong> with the import confirmed, retire the "legacy platform" branch of your routing. A legacy token is now charged on the new platform — same endpoint, same fields — with one different payment method:</p>

```json
  "payment_method": {
    "type": "LEGACY_TOKEN",
    "legacy_token": {
      "token_id": "tm_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345",
      "recurring_payment_type": "RECURRING"
    }
  }
```

<p><strong>What happens on first use.</strong> The first time each legacy token is charged on the new platform, it is upgraded in-line, inside that payment:</p>
<ol>
<li>The platform checks its mapping store for your store + this token. If a mapping already exists, the payment proceeds directly as a normal token payment and none of the following steps run.</li>
<li>If not, the imported records are consulted under your merchant identity to find the stored card behind the token.</li>
<li>The card details are retrieved — in memory, transiently; they never appear in responses, logs or events.</li>
<li>A <strong>network token</strong> — a card credential issued by the card scheme itself — is created for that card, under your store.</li>
<li>The mapping legacy token → network token is saved. Concurrent first charges of the same token are safe: one mapping wins and both payments use the same network token.</li>
<li>Only then does the payment itself run — authorisation, persistence, webhook — using the network token.</li>
</ol>
<p>Two properties of this design are worth knowing: the imported card record is <strong>never used to pay directly</strong> — it exists only to mint the network token, once; and if any step of the conversion fails, the payment fails with an explicit error (below) with nothing half-saved, so a later retry starts clean. Expect the first charge of each token to take a little longer than usual; every later charge skips the conversion entirely.</p>

<p><strong>What comes back — and the one thing you must do with it.</strong> A successful charge returns <code>201</code>, and the response carries the customer's new Teya token:</p>

```json
  "token_response": {
    "token_status": "SUCCESS",
    "token_id": "9c1b6f2e-4a8d-4e0f-b3aa-2f7d1c5e8a90",
    "brand": "VISA",
    "last4": "1234"
  }
```

<p>The same <code>token_response</code> also appears on the transaction lookup and on the <code>payment.succeeded.v1</code> <a href="/partners/online-payments/webhooks">webhook</a>. Store the <code>token_id</code>, and from this customer's next charge on send it as <code>payment_method.type: "TOKEN"</code>. Your routing rule becomes:</p>

```
for each due charge:
    customer has a Teya token (UUID)  → payment method TOKEN
    only a legacy value               → payment method LEGACY_TOKEN,
                                        store token_response.token_id on success
```

<p><strong>Things to get right:</strong></p>
<ul>
<li>Each billing cycle converts every customer it touches. A legacy token always resolves to the <em>same</em> Teya token, so charging with the legacy value again simply returns the same <code>token_id</code> — a lagging deploy or a webhook/response race does no harm.</li>
<li>Track the drain with one query: customers who still have only a legacy value. When that number flattens, what's left is customers you haven't billed since the switch; their tokens convert whenever you next charge them.</li>
<li>Customer-present charges (<code>initiator: "CUSTOMER"</code>) may return <code>202</code> with <code>redirect_html</code> for a 3DS challenge — handle it exactly as on <a href="/partners/online-payments/saved-payments/charge">Charge a Saved Payment Method</a>. The conversion happens before the challenge, and the completed payment carries the same <code>token_response</code>.</li>
</ul>
</div>

<!-- Errors -->
<div class="ig-subsection">
<h3>Errors specific to legacy tokens</h3>
<p>All errors use the API's standard shape — a <code>code</code> and a human-readable <code>description</code>. A payment that fails this way is never sent for authorisation: the customer is not charged and no webhook is emitted.</p>
<div style="overflow-x:auto;">
<table class="ig-comparison">
<thead>
<tr><th>HTTP</th><th>Description you'll see</th><th>Meaning</th><th>What to do</th></tr>
</thead>
<tbody>
<tr><td><code>400</code></td><td>Legacy token must be either a tm_ prefixed token or a 16-digit virtual card PAN starting with 991001</td><td>The value doesn't match the two valid shapes</td><td>Fix the stored value or the lookup that produced it — this was never a valid token.</td></tr>
<tr><td><code>400</code></td><td>Legacy token has not been migrated to a new token. Please contact support.</td><td>No imported record for your store + this token</td><td>Either the import (Phase 3) hasn't completed for this store, or the token was created after Phase 1 should have ended. Don't retry in a loop; flag it and check with your Teya contact.</td></tr>
<tr><td><code>400</code></td><td>Legacy token payment method is not enabled</td><td>The legacy-token flow isn't switched on for your environment yet</td><td>Ask your Teya contact about rollout status.</td></tr>
<tr><td><code>500</code></td><td>Error resolving legacy token</td><td>The first-use conversion failed part-way</td><td>Transient — safe to retry later. Nothing was half-migrated.</td></tr>
</tbody>
</table>
</div>
<p>The signal to watch at cut-over is the ratio of legacy-token failures to attempts. A burst of "not migrated" errors on day one almost always means Phases 3 and 4 happened in the wrong order — or that something, somewhere, is still creating legacy tokens.</p>
</div>

<!-- Rollout -->
<div class="ig-subsection">
<h3>Testing and rolling out</h3>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Phase 1 go-live:</strong> deploy the new-card path, verify a real saved card end to end in staging with <a href="/partners/online-payments/testing">test cards</a>, then disable legacy token creation everywhere.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Phase 2</strong> is a steady state, not an event. There is no pressure on its duration.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Phase 3:</strong> confirm to your Teya contact that legacy token creation has stopped; keep your routing unchanged until the import is confirmed.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Phase 4 cut-over:</strong> behind a flag. Canary first — one internal customer or a small slice of a billing run; verify the charge, the returned token, and a follow-up <code>TOKEN</code> charge. Then a full billing cycle, watching the failure ratio.</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Wind down:</strong> when your legacy-value-only count flattens, agree with your Teya contact how long the imported data stays available, and clean dead legacy values out of your database at your leisure.</span></div>
</div>

```admonition
icon: fas fa-life-ring
title: Need help?
content: Your Teya partner contact is the route for agreeing phase dates per merchant, confirming each import (test and production), test-environment legacy-token fixtures, rollout status of the legacy-token payment method, and the retention window for imported data. For integration questions, see <a href="/support">Support</a>.
```

</div>

<div class="ig-subsection">
<h3>Next steps</h3>
<ul>
<li><a href="/partners/online-payments/saved-payments/charge">Charge a Saved Payment Method</a> - Token payments, 3DS challenges and the full request reference</li>
<li><a href="/partners/online-payments/saved-payments/overview">Card on File</a> - Storing cards during a payment or with an account verification</li>
<li><a href="/partners/online-payments/webhooks">Webhooks</a> - Receive and verify payment notifications</li>
<li><a href="/partners/online-payments/migration/overview">Migration overview</a> - The other migration guides</li>
<li><a href="/apis/online-payments/apis">API Reference</a> - Full API endpoint documentation</li>
</ul>
</div>

</div>
</div>
