Migrating from RPG to Embedded Components
For businesses whose website talks directly to RPG (Borgun Payment API). The first half is for the business owner; the second half is for your developer.
Who this guide is for
- Your code calls URLs containing
borgun.is,rpgapior/api/payment, authenticated with a Private Access Token. - You run 3D Secure yourself through
/api/mpi/v2/enrollmentand/api/mpi/v2/validation. - Your checkout page loads
BAPIjs/borgun-payment.jsto create single-use tokens.
securepay.borgun.is, payment URLs created in advance, and stored cards charged without the customer present each have their own guide.
Why migrate, and what you gain
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.
Your options
Checkout component Recommended — 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.
- Card component — a single card field inside your own form with your own pay button. See Embedded Components.
- Hosted Checkout — the customer is redirected to a Teya page and back. Least code, different customer experience. See Hosted Checkout.
- Direct API — 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 RPG capability reference.
What the migration involves
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.
Developer guide
How an RPG integration maps onto a checkout session and the Checkout component, with the one request you need at each step.
How it works: before and after
Today (RPG)
- Your form or
BAPIjscollects the card; card data or a single-use token reaches your server. - Your server runs 3D Secure: enrollment, ACS redirect,
TermURLcallback, validation,MdStatus. - Your server calls
POST /api/paymentwith theThreeDSecureblock and readsActionCode. - Optionally a
PaymentCreatewebhook arrives and you call backGET /api/events/{EventId}.
After (Embedded Components)
- Your server gets a short-lived access token and creates a checkout session; the response carries a session token.
- Your page initialises the SDK with that token and mounts the Checkout component. Card data goes into Teya-hosted fields, never into your DOM or server.
- The customer pays in place; 3D Secure runs inside the component.
onSuccessfires for the user experience; a signed webhook confirms the payment to your server and drives fulfilment.
The biggest change: the whole MPI flow and all card-data or single-use-token handling are deleted — the component replaces both.
Before you start
1. Credentials
Ask your Teya account manager for a development store. In the Staging Business Portal, select the store, go to Integrations and create ecommerce API credentials: a Client ID, a Client Secret (shown once) and your Store ID. Repeat at business.teya.com for production; never mix the two. This replaces your Private and Public Access Tokens. See Authenticate.
2. Environments
Staging is api.teya.xyz / id.teya.xyz / business.teya.xyz with developmentMode: true in the SDK; production is the same hosts on .com without the flag. Full table on Embedded Checkout.
3. Webhook
Teya sends a signed payment.succeeded.v1 event to a public HTTPS endpoint with no authentication on it.
Register the URL in the Business Portal next to your credentials; the portal issues a public key per webhook for verifying signatures.
Unlike RPG's PaymentCreate / Refund / Capture events, only successful customer payments produce a webhook today; refunds and captures you trigger return their outcome in the API response. See Webhooks.
4. Find the legacy code
Search for borgun.is, rpgapi, /api/payment, /api/token/single, /api/mpi, TransactionType, ActionCode, MdStatus, TermURL, MpiToken, BAPIjs, borgun-payment, PrivateAccessToken, BorgunRPGNet, RPGClient. 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.
5. Ground rules
What replaces what
The RPG capability reference maps each capability to its replacement. This table goes one level down: the calls and fields an RPG developer has to change.
| RPG today | New platform | What it means for you |
|---|---|---|
| Private Access Token (permanent, Basic auth) | client_id + client_secret → short-lived Bearer token | Cache and refresh from expires_in (currently 15 min in production, 24 h in staging). |
| Public Access Token (browser tokenisation) | Not needed — the session token comes from your backend per payment | Delete with the BAPIjs code. |
POST /api/payment, TransactionType: "Sale" | POST /v2/checkout/sessions, type: "SALE" + the component | The server sends intent only — amount, currency, reference. No card data, no 3DS block. |
POST /api/payment, TransactionType: "PreAuthorization" | type: "PRE_AUTHORISATION" on the same endpoint | See Pre-authorisations below. |
PUT /api/payment/{id}/capture (full only) | POST /v1/transactions/{id}/capture, {"amount": n} | Partial capture gained. Once per authorisation; the remainder is released. |
PUT /api/payment/{id}/cancel | POST /v3/refunds on the uncaptured payment | A refund before capture reverses the authorisation. No separate cancel. |
PUT /api/payment/{id}/refund + PartialAmount | POST /v3/refunds with transaction_id, amount — or the Business Portal | Same semantics: partial, multiple, total ≤ original. |
GET /api/payment/{id} | GET /v2/checkout/sessions/{session_id} | session_status + payment_status instead of TransactionStatus. |
POST /api/token/single + BAPIjs.getToken | Gone — the component's hosted fields do this | No token creation, lifetime management or disable calls. |
/api/mpi/v2/enrollment, /validation, TermURL, MdStatus, MpiToken | Gone — 3D Secure runs inside the component, challenge inline | Delete the whole flow. |
POST /api/payment with raw PAN | Direct API — restricted, PCI SAQ D evidence required | Use the component unless you have no alternative. |
POST /api/token/multi, VerifyCard | Card on File | Separate track — Stored cards & recurring. |
Amount — 2 implied decimals, including ISK (100 ISK = 10000) | amount.value — minor units per ISO 4217; ISK has no subunit (100 ISK = 100, €54.43 = 5443) | Divide RPG ISK amounts by 100. The most likely silent bug in an ISK migration; other currencies are unchanged. |
Currency — ISO numeric ("352") | amount.currency — ISO alphabetic ("ISK") | Lookup change. |
OrderId — exactly 12 chars, A–Z/0–9 | merchant_reference (free-form) + optional metadata | Write the legacy OrderId into metadata during transition if it helps reconciliation. |
TransactionDate | Not sent — Teya timestamps server-side | Delete. |
ActionCode ("000" = approved) + TransactionStatus | payment_status (SUCCESS / FAILED) on the session | No action-code tables; declines show in the component and the customer retries in place. |
DynamicDescriptors | Not exposed on checkout sessions | Tell your Teya contact before migrating if you rely on them. |
PaymentCreate webhook + GET /api/events/{EventId} | payment.succeeded.v1, RSA-signed (x-teya-signature) | Verify by signature instead of calling back. |
| B-Online | Business Portal | Credentials, webhooks, refunds, transaction visibility. |
Step-by-step
Steps 1–5 build the Checkout component end to end. Samples are cURL and minimal JavaScript; other languages are on the Embedded Checkout Get Started page.
Step 1 — Fetch an access token (server)
Exchange client_id + client_secret for a Bearer token. This replaces the permanent Private Access Token: cache the token per environment and refresh it before expires_in runs out or on a 401. Scopes for this guide: checkout/sessions/create, checkout/sessions/id/get, refunds/create, plus captures/create for pre-authorisations.
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"
{ "access_token": "eyJhbGciOiJSUzI1NiJ9...", "token_type": "Bearer", "expires_in": 900 }
Full reference: Authenticate.
Step 2 — Create a checkout session (server)
When the customer clicks Pay, your server describes the payment and gets back a session token for the page. This replaces POST /api/payment, but the body carries no card data and no 3DS block — only the intent.
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" }
}'
{
"session_id": "14411456-79c8-4d8c-8498-f01907093b83",
"session_token": "eyJhbGciOiJSUzI1NiJ9...",
"session_status": "ACTIVE"
}
Things to get right
amount.valueis an integer in minor units per ISO 4217: 100 ISK =100, not RPG's10000.Idempotency-Keyis unique per order attempt; retries with the same key can't create duplicate sessions.- Create the session when the customer clicks Pay, not when the cart loads — sessions expire.
- Return only
session_tokento the browser through a small endpoint of your own — never the access token or the Client Secret.
Full reference: API Reference.
Step 3 — Mount the Checkout component (frontend)
Install @teyaproduct/teya-blocks-js, initialise the SDK with the session token from your backend, and mount the component. This replaces your card form and BAPIjs.getToken: the fields are Teya-hosted, so card data never enters your DOM.
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');
Things to get right
- A staging session token only works with
developmentMode: true; a production token only works without it. Gate the flag on your own environment. - Apple Pay needs a one-off domain verification per domain in the Business Portal under Integrations > Apple Pay; card payments work without it.
- Where did 3D Secure go? Inside the component. Enrollment, the inline challenge, validation and
MdStatusare no longer your code.
Full reference: SDK Initialization and Embedded Checkout.
Variant: the Card component
For your own form layout and pay button, create a card element instead and call card.submitPayment() from your button; the session and everything after are the same. No built-in Apple Pay button. See Card Element.
Step 4 — Handle the result
onSuccess / onError 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 GET /api/payment/{id} and the "only 000 means approved" rule.
curl https://api.teya.xyz/v2/checkout/sessions/$SESSION_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"session_id": "14411456-79c8-4d8c-8498-f01907093b83",
"session_status": "COMPLETED",
"amount": { "value": 2490, "currency": "ISK" },
"type": "SALE",
"payment_status": "SUCCESS"
}
Things to get right
session_statusrunsACTIVE→PROCESSING→COMPLETEDorEXPIRED; trustpayment_statusonly onceCOMPLETED.- Store the
transaction_idfrom the webhook against the order — it is the key for refunds and receipts.
Full reference: API Reference.
Step 5 — Receive and verify the webhook (server)
Teya POSTs a signed payment.succeeded.v1 event to your registered URL when a payment succeeds — the equivalent of RPG's PaymentCreate, verified by signature instead of a call back to GET /api/events/{EventId}. Verify x-teya-signature (SHA256withRSA, Base64) over the raw request body with the public key issued for this webhook, before parsing anything.
{
"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"
}
}
Things to get right
- Be idempotent — the same event can arrive more than once. Return
200promptly and do slow work asynchronously. - Register the route before any JSON body parser so you verify the raw bytes; use the key for this webhook in this environment.
- Check your handler with the Webhook Signature Validator before go-live.
Full reference: Webhooks.
After the payment
Each operation returns its outcome in the synchronous API response — there is no webhook for refunds or captures today, so record the response.
Refunds
Replaces PUT /api/payment/{id}/refund, same rules: partial and multiple refunds, total ≤ original. 201 is processed; 202 is still processing — retry with the same Idempotency-Key. Staff can also refund from the Business Portal. Payments taken on RPG before cutover stay refundable through B-Online for a transition period.
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 }'
See Payment Actions.
Payment status and receipts
Status: GET /v2/checkout/sessions/{session_id} (Step 4) replaces GET /api/payment/{id}; the Business Portal covers day-to-day visibility. Receipts are new — POST /v1/transactions/{id}/receipts emails a Teya-generated receipt (scope transactions/id/receipts/create). See Payment Actions.
Pre-authorisations
Skip this if you only send TransactionType: "Sale". Create the session with type: "PRE_AUTHORISATION"; the component flow is unchanged and the funds are held (no DCC on pre-auth). Capture with POST /v1/transactions/{id}/capture and {"amount": n} — any amount up to the authorised total, but exactly once: 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 Payment Actions.
Testing and going live
Test in staging with developmentMode: true and the cards on the Testing 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.
Definition of done
onSuccess fires, webhook received and verified, session COMPLETED / SUCCESS401 by refreshing the tokenGo live
- Create production credentials and register the production webhook in the Business Portal at
business.teya.com; store the new public key. - Swap hosts by config (
.xyz→.com) and removedevelopmentMode. - Process one small real payment end to end: payment → webhook → refund.
- Run RPG in parallel for a day or two behind a flag; watch logs, webhook deliveries and the Business Portal.
- 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.
Common errors
| Symptom | Likely cause | Fix |
|---|---|---|
| Charged amount is 100× too big (ISK) | RPG's two-implied-decimals convention carried over | Divide legacy ISK amounts by 100 |
| Staging session token rejected by the SDK | developmentMode: true missing | Add it in staging, remove in production |
401 on API calls | Access token expired — currently 15 min in production, not permanent like the RPG token | Refresh from expires_in; retry once on 401 |
403 | Missing scope | Add the scope to the token request (Step 1) |
409 | Same Idempotency-Key, different body | Unique key per order attempt; reuse only for retries of the identical request |
422 creating a session | Numeric currency code or non-integer amount | Alphabetic currency; integer amount.value in minor units |
| Webhook never arrives | URL not public, no valid certificate, auth on the endpoint, or non-200 responses | Public HTTPS, no auth, return 200 after processing |
| Signature verification fails | Parsed body re-serialised, or the wrong key | Verify the raw body; use the key issued for this webhook in this environment |
| Payment succeeded but not in the Business Portal | Wrong environment | Staging payments are in business.teya.xyz, production in business.teya.com |
| Waiting for a refund or capture webhook | Only successful customer payments send one | Read the outcome from the API response |
Next steps
- Stored cards & recurring - Migrate multi-use tokens once one-off payments are live
- Embedded Checkout - Interactive quickstart in your language and framework
- Webhooks - Payload fields and signature verification
- Testing - Test cards and integration checklist
- API Reference - Full API endpoint documentation