<div id="integration-guide">

<div class="ig-section" id="rpg-embedded-components">
<div class="ig-section-header">
<div class="ig-section-icon" style="background:#fef7f0;color:#ea580c;"><i class="fas fa-puzzle-piece"></i></div>
<h2>Migrating from RPG to Embedded Components</h2>
</div>

<p class="ig-section-desc">For businesses whose website talks directly to <strong>RPG (Borgun Payment API)</strong>. The first half is for the business owner; the second half is for your developer.</p>

<!-- Who -->
<div class="ig-subsection">
<h3>Who this guide is for</h3>
<ul>
<li>Your code calls URLs containing <code>borgun.is</code>, <code>rpgapi</code> or <code>/api/payment</code>, authenticated with a <strong>Private Access Token</strong>.</li>
<li>You run 3D Secure yourself through <code>/api/mpi/v2/enrollment</code> and <code>/api/mpi/v2/validation</code>.</li>
<li>Your checkout page loads <code>BAPIjs</code> / <code>borgun-payment.js</code> to create single-use tokens.</li>
</ul>

```admonition
icon: fas fa-map-signs
title: Not you?
content: Redirects to <code>securepay.borgun.is</code>, payment URLs created in advance, and stored cards charged without the customer present each have their own guide.
link: /online-payments/migration/overview
```
</div>

<!-- Why -->
<div class="ig-subsection">
<h3>Why migrate, and what you gain</h3>
<p>RPG runs on Teya's legacy platform, which is retiring in early 2027; onboarding to it stops soon. The new platform is an upgrade, not a like-for-like swap: it removes the parts of RPG that cost the most to build and maintain.</p>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>More completed payments</strong> — merchants on the new platform generally see higher conversion: modern 3D Secure with fewer challenges, less checkout drop-off, and card retries that keep the sale</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>No 3D Secure to build</strong> — enrollment, challenge and validation run inside the component</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>No card data on your systems</strong> — Teya-hosted fields inside your page, a smaller PCI scope</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Payment confirmations that can't get lost</strong> — a signed webhook for every successful payment</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Apple Pay and currency conversion</strong> without extra code</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>Refunds without a developer</strong>, and partial captures on pre-authorisations</span></div>
</div>
</div>

<!-- Options -->
<div class="ig-subsection">
<h3>Your options</h3>
<p><strong>Checkout component</strong> <span class="ig-badge ig-badge-recommended">Recommended</span> — the closest match to an RPG checkout. The customer stays on your page; one block provides card fields, pay button, 3D Secure, Apple Pay and currency conversion.</p>
<ul>
<li><strong>Card component</strong> — a single card field inside your own form with your own pay button. See <a href="/online-payments/embedded-components/overview">Embedded Components</a>.</li>
<li><strong>Hosted Checkout</strong> — the customer is redirected to a Teya page and back. Least code, different customer experience. See <a href="/online-payments/hosted-checkout/get-started">Hosted Checkout</a>.</li>
<li><strong>Direct API</strong> — your servers send card data to Teya, as RPG allows today. Restricted: needs evidence of PCI DSS compliance (SAQ D). Only where components have no equivalent — see the <a href="/online-payments/migration/rpg">RPG capability reference</a>.</li>
</ul>
</div>

<!-- Involves -->
<div class="ig-subsection">
<h3>What the migration involves</h3>
<div class="ig-flow-steps">
<div class="ig-flow-step"><span class="ig-flow-step-num">1</span> Get credentials</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> Build</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> Test in staging</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> Go live</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">5</span> Retire RPG</div>
</div>
<p>For a one-off-payments RPG integration this is a small development project; pre-authorisations and custom logic sit at the upper end. RPG keeps working in parallel until you switch it off.</p>

```admonition
icon: fas fa-paper-plane
title: What to send your developer
content: (1) A link to this page, pointing at the developer guide below. (2) A development store from your Teya account manager, for staging credentials. (3) Two questions to answer back: does the integration use <strong>pre-authorisations</strong>, and does it use <strong>stored cards or recurring payments</strong> (multi-use tokens)? Both change the plan.
```

</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">How an RPG integration maps onto a checkout session and the Checkout component, with the one request you need at each step.</p>

<!-- Before / after -->
<div class="ig-subsection">
<h3>How it works: before and after</h3>
<p><strong>Today (RPG)</strong></p>
<ol>
<li>Your form or <code>BAPIjs</code> collects the card; card data or a single-use token reaches your server.</li>
<li>Your server runs 3D Secure: enrollment, ACS redirect, <code>TermURL</code> callback, validation, <code>MdStatus</code>.</li>
<li>Your server calls <code>POST /api/payment</code> with the <code>ThreeDSecure</code> block and reads <code>ActionCode</code>.</li>
<li>Optionally a <code>PaymentCreate</code> webhook arrives and you call back <code>GET /api/events/{EventId}</code>.</li>
</ol>
<p><strong>After (Embedded Components)</strong></p>
<ol>
<li>Your server gets a short-lived access token and creates a <strong>checkout session</strong>; the response carries a session token.</li>
<li>Your page initialises the SDK with that token and mounts the <strong>Checkout component</strong>. Card data goes into Teya-hosted fields, never into your DOM or server.</li>
<li>The customer pays in place; 3D Secure runs inside the component.</li>
<li><code>onSuccess</code> fires for the user experience; a signed <strong>webhook</strong> confirms the payment to your server and drives fulfilment.</li>
</ol>
<p><strong>The biggest change:</strong> the whole MPI flow and all card-data or single-use-token handling are deleted — the component replaces both.</p>
</div>

<!-- Before you start -->
<div class="ig-subsection">
<h3>Before you start</h3>

<h4>1. Credentials</h4>

<p>Ask your Teya account manager for a development store. In the <a href="https://business.teya.xyz" target="_blank" rel="noopener">Staging Business Portal</a>, select the store, go to <strong>Integrations</strong> and create ecommerce API credentials: a <strong>Client ID</strong>, a <strong>Client Secret</strong> (shown once) and your <strong>Store ID</strong>. Repeat at <code>business.teya.com</code> for production; never mix the two. This replaces your Private and Public Access Tokens. See <a href="/online-payments/authenticate">Authenticate</a>.</p>

<h4>2. Environments</h4>
<p>Staging is <code>api.teya.xyz</code> / <code>id.teya.xyz</code> / <code>business.teya.xyz</code> with <code>developmentMode: true</code> in the SDK; production is the same hosts on <code>.com</code> without the flag. Full table on <a href="/online-payments/embedded-components/checkout/get-started">Embedded Checkout</a>.</p>

<h4>3. Webhook</h4>
<p>Teya sends a signed <code>payment.succeeded.v1</code> event to a public HTTPS endpoint with no authentication on it.</p>

<p>Register the URL in the Business Portal next to your credentials; the portal issues a <strong>public key per webhook</strong> for verifying signatures.</p>

<p>Unlike RPG's <code>PaymentCreate</code> / <code>Refund</code> / <code>Capture</code> events, only successful customer payments produce a webhook today; refunds and captures you trigger return their outcome in the API response. See <a href="/online-payments/webhooks">Webhooks</a>.</p>

<h4>4. Find the legacy code</h4>
<p>Search for <code>borgun.is</code>, <code>rpgapi</code>, <code>/api/payment</code>, <code>/api/token/single</code>, <code>/api/mpi</code>, <code>TransactionType</code>, <code>ActionCode</code>, <code>MdStatus</code>, <code>TermURL</code>, <code>MpiToken</code>, <code>BAPIjs</code>, <code>borgun-payment</code>, <code>PrivateAccessToken</code>, <code>BorgunRPGNet</code>, <code>RPGClient</code>. Each hit is a line on the to-do list. The .NET SDK is a thin wrapper over the same endpoints, so the mapping below applies to its methods one-to-one; the new platform has no server-side SDK — plain HTTPS on the server, the JS SDK in the browser.</p>

<h4>5. Ground rules</h4>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span>Complete a payment, webhook and refund end to end in staging before touching production code</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Keep RPG live behind a flag for a day or two after switching, so rollback is a config change</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Never expose the Client Secret in browser code or repositories</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Fulfil orders from the webhook, never from a browser callback alone</span></div>
</div>
</div>

<!-- Mapping -->
<div class="ig-subsection">
<h3>What replaces what</h3>
<p>The <a href="/online-payments/migration/rpg">RPG capability reference</a> maps each capability to its replacement. This table goes one level down: the calls and fields an RPG developer has to change.</p>

<div style="overflow-x:auto;">
<table class="ig-comparison">
<thead>
<tr><th>RPG today</th><th>New platform</th><th>What it means for you</th></tr>
</thead>
<tbody>
<tr><td>Private Access Token (permanent, Basic auth)</td><td><code>client_id</code> + <code>client_secret</code> → short-lived Bearer token</td><td>Cache and refresh from <code>expires_in</code> (currently 15 min in production, 24 h in staging).</td></tr>
<tr><td>Public Access Token (browser tokenisation)</td><td>Not needed — the session token comes from your backend per payment</td><td>Delete with the <code>BAPIjs</code> code.</td></tr>
<tr><td><code>POST /api/payment</code>, <code>TransactionType: "Sale"</code></td><td><code>POST /v2/checkout/sessions</code>, <code>type: "SALE"</code> + the component</td><td>The server sends intent only — amount, currency, reference. No card data, no 3DS block.</td></tr>
<tr><td><code>POST /api/payment</code>, <code>TransactionType: "PreAuthorization"</code></td><td><code>type: "PRE_AUTHORISATION"</code> on the same endpoint</td><td>See <em>Pre-authorisations</em> below.</td></tr>
<tr><td><code>PUT /api/payment/{id}/capture</code> (full only)</td><td><code>POST /v1/transactions/{id}/capture</code>, <code>{"amount": n}</code></td><td>Partial capture gained. Once per authorisation; the remainder is released.</td></tr>
<tr><td><code>PUT /api/payment/{id}/cancel</code></td><td><code>POST /v3/refunds</code> on the uncaptured payment</td><td>A refund before capture reverses the authorisation. No separate cancel.</td></tr>
<tr><td><code>PUT /api/payment/{id}/refund</code> + <code>PartialAmount</code></td><td><code>POST /v3/refunds</code> with <code>transaction_id</code>, <code>amount</code> — or the Business Portal</td><td>Same semantics: partial, multiple, total ≤ original.</td></tr>
<tr><td><code>GET /api/payment/{id}</code></td><td><code>GET /v2/checkout/sessions/{session_id}</code></td><td><code>session_status</code> + <code>payment_status</code> instead of <code>TransactionStatus</code>.</td></tr>
<tr><td><code>POST /api/token/single</code> + <code>BAPIjs.getToken</code></td><td>Gone — the component's hosted fields do this</td><td>No token creation, lifetime management or disable calls.</td></tr>
<tr><td><code>/api/mpi/v2/enrollment</code>, <code>/validation</code>, <code>TermURL</code>, <code>MdStatus</code>, <code>MpiToken</code></td><td>Gone — 3D Secure runs inside the component, challenge inline</td><td>Delete the whole flow.</td></tr>
<tr><td><code>POST /api/payment</code> with raw <code>PAN</code></td><td>Direct API — restricted, PCI SAQ D evidence required</td><td>Use the component unless you have no alternative.</td></tr>
<tr><td><code>POST /api/token/multi</code>, <code>VerifyCard</code></td><td>Card on File</td><td>Separate track — <a href="/online-payments/migration/stored-cards">Stored cards &amp; recurring</a>.</td></tr>
<tr><td><code>Amount</code> — 2 implied decimals, including ISK (100 ISK = <code>10000</code>)</td><td><code>amount.value</code> — minor units per ISO 4217; ISK has no subunit (100 ISK = <code>100</code>, €54.43 = <code>5443</code>)</td><td><strong>Divide RPG ISK amounts by 100.</strong> The most likely silent bug in an ISK migration; other currencies are unchanged.</td></tr>
<tr><td><code>Currency</code> — ISO numeric (<code>"352"</code>)</td><td><code>amount.currency</code> — ISO alphabetic (<code>"ISK"</code>)</td><td>Lookup change.</td></tr>
<tr><td><code>OrderId</code> — exactly 12 chars, A–Z/0–9</td><td><code>merchant_reference</code> (free-form) + optional <code>metadata</code></td><td>Write the legacy <code>OrderId</code> into <code>metadata</code> during transition if it helps reconciliation.</td></tr>
<tr><td><code>TransactionDate</code></td><td>Not sent — Teya timestamps server-side</td><td>Delete.</td></tr>
<tr><td><code>ActionCode</code> (<code>"000"</code> = approved) + <code>TransactionStatus</code></td><td><code>payment_status</code> (<code>SUCCESS</code> / <code>FAILED</code>) on the session</td><td>No action-code tables; declines show in the component and the customer retries in place.</td></tr>
<tr><td><code>DynamicDescriptors</code></td><td>Not exposed on checkout sessions</td><td>Tell your Teya contact before migrating if you rely on them.</td></tr>
<tr><td><code>PaymentCreate</code> webhook + <code>GET /api/events/{EventId}</code></td><td><code>payment.succeeded.v1</code>, RSA-signed (<code>x-teya-signature</code>)</td><td>Verify by signature instead of calling back.</td></tr>
<tr><td>B-Online</td><td>Business Portal</td><td>Credentials, webhooks, refunds, transaction visibility.</td></tr>
</tbody>
</table>
</div>
</div>

<!-- Steps -->
<div class="ig-subsection">
<h3>Step-by-step</h3>
<p>Steps 1–5 build the Checkout component end to end. Samples are cURL and minimal JavaScript; other languages are on the <a href="/online-payments/embedded-components/checkout/get-started">Embedded Checkout Get Started</a> page.</p>

<h4>Step 1 — Fetch an access token (server)</h4>
<p>Exchange <code>client_id</code> + <code>client_secret</code> for a Bearer token. This replaces the permanent Private Access Token: cache the token per environment and refresh it before <code>expires_in</code> runs out or on a <code>401</code>. Scopes for this guide: <code>checkout/sessions/create</code>, <code>checkout/sessions/id/get</code>, <code>refunds/create</code>, plus <code>captures/create</code> for pre-authorisations.</p>

```bash
curl -X POST https://id.teya.xyz/oauth/v2/oauth-token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$TEYA_CLIENT_ID" \
  -d "client_secret=$TEYA_CLIENT_SECRET" \
  -d "scope=checkout/sessions/create checkout/sessions/id/get refunds/create"
```

```json
{ "access_token": "eyJhbGciOiJSUzI1NiJ9...", "token_type": "Bearer", "expires_in": 900 }
```

<p>Full reference: <a href="/online-payments/authenticate">Authenticate</a>.</p>

<h4>Step 2 — Create a checkout session (server)</h4>
<p>When the customer clicks Pay, your server describes the payment and gets back a session token for the page. This replaces <code>POST /api/payment</code>, but the body carries no card data and no 3DS block — only the intent.</p>

```bash
curl -X POST https://api.teya.xyz/v2/checkout/sessions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "store_id": "123e4567-e89b-12d3-a456-426614174000",
    "type": "SALE",
    "amount": { "value": 2490, "currency": "ISK" },
    "merchant_reference": "ORDER-2026-00042",
    "metadata": { "legacy_orderid": "ORDER1230001" }
  }'
```

```json
{
  "session_id": "14411456-79c8-4d8c-8498-f01907093b83",
  "session_token": "eyJhbGciOiJSUzI1NiJ9...",
  "session_status": "ACTIVE"
}
```

<p><strong>Things to get right</strong></p>
<ul>
<li><code>amount.value</code> is an integer in minor units per ISO 4217: 100 ISK = <code>100</code>, not RPG's <code>10000</code>.</li>
<li><code>Idempotency-Key</code> is unique per order attempt; retries with the same key can't create duplicate sessions.</li>
<li>Create the session when the customer clicks Pay, not when the cart loads — sessions expire.</li>
<li>Return only <code>session_token</code> to the browser through a small endpoint of your own — never the access token or the Client Secret.</li>
</ul>
<p>Full reference: <a href="/apis/online-payments/apis">API Reference</a>.</p>

<h4>Step 3 — Mount the Checkout component (frontend)</h4>
<p>Install <code>@teyaproduct/teya-blocks-js</code>, initialise the SDK with the session token from your backend, and mount the component. This replaces your card form and <code>BAPIjs.getToken</code>: the fields are Teya-hosted, so card data never enters your DOM.</p>

```javascript
import { initTeyaBlocks } from '@teyaproduct/teya-blocks-js';

// 1. Ask your backend for a session (Step 2)
const { session_token } = await (await fetch('/api/teya-session', { method: 'POST' })).json();

// 2. Initialise the SDK
const teya = await initTeyaBlocks(session_token, {
  locale: 'en',
  appearance: { theme: 'flat' },
  // Routes SDK calls to Teya staging. Remove for production.
  developmentMode: true,
});

// 3. Mount the checkout — card fields, pay button, 3DS, Apple Pay, DCC included
const checkout = teya.elements.create('checkout', {
  onSuccess: (response) => { window.location.href = '/order/confirmation'; },
  onError: (error) => console.error('Payment failed', error),
});

checkout.mount('#checkout-container');
```

<p><strong>Things to get right</strong></p>
<ul>
<li>A staging session token only works with <code>developmentMode: true</code>; a production token only works without it. Gate the flag on your own environment.</li>
<li>Apple Pay needs a one-off domain verification per domain in the Business Portal under <strong>Integrations &gt; Apple Pay</strong>; card payments work without it.</li>
<li>Where did 3D Secure go? Inside the component. Enrollment, the inline challenge, validation and <code>MdStatus</code> are no longer your code.</li>
</ul>
<p>Full reference: <a href="/online-payments/embedded-components/sdk-initialization">SDK Initialization</a> and <a href="/online-payments/embedded-components/checkout/get-started">Embedded Checkout</a>.</p>

<h4>Variant: the Card component</h4>
<p>For your own form layout and pay button, create a <code>card</code> element instead and call <code>card.submitPayment()</code> from your button; the session and everything after are the same. No built-in Apple Pay button. See <a href="/online-payments/embedded-components/card/get-started">Card Element</a>.</p>

<h4>Step 4 — Handle the result</h4>
<p><code>onSuccess</code> / <code>onError</code> are for the user experience: navigate to a confirmation page, show an error state. The webhook (Step 5) is the source of truth for fulfilment. As a fallback (customer closed the browser, webhook delayed), look the session up by ID. This replaces <code>GET /api/payment/{id}</code> and the "only <code>000</code> means approved" rule.</p>

```bash
curl https://api.teya.xyz/v2/checkout/sessions/$SESSION_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json
{
  "session_id": "14411456-79c8-4d8c-8498-f01907093b83",
  "session_status": "COMPLETED",
  "amount": { "value": 2490, "currency": "ISK" },
  "type": "SALE",
  "payment_status": "SUCCESS"
}
```

<p><strong>Things to get right</strong></p>
<ul>
<li><code>session_status</code> runs <code>ACTIVE</code> → <code>PROCESSING</code> → <code>COMPLETED</code> or <code>EXPIRED</code>; trust <code>payment_status</code> only once <code>COMPLETED</code>.</li>
<li>Store the <code>transaction_id</code> from the webhook against the order — it is the key for refunds and receipts.</li>
</ul>
<p>Full reference: <a href="/apis/online-payments/apis">API Reference</a>.</p>

<h4>Step 5 — Receive and verify the webhook (server)</h4>
<p>Teya POSTs a signed <code>payment.succeeded.v1</code> event to your registered URL when a payment succeeds — the equivalent of RPG's <code>PaymentCreate</code>, verified by signature instead of a call back to <code>GET /api/events/{EventId}</code>. Verify <code>x-teya-signature</code> (SHA256withRSA, Base64) over the <strong>raw request body</strong> with the public key issued for this webhook, before parsing anything.</p>

```json
{
  "event": "payment.succeeded.v1",
  "timestamp": "2025-12-02T12:04:22Z",
  "data": {
    "transaction_id": "tr_019adef3-0d07-7543-9b74-efa2d5b6fa2a",
    "session_id": "019adef2-e5b3-7e00-9fe1-61a5a01f3eef",
    "merchant_reference": "ORDER-2026-00042",
    "amount": { "value": 2490, "currency": "ISK" },
    "status": "SUCCESS"
  }
}
```

<p><strong>Things to get right</strong></p>
<ul>
<li>Be idempotent — the same event can arrive more than once. Return <code>200</code> promptly and do slow work asynchronously.</li>
<li>Register the route before any JSON body parser so you verify the raw bytes; use the key for <em>this</em> webhook in <em>this</em> environment.</li>
<li>Check your handler with the <a href="/online-payments/webhook-signature-validator">Webhook Signature Validator</a> before go-live.</li>
</ul>
<p>Full reference: <a href="/online-payments/webhooks">Webhooks</a>.</p>
</div>

<!-- After the payment -->
<div class="ig-subsection">
<h3>After the payment</h3>
<p>Each operation returns its outcome in the synchronous API response — there is no webhook for refunds or captures today, so record the response.</p>

<h4>Refunds</h4>
<p>Replaces <code>PUT /api/payment/{id}/refund</code>, same rules: partial and multiple refunds, total ≤ original. <code>201</code> is processed; <code>202</code> is still processing — retry with the same <code>Idempotency-Key</code>. Staff can also refund from the Business Portal. Payments taken on RPG before cutover stay refundable through B-Online for a transition period.</p>

```bash
curl -X POST https://api.teya.xyz/v3/refunds \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "transaction_id": "tr_019adef3-0d07-7543-9b74-efa2d5b6fa2a", "amount": 1000 }'
```

<p>See <a href="/online-payments/payment-actions">Payment Actions</a>.</p>

<h4>Payment status and receipts</h4>
<p>Status: <code>GET /v2/checkout/sessions/{session_id}</code> (Step 4) replaces <code>GET /api/payment/{id}</code>; the Business Portal covers day-to-day visibility. Receipts are new — <code>POST /v1/transactions/{id}/receipts</code> emails a Teya-generated receipt (scope <code>transactions/id/receipts/create</code>). See <a href="/online-payments/payment-actions">Payment Actions</a>.</p>

<h4>Pre-authorisations</h4>
<p>Skip this if you only send <code>TransactionType: "Sale"</code>. Create the session with <code>type: "PRE_AUTHORISATION"</code>; the component flow is unchanged and the funds are held (no DCC on pre-auth). Capture with <code>POST /v1/transactions/{id}/capture</code> and <code>{"amount": n}</code> — any amount up to the authorised total, but <strong>exactly once</strong>: the remainder is released, so capture the final amount. There is no cancel call: refund the uncaptured payment to reverse the authorisation; untouched holds expire on their own. Pre-authorisations are currently available for certain tourism-related business activities; if your business needs them for another use case, contact your Teya account manager. Capture is API-only today; confirm before migrating pre-auth flows. See <a href="/online-payments/payment-actions">Payment Actions</a>.</p>
</div>

<!-- Testing -->
<div class="ig-subsection">
<h3>Testing and going live</h3>
<p>Test in staging with <code>developmentMode: true</code> and the cards on the <a href="/online-payments/testing">Testing</a> page — frictionless, challenge and declined scenarios are all there. If your development machine isn't publicly reachable, tunnel it so staging webhooks can reach you.</p>

<p><strong>Definition of done</strong></p>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span>Frictionless card: <code>onSuccess</code> fires, webhook received and verified, session <code>COMPLETED</code> / <code>SUCCESS</code></span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Challenge card: the challenge renders inline and the payment completes</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Declined card: error shown in the component, customer can retry, order not marked paid</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Abandoned or expired session: no webhook, nothing fulfilled, your code creates a fresh session</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Amounts: an ISK (and, if relevant, EUR) payment charges exactly the right amount in the Business Portal</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Full refund, then two partial refunds; a third exceeding the original fails</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Replaying the same webhook doesn't double-fulfil</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Integration recovers from a <code>401</code> by refreshing the token</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Pre-auth users: authorise, capture a partial amount, remainder released</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Apple Pay users: domain verified, button renders, test payment completes</span></div>
</div>

<p><strong>Go live</strong></p>
<ol>

<li>Create production credentials and register the production webhook in the Business Portal at <code>business.teya.com</code>; store the new public key.</li>

<li>Swap hosts by config (<code>.xyz</code> → <code>.com</code>) and remove <code>developmentMode</code>.</li>
<li>Process one small real payment end to end: payment → webhook → refund.</li>
<li>Run RPG in parallel for a day or two behind a flag; watch logs, webhook deliveries and the Business Portal.</li>
<li>Retire the legacy integration: delete the payment, MPI, tokenisation and webhook code paths. Teya retires the RPG access tokens for you; there is nothing to request.</li>
</ol>
</div>

<!-- Common errors -->
<div class="ig-subsection">
<h3>Common errors</h3>
<div style="overflow-x:auto;">
<table class="ig-comparison">
<thead>
<tr><th>Symptom</th><th>Likely cause</th><th>Fix</th></tr>
</thead>
<tbody>
<tr><td>Charged amount is 100× too big (ISK)</td><td>RPG's two-implied-decimals convention carried over</td><td>Divide legacy ISK amounts by 100</td></tr>
<tr><td>Staging session token rejected by the SDK</td><td><code>developmentMode: true</code> missing</td><td>Add it in staging, remove in production</td></tr>
<tr><td><code>401</code> on API calls</td><td>Access token expired — currently 15 min in production, not permanent like the RPG token</td><td>Refresh from <code>expires_in</code>; retry once on <code>401</code></td></tr>
<tr><td><code>403</code></td><td>Missing scope</td><td>Add the scope to the token request (Step 1)</td></tr>
<tr><td><code>409</code></td><td>Same <code>Idempotency-Key</code>, different body</td><td>Unique key per order attempt; reuse only for retries of the identical request</td></tr>
<tr><td><code>422</code> creating a session</td><td>Numeric currency code or non-integer amount</td><td>Alphabetic <code>currency</code>; integer <code>amount.value</code> in minor units</td></tr>
<tr><td>Webhook never arrives</td><td>URL not public, no valid certificate, auth on the endpoint, or non-<code>200</code> responses</td><td>Public HTTPS, no auth, return <code>200</code> after processing</td></tr>
<tr><td>Signature verification fails</td><td>Parsed body re-serialised, or the wrong key</td><td>Verify the raw body; use the key issued for this webhook in this environment</td></tr>
<tr><td>Payment succeeded but not in the Business Portal</td><td>Wrong environment</td><td>Staging payments are in <code>business.teya.xyz</code>, production in <code>business.teya.com</code></td></tr>
<tr><td>Waiting for a refund or capture webhook</td><td>Only successful customer payments send one</td><td>Read the outcome from the API response</td></tr>
</tbody>
</table>
</div>

```admonition
icon: fas fa-life-ring
title: Need help?
content: Your Teya account manager can provide a development store, confirm pre-authorisation availability and plan the stored-card migration. For integration questions, see <a href="/support">Support</a>.
```

</div>

<div class="ig-subsection">
<h3>Next steps</h3>
<ul>
<li><a href="/online-payments/migration/stored-cards">Stored cards &amp; recurring</a> - Migrate multi-use tokens once one-off payments are live</li>
<li><a href="/online-payments/embedded-components/checkout/get-started">Embedded Checkout</a> - Interactive quickstart in your language and framework</li>
<li><a href="/online-payments/webhooks">Webhooks</a> - Payload fields and signature verification</li>
<li><a href="/online-payments/testing">Testing</a> - Test cards and integration checklist</li>
<li><a href="/apis/online-payments/apis">API Reference</a> - Full API endpoint documentation</li>
</ul>
</div>

</div>
</div>
