# Teya online payments — migrating from RPG & SecurePay (partner edition) Source: https://docs.teya.com/partners/online-payments/migration/overview — generated 2026-10-21. ## Facts and rules for assistants - The legacy platform (SecurePay hosted page, RPG / Borgun Payment API, Ticket API, legacy tm_/991001 tokens) is retiring in early 2027; onboarding to it stops soon. - New platform hosts: staging api.teya.xyz / id.teya.xyz / business.teya.xyz; production api.teya.com / id.teya.com / business.teya.com. - Auth: OAuth client_credentials per store -> Bearer token; token lifetime currently 15 min production, 24 h staging; cache by expires_in, refresh on 401. - amount.value is an integer in minor units; ISK has no subunit (100 ISK = 100). - Session create: POST /v2/checkout/sessions (Idempotency-Key required); lookup GET /v2/checkout/sessions/{session_id}; type SALE | PRE_AUTHORISATION | ACCOUNT_VERIFICATION. - Confirmation: webhook payment.succeeded.v1 (success only; x-teya-signature = SHA256withRSA over raw body, verify with the per-webhook public key) or the session lookup. Never confirm from the browser redirect. - Refund POST /v3/refunds; capture POST /v1/transactions/{id}/capture once per authorisation; refund of an uncaptured payment reverses it (no cancel). - Direct API POST /v3/transactions/online: store_payment_method, initiator MERCHANT|CUSTOMER, recurring_payment_type, payment_method TOKEN or LEGACY_TOKEN; DELETE /v1/tokens/{id}. - Legacy fingerprints to search for: borgun.is, securepay, rpgapi, /api/payment, /api/mpi, /api/token, /api/events, merchantid, paymentgatewayid, checkhash, orderhash, returnurlsuccess(server), displaymode, skipreceiptpage, itemdescription_, BAPIjs, borgun-payment.js, tm_ tokens, 991001 PANs. - Do not invent endpoints or fields; when unsure, say so and point to https://docs.teya.com/apis/online-payments/apis. - When something is not answered by the documentation, draft a complete email to the merchant's or partner's Teya contact using the template on the 'Migrate with an AI assistant' page (who, where we are, precise issue with sanitised request/response, what was tried, the ask). ---

Migrating from RPG & SecurePay

This section is for partners and platforms that take payments on behalf of merchants through Teya's legacy online payments platform — RPG (Borgun Payment API), SecurePay, B-Gateway or the Ticket API — and need to move those merchants to the new platform. It explains what is changing, the order to do things in, and which guide applies to each of your integrations.

```admonition icon: fas fa-store title: Migrating your own business, not other merchants? content: If you built the integration for your own website, the Businesses section has the same guides written for a single store. link: /online-payments/migration/overview ```

What is changing

Teya's legacy online payment systems — SecurePay, the RPG payment API, the B-Gateway web service and the Ticket API — continue to work as normal today. Onboarding new merchants to them will stop soon, and they will be retired in early 2027. Every integration still on them needs to be moved to the new Teya online payments platform before then.

The new platform is live and processing payments across Teya's markets. It replaces per-request signatures with OAuth credentials, browser-side confirmations with signed webhooks, and self-built 3D Secure with authentication that is handled for you. Everything you build for one merchant works for all of them — only the credentials differ.

The migration in three stages

Migrating a merchant base is not one switch. Do it in this order, and each stage de-risks the next.

1 Build and test a fresh integration
2 Verify it with one merchant
3 Migrate existing merchants and stored cards

1. Build and test a fresh integration

Integrate with the new platform as if you were a new partner, using a staging partner account and development stores from your Teya partner contact. Nothing on the legacy platform changes while you do this, and you need no merchant credentials yet. Cover every flow your merchants use today — checkout, refunds, payment status, and stored-card charging if you offer it — and test each with test cards in staging.

  • Start from the partner overview to choose the integration: Embedded Components for most RPG and B-Gateway integrations, Hosted Checkout for redirect-style SecurePay flows, Pay By Link for Ticket API flows.
  • Use the migration guides below for the differences — what replaces what, and the traps a legacy integration falls into.

2. Verify it with one merchant

Before touching your existing base, prove the integration end to end in production with a single store: either onboard a new merchant through the registration flow, or migrate one existing merchant using the credentials route below. Run real payments, a refund, and confirm the webhook arrives and verifies. Keep that merchant's legacy integration available for a day or two in case you need to roll back.

3. Migrate existing merchants and stored cards

With one merchant live, the rest is repetition: obtain credentials for each store, switch its traffic, verify, and retire its legacy credentials. Stored cards and recurring payments are migrated last, on their own track, once every merchant's new-card flow is already on the new platform — see the stored cards and recurring guide.

```admonition icon: fas fa-robot title: Building with an AI coding assistant? content: Migrate with an AI assistant has ready-made prompts for stage 1 (inventory your legacy integration and build the new one), the per-store credentials step, stored cards, and raising questions with Teya — plus a single downloadable file containing this whole section for the assistant to read. Worth a look before you start; it can save your team a lot of time. ```

Credentials for existing merchants

On the new platform every store has its own OAuth client credentials, issued to you when the merchant is registered on your platform. For merchants you already serve, you don't want to ask each one to re-onboard:

Your merchants today useHow you get their new credentials
RPG (you hold each merchant's private key)Exchange the RPG key for a Client ID and Client Secret, one call per store. No merchant involvement.
SecurePay (you post to SecurePay with each merchant's gateway ID)Register each merchant through the registration flow — the merchant approves once from their Teya account. To migrate a large merchant base, talk to your Teya partner contact about the approach.
B-Gateway (you hold a username, password and MerchantID per merchant)Register each merchant through the registration flow. If you hold B-Gateway credentials for many merchants, talk to your Teya partner contact about the approach.
Your own Teya credentials only (a single legacy merchant account)You are a merchant for this purpose — see the Businesses migration section.
```admonition icon: fas fa-key title: One set of credentials per store content: Credentials are never shared across merchants. Store each merchant's client_id, client_secret, store_id and webhook public key against that merchant, exactly as you do for merchants registered through the standard flow. ```

Which guide applies to you

Most platforms have more than one legacy integration. Take each row that applies and follow its guide; they share the same structure and the same credentials.

Legacy integrationTypical signs in your codeMigrate toGuide
RPG payment APICalls to /api/payment, /api/mpi/v2/…; a private key per merchant; your own 3D Secure handlingEmbedded Components (or the direct API where components have no equivalent)RPG → Embedded Components
B-Gateway web serviceSOAP/XML requests such as getAuthorization to gateway01.borgun.is; a username, password and MerchantID per merchant; card numbers reach your serversEmbedded Components (Hosted Checkout as an alternative)B-Gateway → Embedded Components
SecurePay redirectA form posted to securepay.borgun.is with a checkhash; customers pay on a Teya-hosted pageHosted CheckoutSecurePay → Hosted Checkout
SecurePay embeddeddisplaymode=iframe; the payment form sits inside your checkout pageEmbedded ComponentsSecurePay → Embedded Components
Ticket APIPayment URLs created in advance and sent to customersPay By LinkTicket API → Payment Links
Stored cards / recurringMulti-use tokens (tm_…) or virtual card numbers (991001…) charged without the customer presentCard on FileStored cards & recurring
```admonition icon: fas fa-plug title: Do you maintain a plugin for an ecommerce platform? content: If your merchants install your plugin (WooCommerce, OpenCart, Magento, PrestaShop …) rather than integrating with you as a service, the same guides apply to the plugin's code, and each merchant's credentials arrive through the registration flow when they connect. Tell your Teya partner contact which platform you build for so a plugin_type can be assigned. ```

Migration guides

Each guide starts with a short, non-technical summary you can forward to a merchant or a product owner, followed by the developer guide: how it works before and after, a table of what replaces what, and the migration steps with the one request you need at each.

RPG → Embedded Components

For direct RPG API integrations

Replace /api/payment and your MPI calls with a checkout session and a component; 3D Secure and Apple Pay are handled for you.

B-Gateway → Embedded Components

For SOAP/XML B-Gateway integrations

Replace getAuthorization, your card form and any 3D Secure flow with a checkout session and the Checkout component; save cards for subscriptions and charge them by API.

SecurePay → Hosted Checkout

For redirect-style SecurePay integrations

The like-for-like path: one server-side call replaces the signed form post, and the customer still pays on a Teya page.

SecurePay → Embedded Components

For iframe integrations, or to upgrade the checkout

Keep the payment form on your page. Includes a comparison to help you choose between Hosted Checkout and Embedded Components.

Ticket API → Payment Links

For pre-created payment URLs

Create links by API or in the Business Portal, get paid by webhook.

Stored cards & recurring

Cross-path: do this last

Stop creating legacy tokens, run both platforms side by side, then let each legacy token convert itself on first use. No customer re-enters a card.

Reference: what replaces what

Whichever path you take

Test first. Every guide rehearses the full flow in staging with test cards before any real traffic moves.
Nothing switches until you switch it. Legacy integrations keep working in parallel while you build, and for a safety window after each merchant moves.
Migrate one-off payments before stored cards. The stored-card migration assumes each merchant's new-card flow is already on the new platform.
Start early. The work is the same in early 2027 as it is now; doing it now means doing it calmly, with support. ```admonition icon: fas fa-life-ring title: Need help? content: Your Teya partner contact can provision staging accounts and development stores, agree the credentials route for your merchant base, and plan the stored-card import. For integration questions, see Support. ```

Next steps

---

Migrating from SecurePay

This page is for partners and merchants who post payments to SecurePay, Teya's legacy hosted payment page, and are moving to the new online payments platform. It walks through the integration options and maps every SecurePay parameter to its replacement.

```admonition icon: fas fa-key title: Taking payments on behalf of merchants? content: Every integration below authenticates with new ecommerce credentials, issued per store. Register each merchant through the registration flow; for larger merchant bases, agree a batch approach with your Teya partner contact. See the migration overview. ```

Choose your integration

SecurePay was used in two ways, and each has a natural successor on the new platform:

Hosted CheckoutEmbedded Components
Best forSecurePay redirect integrations — the like-for-like pathSecurePay iframe integrations (displaymode=iframe), or anyone who wants checkout to stay on their site
Customer experienceRedirected to a Teya-hosted page, then back to your site — as todayPays without leaving your site; the components render inside your checkout
Integration effortLow — one server-side call and a redirectMedium — a server-side call plus a small frontend component
3D Secure, Apple Pay, DCCHandled for youHandled for you
PCI scopeSAQ ASAQ A (components are hosted in iframes)
GuideSecurePay → Hosted CheckoutSecurePay → Embedded Components

Two related legacy services have their own successors: pre-created payment URLs (the Ticket API) become Payment Links, and stored cards or recurring payments follow the stored cards & recurring guide.

What replaces what

The biggest change: SecurePay let your site talk to Teya straight from the customer's browser with a signed form. On the new platform your server makes one API call first — creating a checkout session — and everything you used to put in the form goes in that call's JSON body. The table covers the SecurePay form parameters and where each one lives now.

SecurePayNew platformWhat it means for you
Authentication
merchantid + paymentgatewayid; secret key + HMAC checkhash on every request
store_id (one UUID) + OAuth client_id/client_secret exchanged for a short-lived bearer tokenNo per-request signing. Mint a token, cache it for its expires_in, refresh on expiry. See Authenticate.
Starting a payment
HTML form posted from the browser to the SecurePay URL
POST /v2/checkout/sessions from your server, then redirect to session_url (Hosted Checkout) or mount a component with session_token (Embedded Components)Requires a backend. See Hosted Checkout or Embedded checkout.
amount as a decimal string, e.g. 54,43amount.value as an integer in minor units, e.g. 5443Multiply by 100 for EUR, GBP, CZK, HUF; ISK has no decimals — 100 ISK is 100.
currencyamount.currencySame ISO 4217 alphabetic codes.
orderid (12 characters), referencemerchant_reference (free-form) + metadata (key/value)Keep writing your legacy order ID into metadata during the transition if it helps reconciliation.
languagelanguage (BCP 47, e.g. is-IS, cs-CZ)Optional — auto-detected from the customer's browser.
buyername, buyeremailcustomer.name, customer.email, customer.phone_numberPrefills the checkout form.
pagetype=1 (collect address/phone/email)required_customer_fields, e.g. ["SHIPPING_ADDRESS", "PHONE", "EMAIL"]Explicit per field; returned in the session lookup.
itemdescription_n, itemcount_n, itemunitamount_nline_items[].description, .quantity, .unit_priceA JSON array; unit price in minor units; shown to the customer.
returnurlsuccesssuccess_urlA browser redirect after payment — a UX affordance, never payment confirmation.
returnurlcancelcancel_urlRendered as a "Back" option on the checkout.
returnurlerrorfailure_url (terminal errors only)Declined cards stay on the checkout so the customer can retry with another card.
Payment confirmation
returnurlsuccessserver POST with orderhash
Webhook payment.succeeded.v1, RSA-signed, or GET /v2/checkout/sessions/{session_id}Same purpose, verified with a public key instead of a shared-secret hash. Never trust the browser redirect to confirm success.
skipreceiptpage=1post_success_payment: "REDIRECT"Go straight to your own success page instead of Teya's receipt page.
merchantlogo, page textsBusiness Portal settings (logo, checkout and receipt messages, legal URLs)Configured once per store, not sent per payment. See Customize.
displaymode=iframeEmbedded ComponentsHosted Checkout does not support iframe embedding.
Verified by Visa / Mastercard SecureCode setupAutomatic 3D SecureNo code, no configuration.
Dynamic Currency ConversionAutomaticOffered when the card currency differs from the transaction currency.
(no equivalent)Pre-authorisation and capture: type: "PRE_AUTHORISATION", then POST /v1/transactions/{id}/captureNew capability. See Payment Actions.
Refunds via portal or manual processPOST /v3/refunds, or the Business Portal with no codePartial and multiple refunds; a refund on an uncaptured payment reverses it. See Payment Actions.
Ticket API (pre-created payment URLs)Payment LinksSee Ticket API → Payment Links.
SecurePay tokenisation / recurringCard on FileSee Migrating stored cards & recurring payments.
```admonition icon: fas fa-search title: Finding the SecurePay code in your codebase content: Search for borgun.is, securepay, checkhash, orderhash, merchantid, paymentgatewayid, returnurlsuccess, returnurlsuccessserver, itemdescription_, skipreceiptpage, displaymode. Each hit is a line on your migration to-do list — and displaymode=iframe tells you which guide to follow. ```

Next steps

---

Migrating from RPG

This guide is for partners and merchants who are currently integrated with RPG (Borgun Payment API) and are moving to the new Teya ecommerce stack. It walks through the integration options available and maps each RPG capability to its replacement.

```admonition icon: fas fa-key title: Are you a partner managing credentials on merchants' behalf? content: Every integration below authenticates with the new ecommerce credentials. You can exchange each merchant's RPG key for a new pair — see Credentials Migration. ```

Choose your integration

There are two ways to take payments on the new stack: Embedded Components and direct API integration. We highly recommend Embedded Components.

Embedded Components (highly recommended)Direct API
Integration effortMedium — drop pre-built UI components into your checkout pageHigh — you build the payment UI and orchestrate every call yourself
Look and feelSeamlessly blends into your website, with customisable stylingFully yours to build
3D SecureHandled for youHandled for you on the transaction flow
Fraud protectionEnhanced fraud protection built in — the components collect device and behaviour signals that strengthen risk decisions on every paymentStandard fraud checks only

With Embedded Components you keep full control of your checkout page while the components handle card capture, 3D Secure and Apple Pay for you. See the Embedded Components overview to explore what they look like.

Use the direct API only where components have no equivalent, such as collecting merchant-initiated payments on stored cards.

What replaces what

The table below covers the RPG operations documented in the RPG API reference and where each one lives on the new stack.

RPG capabilityRPG endpointsNew integration
Authentication HTTP Basic with a private key Authenticate — OAuth client credentials, issued per store, exchanged for a bearer access token.
Card payments (incl. 3D Secure) POST /api/payment
POST /api/mpi/v2/enrollment
POST /api/mpi/v2/validation
Embedded checkout or card components — 3D Secure is handled for you, with no separate enrollment or validation calls.
Tokenisation POST /api/token/single
POST /api/token/multi
Card on File — save a customer's payment method during a payment or with a zero-amount account verification.
Apple Pay POST /api/payment with a wallet token Embedded Apple Pay component — Apple Pay support without hosting your own wallet integration.
MIT / collection payments POST /api/payment with a multi-use token Charge a Saved Payment Method — a merchant-initiated transaction on a stored card via the direct API.
Refunds, captures & cancellations PUT /api/payment/{id}/refund
PUT /api/payment/{id}/capture
PUT /api/payment/{id}/cancel
Payment Actions — capture with POST /v1/transactions/{id}/capture and refund with POST /v3/refunds. There is no separate cancel: a refund on an uncaptured payment reverses the authorisation, replacing the RPG cancel.
Payment notifications GET /api/events/{eventId} Webhooks — signed notifications pushed to your platform in real time.
```admonition icon: fas fa-id-card title: RPG tokens: don't re-collect cards content: Tokens minted on RPG cannot be charged on the new platform as they are — but your customers never need to re-enter a card. Teya imports your stored cards and each legacy token upgrades itself on its first charge. See Migrating stored cards & recurring payments. ```

Next steps

---

Migrating from B-Gateway

This page is for businesses and partners whose systems call B-Gateway directly (the Borgun payment gateway web service, SOAP/XML; also written BGateway, known in Iceland as Greiðslugátt), and are moving to the new Teya ecommerce platform. It lists the integration options and maps every B-Gateway operation and field to its replacement. For the step-by-step migration, see the B-Gateway migration guide.

```admonition icon: fas fa-key title: New credentials content: B-Gateway authenticates every call with a username and password, and identifies the merchant by Processor and MerchantID. The new platform uses OAuth credentials per merchant store, issued to you through the registration flow — see Authenticate. If you hold B-Gateway credentials for many merchants, talk to your Teya partner contact about the approach. ```

Choose your integration

On B-Gateway your own systems collect the card number (or hold a virtual card number) and send it to Teya in one call, with 3D Secure values if you run 3D Secure yourself; the same call serves payments with and without the customer. On the new platform the two are separate. When the customer is paying, Teya collects the card and runs 3D Secure for you, either on a Teya page or inside your own checkout page, and your servers only describe the payment and receive the result. When the customer is not there, your server charges a card they saved earlier, as a merchant-initiated payment.

Hosted CheckoutEmbedded ComponentsDirect API
Customer experienceRedirected to a Teya payment page and back to your siteStays on your page; Teya-hosted card fields sit inside your checkoutYour own card form
Integration effortLowest: one server call and a redirectMedium: one server call plus a component on your pageHigh: you build the form, the 3D Secure challenge display and every call
Card data on your systemsNoneNoneYes: PCI DSS (SAQ D) obligations stay with you
3D SecureHandled for youHandled for youRun by Teya; you display the challenge. Results from your own MPI are not accepted
Saving cards for subscriptionsYes: save_payment_method on the sessionYes: save_payment_method on the sessionYes: store_payment_method on the transaction
When to choose itYou want the least code, or your customers trust a payment-provider pageYou want the checkout to stay on your site, with Apple Pay and currency conversion built inYou want to keep your own card form and the PCI DSS obligations that come with it; check with Teya first, especially if you run your own or a third-party MPI today

Charging a saved card later, without the customer present (subscriptions, instalments), always uses the direct API with a token: POST /v3/transactions/online. No card data is involved, so it needs no PCI evidence. See Charge a Saved Payment Method.

What replaces what

Operations and fields from the B-Gateway developer documentation, and where each one lives on the new platform.

Payments and payment actions

B-GatewayNew platformNotes
Authentication: username + password on every call; Processor, MerchantID, TerminalID (1 ecommerce, 2 ecommerce with 3D Secure, 3 recurring, 4 mail/telephone order) in the XMLOAuth client_id + client_secret per store, exchanged for a short-lived Bearer token; store_id in the requestSee Authenticate. Cache the token for its expires_in.
getAuthorization, TransType 1 (purchase), AuthCode emptyCheckout session POST /v2/checkout/sessions, type: "SALE", then Hosted Checkout or the Checkout componentThe server sends only the intent: amount, currency, reference. No card number, expiry, CVC or 3D Secure values.
getAuthorization, TransType 5 (pre-authorisation)Checkout session with type: "PRE_AUTHORISATION"See Payment Actions.
getAuthorization, TransType 1 with the AuthCode of a pre-authorisation (capture, incl. a lower amount)POST /v1/transactions/{id}/capture with {"amount": n}Any amount up to the authorised total, once per authorisation; the remainder is released.
Zero-amount TransType 5 to verify a card (recurring set-up)Checkout session with type: "ACCOUNT_VERIFICATION", amount.value: 0 and save_payment_method: trueVerifies and saves the card without charging it. Without save_payment_method, nothing is saved.
getAuthorization, TransType 3 (refund), referencing the original DateAndTime + RRNPOST /v3/refunds with transaction_id and amount, or a refund from the Business PortalSame rules: partial and multiple refunds, total ≤ original. The reference is the new-platform transaction_id, not a date and RRN.
getAuthorization, TransType 4 (partial reversal, same day)No direct equivalentTo charge less than you authorised, pre-authorise and capture the final amount. Otherwise, refund the difference.
cancelAuthorizationPOST /v3/refunds on the uncaptured paymentA refund before capture reverses the authorisation. There is no separate cancel and no "open batch" condition.
get3DSAuthentication, or your own / a third-party MPI, then CAVV, UCAF, XID, SecurityLevelInd on the authorisationBuilt into Hosted Checkout and the Embedded ComponentsDelete the whole 3D Secure flow: MerchantReturnURL, PaRes/CRes handling, the MPI connection. With the direct API, Teya returns a challenge for you to display.
Authorisation without 3D Secure valuesCustomer-present payments always go through 3D SecurePayments without the customer are made on a saved card, declared as merchant-initiated (below). Sort your call sites into the two kinds first; see the guide.
ActionCode (000 = approved) and the gateway codes 904, 909, 953payment_status (SUCCESS / FAILED) on the session; status + status_reason on direct-API transactions; HTTP status codes for request errorsNo action-code tables. Declines are shown in the checkout and the customer can retry.
Unknown result after a timeout: poll getTransactionListRetry with the same Idempotency-Key, or look the session up: GET /v2/checkout/sessions/{session_id}A retry with the same key can't create a second payment. The payment.succeeded.v1 webhook confirms every successful payment.
Track1 / Track2 (card read on a device), TerminalID 4 (mail or telephone order), sales keyed into B-OnlineNot part of online paymentsCard-present, telephone and keyed payments are not covered by this migration. Talk to your Teya partner contact before migrating if you use them.

Stored cards and recurring payments

B-GatewayNew platformNotes
getVirtualCard: card number in, virtual card number (16 digits, 9910…) outsave_payment_method: true on the checkout session; the token arrives in the payment.succeeded.v1 webhook as token_response.token_idThe card is saved inside the customer's payment or a zero-amount verification. No separate call, and no card number on your side. See Card on File.
getAuthorization with a virtual card number, without the customer (subscriptions, instalments)POST /v3/transactions/online with initiator: "MERCHANT" and payment_method.type: "TOKEN" + recurring_payment_typeNo 3D Secure, immediate final result. See Charge a Saved Payment Method.
TerminalID 3 (recurring) / sending no CVC and no 3D Secure valuesinitiator (MERCHANT / CUSTOMER) + recurring_payment_type (RECURRING / INSTALLMENT / CARD_ON_FILE)You declare what kind of charge it is. The platform no longer infers it from missing fields.
lifecycleID sent back with the virtual card numberNot sentThe platform keeps the card scheme's reference to the first payment against the token and attaches it to every later charge.
Virtual card numbers you already holdConverted on first use: payment method LEGACY_TOKEN after Teya imports your stored cardsCustomers never re-enter a card. Do this after your one-off payments are live. See Stored cards & recurring.
Card numbers you store on your own systems (never passed through getVirtualCard)No importTalk to your Teya partner contact before planning the move. Only values issued by the legacy platform can be converted.

Batches, reporting and other operations

B-GatewayNew platformNotes
Batches: Batch and Transaction numbers in every response; automatic close at midnight GMT; newBatch to close earlierNo batchesSettlement is automatic. Delete batch handling; key your records on transaction_id.
Batch authorisations and processing results (many transactions in one request)No batchesCharge each saved card with its own POST /v3/transactions/online request. Contact Teya support if your volumes make this a concern.
getTransactionListOne payment: GET /v2/checkout/sessions/{session_id}. All payments: the Business PortalStore the transaction_id from the webhook against each order.
Settlement list, batches and transactions in settlement, chargebacks; disputed and fraud transaction listsThe Business PortalSettlement itself is unchanged: same merchant account, same payout timing and statements.
Other features: sendDetailData (invoice detail), passenger itinerary data, Dynamic Descriptors (MerchantName, MerchantCity, …)Depends on your caseContact Teya support with your specific case before migrating, so we can check it with you. Order lines can be sent with the session as line_items.
Receipts (none on B-Gateway)POST /v1/transactions/{id}/receiptsNew: emails a Teya-generated receipt. See Payment Actions.
B-Online for refunds and cancellationsThe Business Portal and the Teya app, for payments taken on the new platformPayments taken on B-Gateway before you switch are refunded in B-Online, as today.

Fields and formats

B-GatewayNew platformNotes
TrAmount: two implied decimals for every currency, including ISK (100 ISK = 10000); TrCurrencyExponent on 3D Secure requestsamount.value: an integer in minor units per ISO 4217. ISK has no subunit (100 ISK = 100; €54.43 = 5443)If you charge in ISK, divide B-Gateway amounts by 100: the most likely silent bug in an ISK migration. Other currencies are unchanged (€49.90 is 4990 on both).
TrCurrency: ISO numeric (352, 978)amount.currency: ISO alphabetic ("ISK", "EUR")Lookup change.
RRN: exactly 12 characters, your referencemerchant_reference (free-form) + optional metadataWrite the legacy RRN or order number into metadata during the transition if your back office keys on it.
DateAndTimeNot sentTeya timestamps payments server-side.
PAN, ExpDate, CVC2Not sentEntered by the customer on the Teya page or in the Teya-hosted fields.
Version (1000), XML request and responseJSON over HTTPS (REST)There is no server-side SDK; plain HTTPS on the server, the JavaScript SDK in the browser for Embedded Components.

Next steps

---

Migrating from SecurePay to Hosted Checkout

For partners and platforms that redirect the customers of the merchants they serve to a SecurePay payment page. One server-side call replaces the signed form post; customers keep paying on a Teya-hosted page.

Who this guide is for

  • Your platform sends merchants' customers to securepay.borgun.is (or test.borgun.is/SecurePay) to pay, and they come back to the merchant's site afterwards.
  • The integration posts an HTML form signed with a checkhash and confirms payment from an orderhash.
  • The form is not embedded in your own page (displaymode=iframe) — Hosted Checkout does not support iframes.
```admonition icon: fas fa-question-circle title: Not you? content: Embedded form, Ticket API, stored cards or recurring payments, or you sell through a partner platform — a different guide applies. link: /partners/online-payments/migration/overview ```

Why migrate, and what you gain

SecurePay 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.

More completed payments — 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
The same experience for customers — click Pay, enter a card on a Teya page, come back
Payment confirmations that can't get lost — every successful payment is confirmed server-to-server with a signed webhook, whether or not the customer makes it back
Card retries without losing the sale — a declined card keeps the customer on the payment page
3D Secure and currency conversion built in — nothing to build or configure
Refunds without a developer — in the Business Portal, or by API
Pre-authorisations — holds with later, including partial, capture

Your options

Hosted Checkout Recommended — the like-for-like path for a SecurePay redirect: lowest effort, same customer experience.

  • Embedded Components — worth a look before you decide, even if you are happy with a redirect today. Teya's secure card fields sit inside your own checkout page, so the customer completes the whole journey on your site, in your branding, with Apple Pay on the page and no hand-off to another domain; checkouts like this typically convert better. It is somewhat more frontend work than Hosted Checkout, and the only path if you used displaymode=iframe. The guide opens with a side-by-side comparison to help you choose.
  • Payment Links — the successor to the Ticket API.
  • Stored cards & recurring — migrate last, after one-off payments are live.

What the migration involves

1 Credentials
2 Build
3 Test in staging
4 Go live
5 Retire SecurePay

A small development project — days including testing, not weeks. A plain-HTML site with no backend sits at the upper end: the new platform needs one small server-side component (Step 1 below).

```admonition icon: fas fa-paper-plane title: What to send your developers content: (1) Ask your Teya partner contact for a staging partner account and development store. (2) Forward this page. (3) Ask them to confirm whether any merchants use displaymode=iframe, the Ticket API or recurring payments — those have their own guides. ```

Developer guide

What replaces each SecurePay form parameter, and the one request you need at each step.

How it works: before and after

Today (SecurePay)

  1. The customer clicks Pay; the browser posts a checkhash-signed form straight to SecurePay.
  2. The customer pays on the SecurePay page.
  3. SecurePay redirects to returnurlsuccess and posts an orderhash to returnurlsuccessserver.

After (Hosted Checkout)

  1. The customer clicks Pay; your server gets an access token and creates a checkout session, receiving a session_id and session_url.
  2. You redirect the customer to the session_url; they pay there, 3D Secure included.
  3. Teya redirects to your success_url (or cancel_url) and sends a signed webhook to your server.
  4. Your server verifies the webhook (or looks the session up) and fulfils the order.

The biggest change: the browser no longer talks to Teya directly — your server makes one API call first, and confirmation comes from the server side, never the redirect.

Before you start

1. Credentials

Credentials are issued per store through the merchant registration flow: the merchant approves once from their Teya account, and Teya posts store_id, client_id, client_secret and payment_webhook_public_key to your registration webhook. Develop against a staging partner account and development store from your Teya partner contact; staging and production credentials never mix.

SecurePay equivalent: merchantid + paymentgatewayid become the Store ID; the secret key becomes the OAuth client credentials.

2. Environments

Staging is api.teya.xyz, id.teya.xyz and business.teya.xyz; production is the same hosts on .com. Full table: Hosted Checkout Get Started.

3. Webhook

Teya sends a signed webhook when a payment succeeds. Pass a public HTTPS endpoint with no authentication as paymentsWebhook in the registration URL; its public key arrives as payment_webhook_public_key in the registration callback, per webhook and per environment.

SecurePay equivalent: returnurlsuccessserver and its orderhash HMAC — same purpose, verified with an RSA signature instead. The webhook fires only for successful payments (payment.succeeded.v1); failures are covered by the session lookup in Step 5. See Webhooks.

4. Find the SecurePay code

Search your codebase for borgun.is, securepay, checkhash, orderhash, merchantid, paymentgatewayid, returnurlsuccess, returnurlsuccessserver, returnurlcancel, returnurlerror, itemdescription_, itemcount_, itemunitamount_, itemamount_, skipreceiptpage, merchantlogo, pagetype, displaymode. Each hit is a line on your to-do list. Found displaymode=iframe? Use SecurePay → Embedded Components instead.

5. Ground rules

Test in staging first — payment, webhook and refund end to end
Keep SecurePay live in parallel for a day or two after you switch
Never expose the Client Secret in browser code or repositories
Never confirm a payment from the redirect — the webhook or session lookup is the source of truth

What replaces what

SecurePayHosted CheckoutWhat it means for you
merchantid + paymentgatewayidstore_idOne UUID replaces both.
Secret key + HMAC checkhash per requestclient_id + client_secret → OAuth Bearer tokenNo per-request signing. Cache the token and refresh from expires_in (currently 15 minutes in production, 24 hours in staging).
HTML form posted from the browserPOST /v2/checkout/sessions from your server, then redirect to session_urlThe biggest change. Needs a backend (Step 1).
amount as a decimal string (54,43)amount.value, integer in minor units (5443)×100 for EUR, GBP, USD, CZK, HUF. ISK has no subunit: 100 ISK is 100.
currencyamount.currencySame ISO 4217 codes. HRK is gone; Croatia uses EUR.
orderid (12 alphanumeric)merchant_reference + optional metadataFree-form. Keep the legacy orderid in metadata during transition.
referencemetadata keysUp to 10 key-value pairs.
language (IS, EN, CZ…)language, BCP 47 (is-IS, cs-CZ)Optional; browser language if omitted.
buyername, buyeremailcustomer.name, .email, .phone_numberPrefill only; they don't force collection.
pagetype=1required_customer_fields: ["SHIPPING_ADDRESS", "PHONE", "EMAIL"]Explicit per field; returned in the session lookup.
itemdescription_0, itemcount_0, itemunitamount_0line_items[].description, .quantity, .unit_priceJSON array; unit price in minor units. Shown on the payment page.
returnurlsuccesssuccess_urlStill a browser redirect; never a payment confirmation.
returnurlcancelcancel_urlRendered as a Back option on the checkout.
returnurlerrorfailure_urlTerminal errors only. Declined cards stay on the checkout to retry.
returnurlsuccessserver + orderhash POSTWebhook payment.succeeded.v1, RSA-signed (x-teya-signature)Verify the signature, then fulfil. Success-only.
Reading orderhash on the success returnSigned webhook, or GET /v2/checkout/sessions/{session_id}Never trust the redirect to confirm success.
skipreceiptpage=1post_success_payment: "REDIRECT"Straight to your success page. SHOW_SUCCESS_PAGE shows Teya's receipt first.
merchantlogoBusiness Portal settingLogo, checkout and receipt messages, privacy and T&C URLs are configured once — see Customize.
displaymode=iframeNot supportedUse Embedded Components.
Verified by Visa / SecureCode setupAutomatic 3D SecureNo code, no configuration.
Dynamic Currency ConversionAutomaticOffered when the card currency differs from the transaction currency.
(no equivalent)Pre-authorisation + captureNew. Session type: "PRE_AUTHORISATION"; see After the payment.
Ticket APIPayment Links (POST /v2/payment-links)See Ticket API → Payment Links.
Recurring / stored cardsCard on FileOne-off payments first, then Stored cards & recurring.
Refunds by manual processPOST /v3/refunds, or the Business PortalFull, partial and multiple refunds; no code for Portal refunds.

Step-by-step

Requests are cURL against staging. Other languages: see Hosted Checkout Get Started.

Step 1. Add a backend (if you don't have one)

SecurePay worked from a plain HTML page because the browser posted a signed form. Hosted Checkout uses OAuth credentials that must never reach the browser, so token fetching, session creation and webhook handling run server-side. If you already have a backend, skip this step. If your site is static, a single endpoint is enough — a PHP file on your host, or a serverless function.

Step 2. Get an access token

Exchange client_id and client_secret for a Bearer token. This replaces the per-request checkhash: cache the token, derive the TTL from expires_in, refresh before expiry or on any 401.

```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 } ```

Things to get right: one cache per environment; don't fetch a token per payment; add captures/create if you use pre-authorisation. Full reference: Hosted Checkout Get Started.

Step 3. Create a checkout session

When the customer clicks Pay, your server creates a session. This replaces the SecurePay form: every parameter you put in the form goes in this JSON body.

```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": 5443, "currency": "EUR" }, "merchant_reference": "ORDER-2026-00042", "line_items": [ { "description": "Widget Pro", "quantity": 2, "unit_price": 2500 }, { "description": "Delivery", "quantity": 1, "unit_price": 443 } ], "customer": { "name": "Jan Novák", "email": "jan@example.com" }, "success_url": "https://yourstore.com/order/success", "cancel_url": "https://yourstore.com/cart", "metadata": { "legacy_orderid": "ORDER1230001" } }' ``` ```json { "session_id": "14411456-79c8-4d8c-8498-f01907093b83", "session_url": "https://checkout.teya.com/session/14411456-79c8-4d8c-8498-f01907093b83", "session_status": "ACTIVE" } ```

Things to get right:

  • amount.value is an integer in minor units: €54.43 is 5443; 100 ISK is 100. The most common bug in SecurePay migrations.
  • Idempotency-Key is required — unique per order attempt; reuse it only to retry the identical request.
  • Sessions expire after 15 minutes: create one at the Pay click, not on cart load.
  • Store the session_id against the order before redirecting.

Full reference: Hosted Checkout Get Started, API Reference.

Step 4. Redirect the customer

Send the browser to the session_url exactly as returned. As with SecurePay, the customer enters their card on Teya's page, not yours.

Step 5. Handle the return and look up the session

The customer lands on success_url, cancel_url, or failure_url on a terminal error. Unlike SecurePay, the redirect carries no payment data. Fulfil from the webhook (Step 6) and use the lookup to cover the gaps: a customer on your success page before the webhook arrives, or a failed payment, which produces no webhook.

```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", "payment_status": "SUCCESS", "type": "SALE", "amount": { "value": 5443, "currency": "EUR" }, "merchant_reference": "ORDER-2026-00042", "transaction_id": "tr_72302a4d-b2f9-4663-93f8-d9260e5370c8" } ```

Things to get right: session_status runs ACTIVE → PROCESSING → COMPLETED or EXPIRED; trust payment_status (NONE, SUCCESS, FAILED) only once COMPLETED. Store the transaction_id for refunds and receipts. Completion logic that runs only on the success page drops orders from customers who close the browser after paying. Full reference: API Reference.

Step 6. Receive and verify the webhook

Teya POSTs a payment.succeeded.v1 event to your webhook URL when a payment succeeds — the equivalent of returnurlsuccessserver, verified by RSA signature instead of an orderhash HMAC, and delivered whether or not the customer returns.

  • Verify the x-teya-signature header (SHA256withRSA, Base64) against the webhook's public key over the entire raw body, before parsing.
  • Match the order on data.session_id or data.merchant_reference; store data.transaction_id.
  • Be idempotent — the same event can arrive twice. Return 200 promptly; do slow work asynchronously.
  • Test with the Webhook Signature Validator before go-live.

Full reference, including the payload: Webhooks.

After the payment

Refunds, captures and receipts return their outcome in the synchronous API response; there is no webhook for them. All are visible in the Business Portal.

Refunds

On SecurePay, refunds were a manual process. Now: POST /v3/refunds with the transaction_id and an amount in minor units (scope refunds/create). Partial and multiple refunds are allowed up to the original amount; 201 is processed, 202 is still processing — retry with the same Idempotency-Key. Staff can also refund in the Business Portal with no code. See Payment actions.

Payment status

Use the session lookup from Step 5 for order-level status and the Business Portal for reporting. Keep the transaction_id on every order.

Digital receipts

New: POST /v1/transactions/{transaction_id}/receipts (scope transactions/id/receipts/create) emails a Teya-generated receipt. See Payment actions.

Pre-authorisations

Create the session with "type": "PRE_AUTHORISATION"; the flow is otherwise identical, and DCC is not offered on holds. Capture with POST /v1/transactions/{transaction_id}/capture (scope captures/create) for any amount up to the authorised total, once per authorisation; the remainder is released. To release a hold early, refund the uncaptured payment — it reverses the authorisation. 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. See Payment actions.

Testing and going live

Do all of this in staging with the cards on the Testing page. If your development environment isn't publicly reachable, tunnel it so staging webhooks can reach you.

Definition of done

Happy path — webhook received and verified; session COMPLETED/SUCCESS; order fulfilled once
3D Secure challenge — completes; customer returns to success_url
Declined card — customer retries on the payment page; order not marked paid
Cancel — lands on cancel_url; no webhook; not fulfilled
Abandon after paying — webhook still arrives; order fulfilled without the customer returning
Session expiry — after 15 minutes the customer sees the expiry page; your code creates a fresh session
Amounts — your main currency charges exactly the right amount (check the Portal)
Required fields, language, DCC — fields collected; page in the right language; a foreign-currency card triggers the DCC offer
Refunds — full refund; two partial refunds succeed; a third exceeding the original fails
Webhook idempotency — replaying an event doesn't double-fulfil
Token expiry — the integration recovers from a 401 by refreshing
Pre-auth users — authorise, capture a partial amount, remainder released

Going live

  1. Run the registration flow against production for the store; store its production credentials and webhook public key.
  2. Configure the Portal checkout settings: logo, messages, privacy and T&C URLs.
  3. Swap hosts via config: id.teya.xyz → id.teya.com, api.teya.xyz → api.teya.com.
  4. Process one small real transaction end to end: payment → webhook → refund.
  5. Switch traffic; keep SecurePay behind a flag for a day or two so rollback is a config change. Watch logs, webhook deliveries and the Portal.
  6. Remove the SecurePay form code. Teya retires the old SecurePay credentials for you once the platform is decommissioned; there is nothing to request.

Common errors

What a SecurePay developer trips on. For the general catalogue, see Troubleshooting.

SymptomLikely causeFix
Charged amount is 100× wrongMinor-units conversion missed, or applied to ISK€54.43 = 5443; ISK face value as-is
401 once the access token expires (currently about 15 minutes in production)Token cached like a static secret keyTTL from expires_in; refresh on 401
Customer sees "Session expired"Session created on cart loadCreate it at the Pay click
Orders marked paid that weren't, or never marked paidFulfilling from the redirect, as with orderhashFulfil from the webhook; back it with the session lookup
Webhook never arrivesEndpoint not public, has auth, bad certificate, or returns non-200Public HTTPS, no auth; allowlist Teya's webhook IPs if you filter inbound traffic; always return 200
Signature verification failsWrong key, or body parsed before verifyingKey for this webhook in this environment; verify the raw body; use the Signature Validator
Payment succeeded but not in the PortalWrong environmentStaging in business.teya.xyz, production in business.teya.com
"No compatible stores found" when creating credentialsStore lacks the ecommerce subscriptionAsk your Teya contact to add it
```admonition icon: fas fa-life-ring title: Need help? content: Your Teya partner contact can provision a staging partner account and development stores, and agree a batch approach for larger merchant bases. For integration questions, see Support. ```

Next steps

---

Migrating from SecurePay to Embedded Components

For partners and platforms whose SecurePay integration serves the merchants you serve, and who want the payment form inside your own pages, no redirect. Part 1 is a summary you can forward to a merchant or product owner; Part 2 is for your developers.

Who this guide is for

  • Your SecurePay form is embedded today — an iframe, or displaymode=iframe. Hosted Checkout does not support iframe embedding; Embedded Components is the path that keeps the form on the page.
  • You redirect to SecurePay today but want to upgrade — keep customers on the site through payment, in your layout, with Apple Pay built in.
```admonition icon: fas fa-info-circle title: Not you? content: Redirecting to SecurePay and want the simplest migration? Use SecurePay → Hosted Checkout. For the Ticket API, stored cards and recurring payments, or a plugin, start from the migration overview. link: /partners/online-payments/migration/overview ```

Why migrate, and what you gain

SecurePay runs on Teya's legacy platform, which is retiring in early 2027; onboarding to it stops soon. The new platform is an upgrade rather than a swap — and Embedded Components is a chance to improve the checkout, not just replace it.

More completed payments — 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
No redirect. Cart, details and payment on your page — fewer abandoned checkouts
Your brand throughout. Teya-hosted fields themed to match your page
3D Secure, currency conversion and Apple Pay built in. The 3DS challenge renders inline
Payment confirmations that can't get lost. A signed webhook confirms every successful payment server-to-server
Still SAQ A. Card data never touches your page or servers — the same PCI category as a redirect
Refunds in the Business Portal, and pre-authorisations with partial capture

Embedded Components or Hosted Checkout — which is right for you?

Both use the same credentials, session API and webhooks, and both keep you SAQ A. The difference is where the customer pays and how much front-end work that takes.

Hosted CheckoutEmbedded Components
Customer experienceRedirect to a Teya-hosted page and backPays on your page, no redirect
Development effortLower — one server-side call and a redirectModerate — the same call plus a front-end SDK integration
Branding and layoutLogo and messages on Teya's pageYour page, your layout; themeable fields
Apple Pay—Built into the Checkout component
3DS, DCC, refunds, webhooks, PCIIdentical

Choose Hosted Checkout if you want the least effort and checkout design isn't a differentiator — see SecurePay → Hosted Checkout. Choose Embedded Components if your integration was embedded, conversion justifies removing the redirect, or you want Apple Pay. The server side is identical, so you can start with Hosted Checkout and move later without redoing it.

Your options

Checkout component Recommended — a complete payment form (card fields, pay button, Apple Pay, DCC) in one block. Least code; right for most migrations.

What the migration involves

1 Get credentials
2 Build
3 Test in staging
4 Go live
5 Retire SecurePay

A small development project — days including testing, not weeks. It sits a notch above the Hosted Checkout path because of the front-end work. SecurePay keeps working in parallel until you switch it off.

```admonition icon: fas fa-paper-plane title: What to send your developer content: (1) A link to this page, pointing at Part 2. (2) Access to a development store — ask your Teya contact. (3) Two questions to answer back: does the integration need pre-authorisations, and does it use stored cards or recurring payments? Recurring is migrated last — see stored cards & recurring. ```

Developer guide

Replace the signed SecurePay form (redirect or iframe) with a checkout session created on your server and the Checkout component mounted on your page.

How it works: before and after

Today (SecurePay):

  1. Your page posts a form signed with a checkhash to SecurePay — a redirect or an iframe (displaymode=iframe).
  2. The customer pays on SecurePay's page or frame; 3D Secure runs there.
  3. SecurePay redirects to returnurlsuccess and posts an orderhash to returnurlsuccessserver.
  4. Your server verifies the orderhash and fulfils the order.

After (Embedded Components):

  1. Your server gets an OAuth access token and creates a checkout session, receiving a session_token.
  2. Your page initialises the Teya SDK with that token and mounts the Checkout component — Teya-hosted iframe fields; card data never enters your DOM or server.
  3. The customer pays in place; 3D Secure runs inside the component.
  4. Your onSuccess / onError callback fires, and Teya sends a signed webhook to your server — the source of truth for fulfilment.

The biggest change: the signed form post, checkhash, orderhash check and any iframe plumbing are deleted — the component replaces them, and there are no return URLs at all.

Before you start

1. Credentials

Each merchant's store_id, client_id, client_secret and webhook public key arrive through the merchant registration flow — the merchant approves once from their Teya account. For a larger base, agree a batch approach with your Teya partner contact. Build and test first against a staging partner account and development store.

SecurePay equivalent: merchantid + paymentgatewayid become the Store ID; the checkhash secret key becomes the Client ID and Client Secret.

2. Environments

StagingProduction
APIhttps://api.teya.xyzhttps://api.teya.com
OAuthhttps://id.teya.xyz/oauth/v2/oauth-tokenhttps://id.teya.com/oauth/v2/oauth-token
SDKdevelopmentMode: trueflag omitted

3. Webhook

Register a public HTTPS endpoint with no authentication for each merchant; the merchant's payment_webhook_public_key is returned in the registration callback and used in Step 5.

SecurePay equivalent: returnurlsuccessserver + orderhash, verified with an RSA signature instead of a shared-secret hash. Only successful payments produce a webhook; failures surface in onError and via session lookup.

4. Find the legacy code

Search your codebase for: borgun.is, securepay, checkhash, orderhash, merchantid, paymentgatewayid, returnurlsuccess, returnurlsuccessserver, returnurlcancel, returnurlerror, itemdescription_, itemcount_, itemunitamount_, itemamount_, displaymode, skipreceiptpage, merchantlogo, pagetype. Each hit is a line on your to-do list.

5. Ground rules

Staging first. Payment, webhook and refund end to end before touching production
Keep SecurePay live in parallel after the switch, so rollback is a config change
Never expose the Client Secret in browser code. The front end only ever sees the single-use session_token
Never fulfil from a browser-side signal. onSuccess is for UX; the webhook is the source of truth

What replaces what

SecurePay (today)Embedded Components (new)What it means for you
merchantid + paymentgatewayidstore_idOne UUID replaces both.
Secret key + HMAC checkhashclient_id + client_secret → OAuth Bearer tokenNo per-request HMAC. Cache and refresh from expires_in (currently 15 min in production, 24 h in staging).
HTML form posted to SecurePayPOST /v2/checkout/sessions → SDK mounts the component with the session_tokenThe browser never calls Teya's API; it only hosts the component.
displaymode=iframe + iframe result plumbingThe component is the embedded experienceTeya-hosted iframe fields; your iframe glue code goes.
amount as a decimal string (54,43)amount.value — integer in minor units (5443)×100 for EUR, GBP, CZK, HUF; ISK has no subunit — 100 ISK is 100.
currencyamount.currency — ISO 4217 alphabeticISK, EUR, CZK, HUF, GBP…
orderid (max 12 alphanumeric)merchant_reference + optional metadataWrite the legacy orderid into metadata during transition if it helps reconciliation.
languageSDK locale optionSet on initTeyaBlocks.
buyername, buyeremailcustomer.name, customer.email on the sessionPrefill only.
itemdescription_0 …line_items[] on the sessionDescription, quantity, unit price in minor units.
returnurlsuccess / returnurlcancel / returnurlerroronSuccess / onError callbacksNo redirects; your page decides what happens next.
returnurlsuccessserver + orderhashWebhook payment.succeeded.v1, RSA-signedVerify the signature over the raw body, then fulfil.
skipreceiptpage, merchantlogo, pagetypeNot applicable — there is no Teya pageTheme the component to match your page.
Verified by Visa / MC SecureCodeAutomatic 3D Secure inside the componentChallenge renders inline. Nothing to build.
Dynamic Currency ConversionAutomatic in the Checkout and Card componentsNothing to implement.
(no equivalent)Apple PayIn the Checkout component; one-off domain verification (Step 3).
(no equivalent)Pre-authorisation + partial captureSee After the payment.
Ticket APIPayment linksTicket API → Payment Links
Recurring payments / stored cardsCard on FileMigrate last — stored cards & recurring.
Refunds via portal / manualPOST /v3/refunds or the Business PortalPartial and multiple refunds.

Step-by-step

Steps 1–5 build the Checkout component end to end, in cURL against staging. Other languages, and the interactive quickstart, are on the Embedded Checkout get started page.

Step 1 — Get an access token (server)

Exchange client_id and client_secret for a short-lived Bearer token; this replaces checkhash signing. Add captures/create to the scopes if you use pre-authorisations.

```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": "eyJ...", "token_type": "Bearer", "expires_in": 900 } ```

Things to get right: derive the cache lifetime from expires_in (currently 15 min production, 24 h staging); refresh when under a minute remains or on any 401; one cache per environment. Full reference: Authenticate.

Step 2 — Create a checkout session (server)

When the customer reaches payment, create a session describing the intent — amount, currency, reference. The response carries the session_token your page hands to the SDK. This replaces the signed form; no card data is involved.

```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": 5443, "currency": "EUR" }, "merchant_reference": "ORDER-2026-00042", "metadata": { "legacy_orderid": "ORDER1230001" } }' ``` ```json { "session_id": "14411456-79c8-4d8c-8498-f01907093b83", "session_token": "eyJhbGciOiJSUzI1NiJ9...", "session_status": "ACTIVE" } ```

Things to get right:

  • amount.value is an integer in minor units — €54.43 is 5443; ISK face value as-is. The most common migration bug.
  • Idempotency-Key is required and unique per order attempt.
  • The session_token is single-use and the session expires — create it when the customer reaches payment, not on cart load; create a fresh one for a retry.
  • Return only the session_token to the front end through a small endpoint of your own (e.g. POST /api/teya-session) — never the access token or client secret. Store the session_id against the order first.

Full reference: Embedded Checkout · API reference.

Step 3 — Mount the Checkout component (front end)

Install the SDK (npm install @teyaproduct/teya-blocks-js; React also @teyaproduct/teya-blocks-react), initialise it with the session token from your backend, and mount the component into a container. This replaces the SecurePay form or iframe.

```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: 'default' }, // 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) => { // Show your confirmation. Fulfil from the webhook (Step 5), not here. window.location.href = '/order/confirmation'; }, onError: (error) => { // The component shows the error inline; the customer can retry in place. console.error('Payment failed', error); }, }); checkout.mount('#checkout-container'); ```

Things to get right:

  • developmentMode: true is required in staging — without it the SDK targets production and the staging token is rejected. Gate it on environment config; never ship it to production.
  • 3D Secure runs inside the component. Delete any special handling your iframe integration had for the 3DS redirect.
  • Apple Pay needs a one-off domain verification per domain and per environment (Business Portal → Integrations → Apple Pay). Cards work without it.
  • Theme with appearance — presets, CSS variables and fonts on the Customize page.

Full reference: SDK Initialization · Embedded Checkout (HTML/JS and React).

Variant: the Card component

For your own form layout and pay button, mount a card element instead and call card.submitPayment() from your button; use its onChange event (event.complete) to enable the button. Session and webhook are unchanged; Apple Pay is a separate element. See Card element.

Step 4 — Handle the result

Use onSuccess / onError for the user experience only; fulfil from the webhook. If the customer closed the page mid-payment or the webhook is delayed, look the session up by ID — this replaces the returnurlsuccess redirect and orderhash check.

```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": 5443, "currency": "EUR" }, "type": "SALE", "payment_status": "SUCCESS" } ```

Things to get right: session_status runs ACTIVE → PROCESSING → COMPLETED / EXPIRED; trust payment_status only once the session is COMPLETED. Store the transaction_id from the completed session or the webhook — you need it 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 webhook URL when a payment succeeds — your fulfilment trigger, replacing returnurlsuccessserver, and one that no longer depends on the customer's browser. Verify the x-teya-signature header (SHA256 with RSA, Base64) against the public key for this webhook, over the entire raw body, before parsing. Match the order on data.merchant_reference or data.session_id.

Things to get right:

  • Be idempotent — the same event can arrive more than once. Return 200 promptly; do slow work asynchronously.
  • Verify the raw bytes, not re-serialised JSON; the key is per webhook and per environment.
  • Behind a CDN or strict firewall, allowlist Teya's IPs — see Troubleshooting.
  • Check your implementation with the Webhook Signature Validator before go-live.

Full reference: Webhooks — payload fields, verification steps and sample code.

After the payment

Identical to the Hosted Checkout path — the server side doesn't care how the payment was collected. Refunds and captures report their outcome in the synchronous response; there is no webhook for them.

Refunds. Full, partial and multiple refunds up to the original amount — by API or in the Business Portal with no code. 201 is processed; 202 is still processing, retry with the same idempotency key. A DCC payment is refunded in the currency the customer chose.
POST /v3/refunds · scope refunds/create · Payment actions

Status lookup. Reconcile by session; store the transaction_id with each order.
GET /v2/checkout/sessions/{session_id} · scope checkout/sessions/id/get · API reference

Digital receipts (new, optional). Email the customer a receipt for a completed payment.
POST /v1/transactions/{transaction_id}/receipts · scope transactions/id/receipts/create · Payment actions

Pre-authorisations (new). SecurePay never offered holds; the new platform does. 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. Create the session with "type": "PRE_AUTHORISATION"; the component flow is otherwise identical (no DCC on pre-authorisations). Capture once per authorisation, any amount up to the total, with POST /v1/transactions/{transaction_id}/capture (scope captures/create); the remainder is released. To release a hold early, refund the uncaptured payment — that reverses it. See Payment actions.

Testing and going live

Test in staging with developmentMode: true and the test cards on the Testing page. If your dev environment isn't publicly reachable, tunnel it so staging webhooks can reach you.

Definition of done

Frictionless card: onSuccess fires, webhook verified, session COMPLETED / SUCCESS, order fulfilled once
3DS challenge card: challenge renders inline; payment completes
Declined card: error shown in the component, customer retries in place, order not marked paid
Abandoned checkout: no webhook, session expires, order not fulfilled
Expired session and reused token both fail cleanly; your code creates a fresh session
Amounts: your main currency charges exactly the right amount in the Business Portal (minor-units trap)
Locale and theming: the component renders in your language and matches your page on desktop and mobile
DCC: a foreign-currency card triggers the offer; the flow completes either way
Full refund, then two partial refunds; a third exceeding the original fails
Webhook idempotency: replaying the same event doesn't double-fulfil
Token expiry: the integration recovers from a 401 by refreshing
If used: pre-authorise → partial capture → remainder released; Apple Pay domain verified and a test payment completes

Go live

  1. Obtain each merchant's production credentials and webhook public key through the registration flow and store them against that merchant.
  2. Verify your domain for Apple Pay in production if you use it — staging verification doesn't carry over.
  3. Swap hosts via config (id.teya.xyz → id.teya.com, api.teya.xyz → api.teya.com) and remove developmentMode: true.
  4. Process one small real transaction end to end: payment → webhook → refund.
  5. Run SecurePay in parallel behind a flag for a day or two; watch logs, webhook deliveries and the Business Portal.
  6. Retire the legacy integration: delete the SecurePay form or iframe code. Teya retires the old credentials for you; there is nothing to request.

Common errors

SymptomLikely causeFix
Staging session token rejected by the SDKdevelopmentMode: true missingAdd it in staging; remove in production
Payments land in the wrong environmentdevelopmentMode shipped to production, or hosts mixedGate on environment config; audit hosts
Charged amount is 100× wrongDecimal → minor-units conversion missed€54.43 is 5443; ISK face value as-is
Component won't mount / token errorSession token already used, or session expiredOne token per mount; create a fresh session
401 on API callsAccess token expiredRefresh; derive cache lifetime from expires_in
403 / 409Missing scope / same Idempotency-Key, different bodyAdd the scope (Step 1) / unique key per order attempt
Webhook never arrivesURL unreachable, firewall, or non-200 responsePublic HTTPS, no auth; allowlist Teya IPs; return 200
Signature verification failsWrong key, or a parsed bodyPer-webhook, per-environment key; verify the raw body; use the Signature Validator
Apple Pay button doesn't renderDomain not verified in this environmentVerify staging and production separately
```admonition icon: fas fa-life-ring title: Need help? content: Your Teya partner contact can provision a staging partner account and development stores, agree the credentials route for your merchant base and confirm pre-authorisation eligibility. For integration questions, see Support. ```

Next steps

---

Migrating from RPG to Embedded Components

For partners and platforms whose systems talk directly to RPG (Borgun Payment API) on behalf of the merchants you serve. The first half is a summary you can forward; the second half is the developer guide.

Who this guide is for

  • Your code calls URLs containing borgun.is, rpgapi or /api/payment, authenticated with a Private Access Token.
  • You run 3D Secure yourself through /api/mpi/v2/enrollment and /api/mpi/v2/validation.
  • Your checkout page loads BAPIjs / borgun-payment.js to create single-use tokens.
```admonition icon: fas fa-map-signs title: Not you? content: Redirects to securepay.borgun.is, payment URLs created in advance, and stored cards charged without the customer present each have their own guide. link: /partners/online-payments/migration/overview ```

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.

More completed payments — 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
No 3D Secure to build — enrollment, challenge and validation run inside the component
No card data on your systems — Teya-hosted fields inside your page, a smaller PCI scope
Payment confirmations that can't get lost — a signed webhook for every successful payment
Apple Pay and currency conversion without extra code
Refunds without a developer, and partial captures on pre-authorisations

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

1 Get credentials
2 Build
3 Test in staging
4 Go live
5 Retire RPG

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.

```admonition icon: fas fa-paper-plane title: What to line up before you build content: (1) A staging partner account and development store from your Teya partner contact. (2) The list of merchants on this integration, so credentials can be exchanged store by store. (3) Whether any of them use pre-authorisations or stored cards / recurring payments (multi-use tokens) — both change the plan. ```

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)

  1. Your form or BAPIjs collects the card; card data or a single-use token reaches your server.
  2. Your server runs 3D Secure: enrollment, ACS redirect, TermURL callback, validation, MdStatus.
  3. Your server calls POST /api/payment with the ThreeDSecure block and reads ActionCode.
  4. Optionally a PaymentCreate webhook arrives and you call back GET /api/events/{EventId}.

After (Embedded Components)

  1. Your server gets a short-lived access token and creates a checkout session; the response carries a session token.
  2. 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.
  3. The customer pays in place; 3D Secure runs inside the component.
  4. onSuccess fires 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

Each store gets its own client_id, client_secret, store_id and webhook public key. For merchants already on RPG, exchange each merchant's RPG private key for new credentials — one call per store, production only. New merchants come through the registration flow. For staging, ask your Teya partner contact for a development store and create credentials in the Staging Business Portal.

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.

Pass the URL as webhook_url in the credential exchange and store the returned payment_webhook_public_key against that store.

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

Complete a payment, webhook and refund end to end in staging before touching production code
Keep RPG live behind a flag for a day or two after switching, so rollback is a config change
Never expose the Client Secret in browser code or repositories
Fulfil orders from the webhook, never from a browser callback alone

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 todayNew platformWhat it means for you
Private Access Token (permanent, Basic auth)client_id + client_secret → short-lived Bearer tokenCache 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 paymentDelete with the BAPIjs code.
POST /api/payment, TransactionType: "Sale"POST /v2/checkout/sessions, type: "SALE" + the componentThe server sends intent only — amount, currency, reference. No card data, no 3DS block.
POST /api/payment, TransactionType: "PreAuthorization"type: "PRE_AUTHORISATION" on the same endpointSee 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}/cancelPOST /v3/refunds on the uncaptured paymentA refund before capture reverses the authorisation. No separate cancel.
PUT /api/payment/{id}/refund + PartialAmountPOST /v3/refunds with transaction_id, amount — or the Business PortalSame 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.getTokenGone — the component's hosted fields do thisNo token creation, lifetime management or disable calls.
/api/mpi/v2/enrollment, /validation, TermURL, MdStatus, MpiTokenGone — 3D Secure runs inside the component, challenge inlineDelete the whole flow.
POST /api/payment with raw PANDirect API — restricted, PCI SAQ D evidence requiredUse the component unless you have no alternative.
POST /api/token/multi, VerifyCardCard on FileSeparate 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–9merchant_reference (free-form) + optional metadataWrite the legacy OrderId into metadata during transition if it helps reconciliation.
TransactionDateNot sent — Teya timestamps server-sideDelete.
ActionCode ("000" = approved) + TransactionStatuspayment_status (SUCCESS / FAILED) on the sessionNo action-code tables; declines show in the component and the customer retries in place.
DynamicDescriptorsNot exposed on checkout sessionsTell 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-OnlineBusiness PortalCredentials, 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.

```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 } ```

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.

```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" } ```

Things to get right

  • amount.value is an integer in minor units per ISO 4217: 100 ISK = 100, not RPG's 10000.
  • Idempotency-Key is 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_token to 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.

```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'); ```

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 MdStatus are 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.

```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" } ```

Things to get right

  • session_status runs ACTIVE → PROCESSING → COMPLETED or EXPIRED; trust payment_status only once COMPLETED.
  • Store the transaction_id from 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.

```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" } } ```

Things to get right

  • Be idempotent — the same event can arrive more than once. Return 200 promptly 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.

```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 }' ```

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

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

Go live

  1. Exchange each merchant's RPG key for production credentials (with webhook_url) and store the credentials and public key per store.
  2. Swap hosts by config (.xyz → .com) and remove developmentMode.
  3. Process one small real payment end to end: payment → webhook → refund.
  4. Run RPG in parallel for a day or two behind a flag; watch logs, webhook deliveries and the Business Portal.
  5. 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

SymptomLikely causeFix
Charged amount is 100× too big (ISK)RPG's two-implied-decimals convention carried overDivide legacy ISK amounts by 100
Staging session token rejected by the SDKdevelopmentMode: true missingAdd it in staging, remove in production
401 on API callsAccess token expired — currently 15 min in production, not permanent like the RPG tokenRefresh from expires_in; retry once on 401
403Missing scopeAdd the scope to the token request (Step 1)
409Same Idempotency-Key, different bodyUnique key per order attempt; reuse only for retries of the identical request
422 creating a sessionNumeric currency code or non-integer amountAlphabetic currency; integer amount.value in minor units
Webhook never arrivesURL not public, no valid certificate, auth on the endpoint, or non-200 responsesPublic HTTPS, no auth, return 200 after processing
Signature verification failsParsed body re-serialised, or the wrong keyVerify the raw body; use the key issued for this webhook in this environment
Payment succeeded but not in the Business PortalWrong environmentStaging payments are in business.teya.xyz, production in business.teya.com
Waiting for a refund or capture webhookOnly successful customer payments send oneRead the outcome from the API response
```admonition icon: fas fa-life-ring title: Need help? content: Your Teya partner contact can provision staging accounts and development stores, confirm pre-authorisation availability for your merchants and plan the stored-card import. For integration questions, see Support. ```

Next steps

---

Migrating from B-Gateway to Embedded Components

For partners and platforms whose systems send payments to B-Gateway (the Borgun payment gateway web service, SOAP/XML; also written BGateway or Greiðslugátt) on behalf of the merchants you serve. The first half is a summary you can forward; the second half is the developer guide.

Who this guide is for

  • Your systems send XML requests such as getAuthorization to gateway01.borgun.is, with a B-Gateway username and password.
  • Card numbers reach your own servers: your checkout form collects them, or your systems hold card numbers or virtual card numbers (9910…) and send them on to B-Gateway.
  • You use the same getAuthorization call for payments where a customer is checking out and for charges you run later without them (subscriptions, instalments, top-ups). You may also run 3D Secure through get3DSAuthentication or your own or a third-party MPI.
```admonition icon: fas fa-map-signs title: Not you? content: Calls to /api/payment (the RPG payment API), redirects to securepay.borgun.is, and payment URLs created in advance each have their own guide. link: /partners/online-payments/migration/overview ```

Why migrate, and what you gain

B-Gateway runs on Teya's legacy platform, which is retiring in early 2027. The new platform is an upgrade, not a like-for-like swap: it removes the parts of a B-Gateway integration that cost the most to build and maintain.

More completed payments — 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
No 3D Secure to build — enrollment, challenge and validation run inside the component
No card data on your systems — Teya-hosted fields inside your page, a smaller PCI scope
Payment confirmations that can't get lost — a signed webhook for every successful payment
Apple Pay and currency conversion without extra code
Refunds without a developer, and partial captures on pre-authorisations

Your options

Checkout component Recommended — the closest match to a B-Gateway checkout with the least to maintain. The customer stays on your page; one block provides card fields, pay button, 3D Secure, Apple Pay and currency conversion. This guide follows it.

  • 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; the steps below apply, with one change at Step 3. See Hosted Checkout.
  • Direct API — your servers send card data to Teya, as with B-Gateway today. Card data stays on your systems, so the PCI DSS obligations that come with it (SAQ D) stay with you, and Teya runs 3D Secure on the transaction flow while you display the challenge. Check with Teya before choosing this path, and tell us if you run your own or a third-party MPI today. See the B-Gateway reference.
```admonition icon: fas fa-info-circle title: Customer-initiated and merchant-initiated payments content: The new platform supports both: payments where the customer is present, through a checkout session, and merchant-initiated payments on a card the customer saved with you, including card-on-file and tokenisation. Which of your B-Gateway calls is which is the first thing your developer sorts out (developer guide, step 1). See Card on File. ```

What the migration involves

1 Get credentials
2 Build
3 Test in staging
4 Go live
5 Move saved cards
6 Retire B-Gateway

This is a small development project, and the result is less code than you have today: the SOAP client, the card form, any 3D Secure flow and batch handling are all removed. B-Gateway keeps working in parallel until you switch it off. If most of your payments are subscriptions or charges on cards you hold, step 5 and the charging of saved cards are the bulk of the work; the one-off checkout still comes first, because it is how new cards get saved from now on. Stored cards move on their own track, without any customer re-entering a card.

```admonition icon: fas fa-paper-plane title: What to line up before you build content: (1) A staging partner account and development store from your Teya partner contact; card saving (tokenization) is enabled by default, but enrolment can take up to 48 hours. (2) The list of merchants on this integration, with the B-Gateway MerchantID you hold for each, so credentials can be set up store by store. (3) Which of them charge cards without the customer present (virtual card numbers from B-Gateway, or card numbers you store), use pre-authorisations, or use any other B-Gateway features (telephone or keyed orders, reports pulled from B-Gateway, Dynamic Descriptors, detail or itinerary data). Each changes the plan. ```

Developer guide

How a B-Gateway integration maps onto a checkout session, Hosted Checkout or the Checkout component, and token payments, with the one request you need at each step.

How it works: before and after

Today (B-Gateway)

  1. Your checkout form collects the card number, expiry and security code; they reach your server.
  2. If you run 3D Secure, your server calls get3DSAuthentication or your own MPI, sends the customer to the bank and gets CAVV / UCAF / XID back.
  3. Your server sends getAuthorization (SOAP/XML, username and password, Processor, MerchantID, TransType, RRN) with the card or a virtual card number, plus 3D Secure values if you have them, and reads ActionCode. The same call is used whether or not the customer is present.
  4. For subscriptions, you keep a virtual card number from getVirtualCard and later send getAuthorization with it, without the customer.
  5. Refunds and cancellations reference the original DateAndTime + RRN; batches close at midnight GMT or on newBatch.

After (new platform)

  1. Your server gets a short-lived access token and creates a checkout session for the amount. To save the card for later, it adds save_payment_method: true.
  2. Your page mounts the Checkout component with the session token (or, with Hosted Checkout, redirects the customer to a Teya page). Teya collects the card and runs 3D Secure.
  3. A signed webhook confirms the payment to your server. When the card was saved, it carries the token.
  4. For subscriptions, your server charges the token later with POST /v3/transactions/online, marked as merchant-initiated.
  5. Refunds reference the transaction_id. There are no batches.

The biggest change: card numbers no longer pass through your systems, and each payment is either a checkout session (customer present) or a charge on a saved card (merchant-initiated). The SOAP client, the card form, any 3D Secure flow, getVirtualCard and batch handling are all deleted.

Before you start

1. Sort your payments

List every place your code calls getAuthorization and decide, for each one, whether a customer is present. On B-Gateway the same request serves both; on the new platform they are different calls:

  • A customer typing a card now (your checkout): a checkout session, then the Checkout component (Steps 2–3). 3D Secure runs for you.
  • A charge on a stored card without the customer (subscription, instalment, top-up, pay-later): POST /v3/transactions/online on a saved token, declared merchant-initiated (Step 6). No 3D Secure, immediate result.
  • Saving a card for later without charging it: a zero-amount ACCOUNT_VERIFICATION session with save_payment_method (Step 2).

Every item on your list becomes either a session or a token charge. A charge without the customer is only possible on a card they saved with you, so if you hold card numbers that were never saved through Teya, see Stored cards & recurring before you plan Step 6.

2. Credentials

Each store gets its own client_id, client_secret, store_id and webhook public key; together they replace the B-Gateway username, password, Processor and MerchantID you hold per merchant. Merchants come onto the new platform through the registration flow, which issues their credentials to you. If you hold B-Gateway credentials for many merchants, talk to your Teya partner contact about the approach before you start. For staging, ask for a development store. Never mix staging and production credentials. See Authenticate.

Card saving (tokenization) is enabled by default on every store, but enrolment can take up to 48 hours after the store is created. Until then, sessions with save_payment_method succeed but return no token. If that persists after 48 hours, contact support.

3. Environments

Staging is api.teya.xyz / id.teya.xyz / business.teya.xyz; production is the same hosts on .com. With Embedded Components, staging also needs developmentMode: true in the SDK. Full table on Hosted Checkout Get Started.

4. Webhook

Teya sends a signed payment.succeeded.v1 event to a public HTTPS endpoint with no authentication on it. Register the URL per store when the store is set up; you receive a public key per webhook for verifying signatures. Only successful customer payments produce a webhook today; refunds, captures and token charges return their outcome in the API response. See Webhooks.

5. Find the legacy code

Search for gateway01.borgun.is, borgun.is, getAuthorization, cancelAuthorization, get3DSAuthentication, getVirtualCard, newBatch, getTransactionList, TransType, TrAmount, TrCurrency, RRN, AuthCode, ActionCode, Processor, CAVV, UCAF, XID, SecurityLevelInd, lifecycleID, the XML namespace tempuri.org/getAuthorization.xsd, and stored 16-digit values starting 9910. Code based on the B-Gateway demo clients (BGatewayDemoNet, BGatewayDemoPhp, BGatewayDemoPython) follows the same pattern. Each hit is a line on the to-do list. If you send TerminalID 2 (ecommerce with 3D Secure) or CAVV / UCAF values, you run 3D Secure today: also check the MPI configuration behind it.

6. Ground rules

Complete a payment, webhook and refund end to end in staging before touching production code
Keep B-Gateway live behind a flag for a day or two after switching, so rollback is a config change
Never expose the Client Secret in browser code or repositories
Fulfil orders from the webhook, never from a browser redirect or callback alone
Move one-off payments first; saved cards follow on their own track

What replaces what

The calls and fields a B-Gateway developer has to change. The B-Gateway reference covers every operation, including reporting and the ones with no equivalent.

B-Gateway todayNew platformWhat it means for you
Username + password on every callclient_id + client_secret → short-lived Bearer tokenCache and refresh from expires_in (currently 15 min in production, 24 h in staging).
Processor, MerchantID, TerminalID (1 ecommerce, 2 ecommerce with 3D Secure, 3 recurring, 4 mail/telephone order)store_idOne identifier per store, sent in the request body. What the terminal said about the payment is now said by the request itself: a session for customer-present payments, initiator + recurring_payment_type for the rest.
getAuthorization, TransType 1POST /v2/checkout/sessions, type: "SALE" + the Checkout componentThe server sends intent only: amount, currency, reference. No card data, no 3D Secure values.
getAuthorization, TransType 5type: "PRE_AUTHORISATION" on the same endpointSee Pre-authorisations below.
TransType 1 with a pre-authorisation's AuthCodePOST /v1/transactions/{id}/capture, {"amount": n}Once per authorisation; the remainder is released.
TransType 3 by DateAndTime + RRNPOST /v3/refunds with transaction_id, or the Business PortalSame semantics: partial, multiple, total ≤ original.
TransType 4, cancelAuthorizationPOST /v3/refunds on the uncaptured paymentA refund before capture reverses the authorisation. No partial reversal; no open-batch rule.
get3DSAuthentication / own MPI; CAVV, UCAF, XID, SecurityLevelInd (if you send them)Gone: 3D Secure runs inside Hosted Checkout and the componentDelete the whole flow if you have one, including MerchantReturnURL handling. If you don't run 3D Secure today, there is nothing to delete.
PAN, ExpDate, CVC2; your card formGone: entered on the Teya page or in Teya-hosted fieldsDelete the card form and every place card data is logged or stored.
getVirtualCardsave_payment_method: true on the session; token_response.token_id in the webhookSaving happens inside the payment, or a zero-amount ACCOUNT_VERIFICATION.
getAuthorization with a virtual card number (or a card number you store), customer absentPOST /v3/transactions/online, initiator: "MERCHANT", payment_method.type: "TOKEN"See Step 6. Virtual numbers you already hold move later; see Stored cards & recurring. Card numbers you store yourself have no import: talk to your Teya partner contact.
lifecycleIDNot sentThe platform links later charges to the first payment for you.
TrAmount: 2 implied decimals, including ISK (100 ISK = 10000)amount.value: minor units per ISO 4217; ISK has no subunit (100 ISK = 100)If you charge in ISK, divide B-Gateway amounts by 100: the most likely silent bug. Other currencies are unchanged (€49.90 is 4990 on both).
TrCurrency: ISO numeric (352)amount.currency: ISO alphabetic ("ISK")Lookup change.
RRN (12 characters), DateAndTimemerchant_reference (free-form) + optional metadata; no timestampWrite the legacy RRN into metadata during the transition if it helps reconciliation.
ActionCode (000 = approved)payment_status (SUCCESS / FAILED) on the sessionDeclines show in the checkout and the customer retries in place.
Batches, newBatch, Batch / Transaction numbersNoneDelete. Key your records on transaction_id.
getTransactionList, settlement and dispute listsGET /v2/checkout/sessions/{session_id} for one payment; the Business Portal for the restSee the reference for each report.
B-OnlineBusiness PortalCredentials, webhooks, refunds, transaction visibility.

Step-by-step

Steps 1–5 take one-off payments end to end; Step 6 adds charging a saved card. 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 sending a username and password on every call: 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. Add captures/create for pre-authorisations and transactions/online/create for charging saved cards.

```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 transactions/online/create" ``` ```json { "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. This replaces getAuthorization, but the body carries no card data and no 3D Secure values; only the intent. For a subscription sign-up, add save_payment_method: true so the card is saved during this payment.

```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": 4990, "currency": "ISK" }, "merchant_reference": "INV-2026-00042", "save_payment_method": true, "success_url": "https://yourstore.com/order/success", "cancel_url": "https://yourstore.com/checkout", "metadata": { "legacy_rrn": "ACME00012345" } }' ``` ```json { "session_id": "14411456-79c8-4d8c-8498-f01907093b83", "session_token": "eyJhbGciOiJSUzI1NiJ9...", "session_status": "ACTIVE" } ```

Things to get right

  • amount.value is an integer in minor units per ISO 4217: 4,990 ISK is 4990, not B-Gateway's 499000; €49.90 is 4990 on both.
  • Idempotency-Key is unique per order attempt; retries with the same key can't create duplicate sessions. This replaces looking up an unknown result with getTransactionList.
  • To save a card without charging it (a "add a card" or free-trial flow), send type: "ACCOUNT_VERIFICATION" with amount.value: 0 and save_payment_method: true. This replaces the zero-amount pre-authorisation.
  • Record the customer's consent to store the card before you save it. See Card on File.
  • Create the session when the customer clicks Pay, not when the cart loads: sessions expire.
  • The session_token in the response is what your page needs for the component (Step 3). success_url and cancel_url are only used by Hosted Checkout.

Full reference: Embedded Checkout, 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. It replaces your card form and your 3D Secure redirect: the fields are Teya-hosted, so card data never enters your page or your server.

```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: 'is', // Routes SDK calls to Teya staging. Remove for production. developmentMode: true, }); // 3. Mount the checkout: card fields, pay button, 3D Secure, Apple Pay, DCC included const checkout = teya.elements.create('checkout', { onSuccess: () => { 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.
  • Return only session_token to the browser through a small endpoint of your own; never the access token or the Client Secret.
  • For your own form layout and pay button, use the Card component instead; everything else is the same.

Using Hosted Checkout instead? Skip the SDK: send the browser to the session_url returned with the session, exactly as returned. The customer pays on Teya's page and lands on your success_url or cancel_url; the redirect carries no payment data, so Steps 4 and 5 are unchanged. See Hosted Checkout Get Started.

Full reference: SDK Initialization and Embedded Checkout.

Step 4 — Look up the session (server)

The webhook (Step 5) is the source of truth for fulfilment. As a fallback (customer closed the browser, webhook delayed, payment failed), look the session up by ID. This replaces reading ActionCode and polling getTransactionList.

```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", "payment_status": "SUCCESS", "type": "SALE", "amount": { "value": 4990, "currency": "ISK" }, "merchant_reference": "INV-2026-00042", "transaction_id": "tr_72302a4d-b2f9-4663-93f8-d9260e5370c8" } ```

Things to get right: session_status runs ACTIVE → PROCESSING → COMPLETED or EXPIRED; trust payment_status only once COMPLETED. Store the transaction_id: it is the key for refunds, replacing DateAndTime + RRN. 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, whether or not the customer comes back to your site. Verify x-teya-signature (SHA256withRSA, Base64) over the raw request body with the public key issued for this webhook, before parsing anything. When the card was saved, the event carries the token.

```json { "event": "payment.succeeded.v1", "timestamp": "2026-10-05T12:04:22Z", "data": { "transaction_id": "tr_72302a4d-b2f9-4663-93f8-d9260e5370c8", "session_id": "14411456-79c8-4d8c-8498-f01907093b83", "merchant_reference": "INV-2026-00042", "amount": { "value": 4990, "currency": "ISK" }, "status": "SUCCESS", "token_response": { "token_id": "1e2d4f80-9c3e-4f5b-83aa-9e8d2b6a6c49", "token_status": "SUCCESS" } } } ```

Things to get right

  • Store data.token_response.token_id against the customer. It replaces the virtual card number in your subscription records. token_response is null when no card was saved.
  • Be idempotent: the same event can arrive more than once. Return 200 promptly and do slow work asynchronously.
  • Register the route before any JSON body parser so you verify the raw bytes; check your handler with the Webhook Signature Validator.

Full reference: Webhooks.

Step 6 — Charge a saved card later (server)

For each subscription or instalment due, charge the token without the customer present. This replaces getAuthorization with a virtual card number and no 3D Secure values. Declare it as merchant-initiated: there is no 3D Secure and the result is immediate and final.

```bash curl -X POST https://api.teya.xyz/v3/transactions/online \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Idempotency-Key: sub-8812-2026-11" \ -d '{ "store_id": "123e4567-e89b-12d3-a456-426614174000", "type": "SALE", "initiator": "MERCHANT", "amount": { "value": 4990, "currency": "ISK" }, "payment_method": { "type": "TOKEN", "token": { "token_id": "1e2d4f80-9c3e-4f5b-83aa-9e8d2b6a6c49", "recurring_payment_type": "RECURRING" } } }' ``` ```json { "type": "ONLINE_TRANSACTION", "online_transaction": { "transaction_id": "tr_9f2c1a70-4d88-4b3e-91ad-6c7e2f5b8d40", "status": "SUCCESS", "status_reason": null } } ```

Things to get right

  • recurring_payment_type: RECURRING for a fixed schedule, INSTALLMENT for a fixed number of payments, CARD_ON_FILE for unscheduled charges.
  • Use one Idempotency-Key per customer per billing period, so a re-run of your billing job can't charge twice.
  • Retry declines only for temporary reasons (status_reason such as INSUFFICIENT_FUNDS or TEMPORARY_ISSUE_RETRY); the full list is on Charge a Saved Payment Method.
  • Virtual card numbers you already hold are charged on B-Gateway, unchanged, until your stored cards are moved. See Stored cards & recurring.
  • If your systems hold real card numbers rather than virtual card numbers, there is no import for them: talk to your Teya partner contact before planning this step.

Full reference: Charge a Saved Payment Method.

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 TransType 3 and cancelAuthorization: partial and multiple refunds, total ≤ original, referenced by transaction_id instead of DateAndTime + RRN. 201 is processed; 202 is still processing: retry with the same Idempotency-Key. Staff can also refund from the Business Portal. Payments taken on B-Gateway before you switch stay refundable in B-Online.

```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_72302a4d-b2f9-4663-93f8-d9260e5370c8", "amount": 1000 }' ```

See Payment Actions.

Pre-authorisations

Skip this if you only send TransType 1 without an AuthCode. Create the session with type: "PRE_AUTHORISATION"; the checkout is unchanged and the funds are held (no currency conversion on pre-authorisations). 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; this also replaces the partial reversal (TransType 4). To cancel, refund the uncaptured payment; untouched holds expire on their own. See Payment Actions.

Using other features

Batches, transaction and settlement reports, telephone or keyed orders, Dynamic Descriptors and detail or itinerary data each have their own answer on the new platform, and some depend on your set-up. If you use any of them, contact Teya support with your specific case before you migrate, so we can check it with you. The B-Gateway reference lists every operation and where it lives now.

Testing and going live

Test in staging with 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

Frictionless card: payment completes, webhook received and verified, session COMPLETED / SUCCESS
Challenge card: the 3D Secure challenge shows in the component, and the payment completes
Declined card: error shown, customer can retry, order not marked paid
Abandoned or expired session: no webhook, nothing fulfilled, your code creates a fresh session
Amounts: an ISK (and, if relevant, EUR) payment charges exactly the right amount in the Business Portal
Full refund, then two partial refunds; a third exceeding the original fails
Replaying the same webhook doesn't double-fulfil
Integration recovers from a 401 by refreshing the token
Saved cards: a sale with save_payment_method and an ACCOUNT_VERIFICATION both return a token_id in the webhook; a merchant-initiated charge on that token succeeds
Pre-auth users: authorise, capture a partial amount, remainder released

Go live

  1. Set up production credentials and the production webhook for each merchant store, and store the credentials and public key per store. If a merchant saves cards, set the store up early: card saving is on by default but enrolment can take up to 48 hours.
  2. Swap hosts by config (.xyz → .com) and, for Embedded Components, remove developmentMode.
  3. Process one small real payment end to end: payment → webhook → refund.
  4. Run B-Gateway in parallel for a day or two behind a flag; watch logs, webhook deliveries and the Business Portal. New cards are now saved on the new platform; existing virtual card numbers keep being charged on B-Gateway until they are moved.
  5. Move your saved cards with Stored cards & recurring.
  6. Retire the legacy integration: delete the SOAP client, card form, 3D Secure flow, getVirtualCard and batch code. Remove stored card numbers and card data from your systems and logs. Teya retires the B-Gateway usernames and passwords for you; there is nothing to request.

Common errors

SymptomLikely causeFix
Charged amount is 100× too big (ISK)B-Gateway's two-implied-decimals convention carried overDivide legacy ISK amounts by 100
422 creating a sessionNumeric currency code (352) or a non-integer amountAlphabetic currency; integer amount.value in minor units
401 on API callsAccess token expired; unlike a B-Gateway password it is short-livedRefresh from expires_in; retry once on 401
403Missing scope, e.g. transactions/online/create for saved-card chargesAdd the scope to the token request (Step 1)
409Same Idempotency-Key, different bodyUnique key per order attempt or billing period; reuse only for retries of the identical request
Payment succeeds but no token_id in the webhooksave_payment_method missing, or the store was created less than 48 hours ago and tokenization enrolment is still pendingSend save_payment_method: true; wait for enrolment, then contact support if it persists
400 charging a 9910… value as TOKENA legacy virtual card number sent as a Teya tokenKeep charging it on B-Gateway until your stored cards are moved, then use LEGACY_TOKEN. See Stored cards
A refund can't find the paymentLegacy DateAndTime + RRN used as the referenceRefund by transaction_id; refund B-Gateway payments in B-Online
Webhook never arrivesURL not public, no valid certificate, auth on the endpoint, or non-200 responsesPublic HTTPS, no auth, return 200 after processing
Signature verification failsParsed body re-serialised, or the wrong keyVerify the raw body; use the key issued for this webhook in this environment
Payment succeeded but not in the Business PortalWrong environmentStaging payments are in business.teya.xyz, production in business.teya.com
```admonition icon: fas fa-life-ring title: Need help? content: Your Teya partner contact can provision staging accounts and development stores, agree the credential approach for your merchants and plan the move of their saved cards. For integration questions and anything under "Using other features", see Support. ```

Next steps

---

Migrating from the Ticket API to Payment Links

For partners and platforms that create payment URLs in advance with the Ticket API on behalf of the merchants they serve. This guide moves that flow to Pay By Link.

Who this guide is for

  • Your systems or staff create a payment URL ("ticket") for a known amount before the customer pays.
  • The URL is delivered outside a website checkout — email, SMS, invoice, phone order.
  • You learn that a ticket was paid through the legacy confirmation, or by checking manually.
```admonition icon: fas fa-random title: Not you? content: If customers pay in a checkout on your website, start from the migration overview. If you use both, migrate both — they share the same credentials. link: /partners/online-payments/migration/overview ```

Why migrate, and what you gain

The Ticket API runs on Teya's legacy platform, which is retiring in early 2027. Its replacement is Pay By Link: the same idea — a URL you create and share, opening a Teya-hosted payment page — on the new platform.

More completed payments — a modern payment page with built-in 3D Secure that fewer customers abandon
No code, if you don't want any — create and send links from the Business Portal or the Teya app
An API for links created in bulk or from your own systems
3D Secure handled on the payment page; Apple Pay available
Payment confirmations that can't get lost — a signed webhook carrying your own reference
Link expiry you control; links you can extend, resend or cancel
Full and partial refunds in the Business Portal or by API

Your options

Business Portal / Teya app Recommended for a handful of links a week — staff enter the amount, a description and your reference, and the Portal emails or texts the link to the customer. Status, resend, cancel and refund are all in the Portal. No developer needed: the migration is a walkthrough of Pay By Link in the Portal.

Pay By Link API — if your systems generate the URLs (invoicing runs, booking confirmations, reminders), your developer replaces each Ticket API call with one Payment Links call and handles a webhook. See the developer guide below.

Both work side by side on the same store.

What the migration involves

1 Decide: Portal only, or API
2 Credentials
3 Build and test
4 Go live
5 Retire the Ticket API

Portal only: done after a team walkthrough. API: days including testing, not weeks — a fraction of a checkout migration.

```admonition icon: fas fa-paper-plane title: What to send your developer content: (1) The link to this page. (2) Which systems create tickets today, and where the paid confirmation needs to land (invoicing, CRM, booking). (3) Whether links should expire, be resent, or accept Apple Pay. ```

Developer guide

Replace each Ticket API call with one Pay By Link call, and the legacy paid confirmation with a signed webhook.

How it works: before and after

Today (Ticket API)

  1. Your server calls the legacy API to pre-create a ticket for an amount and reference.
  2. You deliver the ticket URL to the customer.
  3. You learn of the payment through the legacy confirmation, or by checking.

After (Pay By Link)

  1. Your server obtains an OAuth token and calls POST /v2/payment-links. Teya returns a payment_link URL and a payment_link_id.
  2. You deliver the URL as before — or set customer.email and let Teya send it.
  3. The customer pays on a Teya-hosted page; 3D Secure is handled there.
  4. Teya sends a signed payment.succeeded.v1 webhook carrying payment_link_id, merchant_reference and transaction_id. You mark the invoice paid.

Biggest change: confirmation arrives as a signed webhook you verify server-side, not through a legacy callback.

Before you start

1. Credentials

Each merchant's client_id, client_secret, store_id and payment_webhook_public_key arrive through the merchant registration flow when the merchant connects Teya to your platform. For larger existing bases, agree a batch approach with your Teya partner contact. One set of credentials per store — never shared.

2. Environments and scopes

Staging api.teya.xyz / id.teya.xyz / business.teya.xyz; production .com — full table on the Pay By Link API page. Scopes: payment-links/create payment-links/id/get payment-links/id/update, plus refunds/create if you refund by API. Cache tokens by expires_in (currently 15 minutes in production, 24 hours in staging).

3. Webhook

The webhook registered at merchant registration receives that store's payment events; its payment_webhook_public_key is the key you verify with. See Webhooks.

4. Find the legacy code

Search for the Ticket API host and paths, the module that creates tickets, and the code that consumes the legacy paid confirmation. Each ticket-creation call site becomes one POST /v2/payment-links; the confirmation consumer becomes your webhook handler.

5. Ground rules

amount.value is an integer in minor units — €54.43 is 5443; ISK has no subunit, so 100 ISK is 100
Send an Idempotency-Key on every create; a retry with the same key returns the same link
Links are single-use — one link, one payment
Branding and payment-page text are Portal settings, not request fields — see Customize

What replaces what

Ticket API todayPay By LinkNotes
Ticket-creation callPOST /v2/payment-linksOr Create Payment Link in the Portal
Legacy credentials / per-request signatureOAuth client_credentials bearer tokenScope payment-links/create
Ticket URLpayment_linkDeliver it as today, or set customer.email / phone_number
Ticket identifierpayment_link_idStore it against the invoice or order
Amount and currencyamount.value, amount.currencyMinor units
Your order / invoice referencemerchant_referenceReturned in the webhook and on lookup
Description shown to the customerline_itemsPortal: Description
Ticket validity / expiryexpires_atExtend with PATCH /v2/payment-links/{id}
Cancelling a ticketPATCH with a past expires_atPortal: Cancel
Legacy paid confirmationpayment.succeeded.v1 webhookSigned; verify with the per-webhook key
Checking whether a ticket was paidGET /v1/payment-links/{id}Scope payment-links/id/get
RefundPOST /v3/refundsUses the webhook's transaction_id; or the Portal
3D SecureHandled on the hosted pageNothing to build

Step-by-step

cURL only. Other languages: see the Pay By Link API page.

1. Get an access token

Replaces the legacy per-request credentials. One token serves every call until it expires.

```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=payment-links/create payment-links/id/get payment-links/id/update refunds/create" ``` ```json { "access_token": "eyJhbGciOiJSUzI1NiJ9...", "token_type": "Bearer", "expires_in": 900 } ```
  • Cache the token; refresh a minute before expires_in runs out.
  • Separate credentials and token caches per environment and per store.

Full reference: Pay By Link API

2. Create the link

Replaces the ticket-creation call. Send the amount, your reference and, if Teya should deliver the link, the customer's details.

```bash curl -X POST https://api.teya.xyz/v2/payment-links \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "amount": { "value": 5443, "currency": "EUR" }, "merchant_reference": "INVOICE-2026-0117", "expires_at": "2026-10-31T23:59:59Z", "line_items": [ { "description": "Invoice 2026-0117", "quantity": 1, "unit_price": 5443 } ], "customer": { "name": "Jan Novák", "email": "jan@example.com" } }' ``` ```json { "payment_link_id": "20eb276c-5b6b-4474-a5a1-49bcd6193148", "payment_link": "https://checkout.teya.com/pbl/20eb276c-5b6b-4474-a5a1-49bcd6193148" } ```
  • Store payment_link_id against the invoice before you send anything.
  • Set expires_at to the invoice due date; an expired link shows the customer an error.
  • Optional fields (metadata, receipt emails, supported_payment_methods): see the reference.

Full reference: Pay By Link API · API Reference

3. Receive the payment webhook

Replaces the legacy paid confirmation. Verify the x-teya-signature header (SHA256withRSA, Base64) over the raw body with the webhook's public key, then match data.payment_link_id or data.merchant_reference to your invoice and mark it paid.

```json { "event": "payment.succeeded.v1", "data": { "transaction_id": "tr_019adef3-0d07-7543-9b74-efa2d5b6fa2a", "payment_link_id": "20eb276c-5b6b-4474-a5a1-49bcd6193148", "merchant_reference": "INVOICE-2026-0117", "amount": { "value": 5443, "currency": "EUR" }, "status": "SUCCESS" } } ```
  • Return 200 quickly, process asynchronously, and treat a replayed event as already handled.
  • The webhook fires only for successful payments — there is no failed or expired event.
  • Keep transaction_id: refunds and receipts need it.

Full reference: Webhooks — payload fields and the signature validator

After the payment

Status lookup. GET /v1/payment-links/{payment_link_id} returns the link's status, amount, merchant_reference and expires_at — use it before resending a reminder. The webhook remains the source of truth. To extend or cancel, PATCH /v2/payment-links/{payment_link_id} with a new expires_at (the only updatable field). See Pay By Link API.

Refunds. POST /v3/refunds with the webhook's transaction_id; partial and multiple refunds are allowed up to the original amount (scope refunds/create). Or refund in the Business Portal with no code. See Payment actions.

Receipts. The customer receives a receipt automatically. To send one yourself: POST /v1/transactions/{transaction_id}/receipts.

Pre-authorisation. Not available on payment links — transaction_type is SALE. To reserve funds and capture later, use Hosted Checkout with a PRE_AUTHORISATION session.

Testing and going live

Test in staging with the cards on the Testing page. Definition of done:

A link created by API opens a Teya page showing the right amount
Paid with a frictionless card: webhook received, signature verified, invoice marked paid once
Declined card: customer can retry; nothing marked paid. Replayed webhook does not double-mark
A paid link cannot be paid again; an expired link shows an error
Token refresh works across the 15-minute expiry
Full and partial refund succeed; over-refund is rejected
Staff can create, resend and cancel a link in the Portal without the API

Go live: production credentials and webhook (new key), swap .xyz hosts for .com, send yourself one real link and pay it, run the Ticket API in parallel for a day or two, then retire the legacy code and ask your Teya contact to decommission the old credentials.

Common errors

SymptomCauseFix
403 on createToken lacks payment-links/createRequest the scopes above when fetching the token
422, amount rejectedMajor units sent (54.43)Send minor units (5443); ISK at face value
Duplicate links after a retryNo Idempotency-KeyOne key per invoice attempt, reused on retry
Signature never verifiesBody re-serialised, or wrong keyVerify the raw bytes with this webhook's key
Invoice never marked paidStill waiting on the legacy callbackConfirmation now arrives only by webhook — check the endpoint is public HTTPS and returns 200
Customer reports "link expired"expires_at passedPATCH a later expires_at, or create a new link
```admonition icon: fas fa-life-ring title: Need help? content: Your Teya partner contact can provision staging accounts, development stores and the credentials route for your merchant base. For integration questions, see Support. ```

Next steps

---

Migrating stored cards & recurring payments

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.

Who this guide is for

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:

  • a multi-use token: tm_ followed by letters and digits, e.g. tm_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345
  • a virtual card number: 16 digits starting with 991001, e.g. 9910012837465190

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.

```admonition icon: fas fa-route title: Where this sits in the migration content: Step 1 — build and go live with your new-platform integration (Hosted Checkout, Embedded Components or the direct API) using the guide for your legacy integration: find it here. Step 2 — 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. ```

Why migrate, and what you gain

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.

No customer re-enters a card. The import and the first-use conversion happen behind the scenes.
No bulk conversion on your side. Your token database migrates itself, charge by charge, as your normal billing runs.
No gap. Both platforms run side by side until you switch, so there is never a moment when a customer could fall through.
Higher authorisation rates. 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.
One kind of token. At the end, every customer has a Teya token and one API charges them all — see Card on File.

What the migration involves

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 after that copy would be missing from it. So the first thing to stop is the creation of legacy tokens; the last thing to stop is their use.

1 New cards go to the new platform
2 Two platforms side by side
3 Teya imports your stored cards
4 Legacy tokens move on first use

Effort: 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.

```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. ```

Developer guide

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 Charge a Saved Payment Method; this page only covers what is specific to migrating.

Before you start

1. Prerequisites

  • Your integration already takes card payments on the new platform, with credentials per store — see the migration overview if not.
  • You can call POST /v3/transactions/online — the direct API used for token payments. Read Card on File and Charge a Saved Payment Method first; the requests below are those pages' requests with one different payment method.

2. Agree the phase dates with your Teya contact

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.

3. Find the code you'll be touching

Search your codebase for the places that:

  • create tokens on the legacy platform (/api/token/single, /api/token/multi, SecurePay tokenisation) — Phase 1's scope
  • charge stored values (tm_, 991001) — Phase 2's routing and Phase 4's scope

4. Ground rules

Tokens are resolved per merchant and store — charge a migrated token with the store_id it was migrated for.
Send legacy values exactly as stored; the format is validated strictly (max 64 characters, the two shapes above).
Store the returned token_response.token_id on every successful charge — it is the customer's permanent Teya token.
Put the Phase 4 cut-over behind a flag so rollback is a config change.

What replaces what

Legacy platformNew platformWhat it means for you
Multi-use token tm_… / virtual card 991001…Teya token — a UUID in token_response.token_idCharged with payment method TOKEN. Legacy values are accepted once more, as payment method LEGACY_TOKEN, to convert them (Phase 4).
POST /api/token/single, /api/token/multistore_payment_method: true on a SALE or a zero-amount ACCOUNT_VERIFICATION transactionTokenisation happens inside the payment; no separate call. See Card on File.
POST /api/payment with a multi-use tokenPOST /v3/transactions/online with payment_method.type: "TOKEN"Same endpoint as every other direct-API payment. See Charge a Saved Payment Method.
Recurring flag on the paymentinitiator (MERCHANT / CUSTOMER) + recurring_payment_type (RECURRING / INSTALLMENT / CARD_ON_FILE)MERCHANT = cardholder not present, no 3DS. CUSTOMER = cardholder present, 3DS applies and may challenge.
Token deletionDELETE /v1/tokens/{token_id} → 204Delete the token when the customer removes a saved card.
Your own token databaseYour own token database — now holding a Teya token (UUID) or, temporarily, a legacy valueThe routing rule below keys on which one the customer has.

Phase 1 — Saving new cards on the new platform

What: 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.

Why: every card saved from now on is already migrated, and the export in Phase 3 will be complete.

A token is created by setting store_payment_method: true on a transaction — as part of a sale (type: "SALE"), or without charging the customer (a zero-amount type: "ACCOUNT_VERIFICATION", for an "add a payment method" flow). Either way the response carries a token_response:

```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" } } } ```

Store token_id against the customer. brand and last4 are for what you show them ("Visa ending 1234").

Things to get right:

  • Token creation can fail while the payment succeeds: token_status: "FAILED" with a failure_reason. Treat it as "saving the card didn't work", not as a payment problem.
  • A token is only kept when the payment succeeds. Don't store anything from a failed transaction.
  • Disable legacy token creation everywhere — a forgotten code path undermines Phase 3.

Full reference: Card on File — storing the card.

Phase 2 — Two platforms side by side

What: 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.

Why: nothing is at risk while the import is prepared, and every card saved during this phase is already on the new platform.

```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" } } }' ```

Your billing engine's routing rule for this whole phase is one line:

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

Two fields describe why you are charging a stored card:

FieldValuesMeaning
initiatorMERCHANTThe cardholder is not present — a recurring charge, an instalment, or a merchant-initiated card-on-file payment. No 3DS/SCA is performed.
CUSTOMERThe 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.
recurring_payment_typeRECURRING, INSTALLMENT, CARD_ON_FILEThe type of stored-card usage. CUSTOMER-initiated payments support only CARD_ON_FILE; MERCHANT-initiated payments support all three.

Things to get right: amount.value is in the smallest currency unit (100 ISK = 100, 100 EUR = 10000). Send an Idempotency-Key per charge attempt.

Full reference: Charge a Saved Payment Method.

Phase 3 — Teya imports your stored cards

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.

Phase 4 — Moving legacy tokens to the new platform

What: 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:

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

What happens on first use. The first time each legacy token is charged on the new platform, it is upgraded in-line, inside that payment:

  1. 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.
  2. If not, the imported records are consulted under your merchant identity to find the stored card behind the token.
  3. The card details are retrieved — in memory, transiently; they never appear in responses, logs or events.
  4. A network token — a card credential issued by the card scheme itself — is created for that card, under your store.
  5. 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.
  6. Only then does the payment itself run — authorisation, persistence, webhook — using the network token.

Two properties of this design are worth knowing: the imported card record is never used to pay directly — 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.

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

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

The same token_response also appears on the transaction lookup and on the payment.succeeded.v1 webhook. Store the token_id, and from this customer's next charge on send it as payment_method.type: "TOKEN". Your routing rule becomes:

``` 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 ```

Things to get right:

  • Each billing cycle converts every customer it touches. A legacy token always resolves to the same Teya token, so charging with the legacy value again simply returns the same token_id — a lagging deploy or a webhook/response race does no harm.
  • 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.
  • Customer-present charges (initiator: "CUSTOMER") may return 202 with redirect_html for a 3DS challenge — handle it exactly as on Charge a Saved Payment Method. The conversion happens before the challenge, and the completed payment carries the same token_response.

Errors specific to legacy tokens

All errors use the API's standard shape — a code and a human-readable description. A payment that fails this way is never sent for authorisation: the customer is not charged and no webhook is emitted.

HTTPDescription you'll seeMeaningWhat to do
400Legacy token must be either a tm_ prefixed token or a 16-digit virtual card PAN starting with 991001The value doesn't match the two valid shapesFix the stored value or the lookup that produced it — this was never a valid token.
400Legacy token has not been migrated to a new token. Please contact support.No imported record for your store + this tokenEither 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.
400Legacy token payment method is not enabledThe legacy-token flow isn't switched on for your environment yetAsk your Teya contact about rollout status.
500Error resolving legacy tokenThe first-use conversion failed part-wayTransient — safe to retry later. Nothing was half-migrated.

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.

Testing and rolling out

Phase 1 go-live: deploy the new-card path, verify a real saved card end to end in staging with test cards, then disable legacy token creation everywhere.
Phase 2 is a steady state, not an event. There is no pressure on its duration.
Phase 3: confirm to your Teya contact that legacy token creation has stopped; keep your routing unchanged until the import is confirmed.
Phase 4 cut-over: 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 TOKEN charge. Then a full billing cycle, watching the failure ratio.
Wind down: 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. ```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 Support. ```

Next steps

---

Migration FAQ

The questions the migration guides don't answer step by step: timelines, what changes for the merchants you serve, migrating a merchant base, and what is different on the new platform. For the technical migration itself, start at the overview.

Before you start

When exactly will the legacy platform stop working?

SecurePay, the RPG payment API, the B-Gateway web service and the Ticket API keep working as normal today. Onboarding to them stops soon, and they are retired in early 2027. You will be given notice before anything changes for your customers, and your Teya contact can tell you where you stand at any time.

What happens if I do nothing?

Any merchant still routed through your legacy integration when the platform is retired will stop being able to take payments through your platform. Teya will contact merchants directly about the retirement, so merchants will also be asking you what your plan is.

We will contact you before your integration is affected — but don't wait for that: the work is the same now as later, and now is calmer.

Does migrating cost anything, and does my pricing change?

There is no Teya fee for migrating, and your pricing agreement with Teya is unchanged — it is not tied to which platform your integration uses. The only prerequisite is that Teya online payments are enabled on your store, which your Teya contact arranges.

How long does it take?

It depends on the path. A SecurePay redirect integration moving to Hosted Checkout is a small development project — days including testing, not weeks. Embedded Components and RPG integrations are a little more, mainly frontend work. B-Gateway integrations are a small development project too, and end with less code than you have today. Ticket API users often need no development at all. Stored cards and recurring payments are a separate, phased track that runs alongside normal billing cycles — see the stored cards guide.

Do I need a new merchant account, contract or credentials?

Your Teya merchant account and settlement stay the same. What changes is how your integration authenticates: instead of a SecurePay secret key, an RPG private key or B-Gateway credentials, each store gets OAuth API credentials (a Client ID and Client Secret). For merchants you already serve on RPG you exchange the existing key; for others, credentials arrive through the registration flow — see credentials for existing merchants. Your store needs Teya's online payments enabled; if credential creation reports that no compatible store was found, ask your Teya contact.

I don't know whether I'm on SecurePay, RPG or B-Gateway. How do I find out?

Ask whoever built your site where the customer types their card number. A Teya/Borgun page means SecurePay redirect; a frame inside your page means SecurePay embedded; your own form means a direct API. For a direct API, check what your server sends to borgun.is: JSON to /api/payment is RPG, XML such as getAuthorization to gateway01.borgun.is is B-Gateway. The overview turns the answers into a guide. If nobody knows, your Teya contact can look up how your payments arrive today.

During the migration

Can I run the old and new integrations in parallel?

Yes, and the guides recommend it: keep the legacy integration live for a day or two after you switch, so rolling back is a configuration change. Both settle to the same merchant account. There is no time limit on parallel running before the retirement date.

How do I test without touching live payments?

Everything is rehearsed in Teya's staging environment with a development store and test cards, including 3D Secure challenges, declines, refunds and webhooks. Nothing reaches your production account until you create production credentials and switch hosts.

Will my customers notice anything?

On Hosted Checkout the experience is the same shape as SecurePay: click Pay, enter the card on a Teya page, come back. Differences they may notice are improvements — a declined card lets them try another instead of ending the checkout, Apple Pay appears where the device supports it, and foreign cardholders may be offered to pay in their own currency. With Embedded Components the card fields sit inside your own checkout, so the visible change is that they no longer leave your site.

What about 3D Secure? I built my own MPI integration.

You delete it. Authentication runs automatically inside Hosted Checkout and the Embedded Components — enrollment, the bank challenge and validation are handled for you, with no separate calls and nothing to certify. The only place you still see 3D Secure explicitly is customer-present charges on saved cards through the direct API, where a challenge may be returned for you to display.

This also applies if you run 3D Secure through B-Gateway's get3DSAuthentication or a third-party 3D Secure provider: the connection is no longer needed. On the new platform every customer-present payment goes through 3D Secure, and charges without the customer are made on a card they saved with you, as merchant-initiated payments. See the B-Gateway guide.

After you switch

Can I still refund payments that were taken on SecurePay, RPG or B-Gateway?

Yes. Refund legacy transactions in B-Online exactly as you do today — it keeps working alongside your new integration and stays available until the legacy platform is retired. Refunds of payments taken on the new platform are made in the Business Portal or by API. Your legacy transactions are also visible in the Business Portal, and stay there.

Will I still be able to see my old transactions and reports?

Yes. Your legacy reporting stays available, read-only, until the legacy platform is retired. Your legacy transactions are also visible in the Teya Business Portal and remain there indefinitely, alongside new-platform payments — which appear in the Portal from your first live transaction.

Our systems pull transaction and settlement data from B-Gateway. What replaces that?

B-Gateway's transaction list, settlement, chargeback, dispute and fraud reports have no one-to-one API replacement. For a single payment, look up its checkout session by ID; for everything else, use the Business Portal. There are no batches on the new platform, so batch numbers disappear from your records: key them on the transaction_id instead. If a finance or reconciliation system depends on these reports, contact Teya support with your specific case before you migrate so we can check it with you. Each report is listed on the B-Gateway reference.

Do my customers' saved cards survive the move?

Yes. Nobody re-enters a card: Teya imports your stored cards, and each legacy token upgrades itself the first time you charge it on the new platform. The order of steps matters — read the stored cards & recurring guide before changing anything about how cards are saved.

This includes virtual card numbers from B-Gateway's getVirtualCard.

What happens to my old credentials and settings?

Once your parallel-running window is over, remove the legacy code; Teya retires the old SecurePay, RPG or B-Gateway credentials for you — nothing to request. Branding settings do not carry over: logo, checkout and receipt messages and legal URLs are configured once per store in the Business Portal.

What about chargebacks and disputes on legacy transactions?

Disputes are handled against your merchant account, not the integration, so a chargeback on a payment taken through SecurePay is handled the same way after you migrate.

Evidence for legacy transactions remains retrievable — they stay visible in the Business Portal.

How do I reconcile orders across the two platforms?

Use merchant_reference for your order number, and keep writing your legacy order ID into metadata during the transition if your back office keys on it. Every new-platform payment has a transaction_id that appears in the webhook, the session lookup and the Business Portal — store it on the order.

What is different on the new platform

Why are amounts sent as integers?

Amounts are in the smallest unit of the currency: €54.43 is 5443, £9.99 is 999. ISK has no subunit, so 100 ISK is simply 100. This is the single most common bug in migrations from SecurePay, which took a decimal string.

Why is there no webhook for failed payments?

The platform sends a signed webhook for successful payments (payment.succeeded.v1). Failures and abandoned checkouts are covered by looking the session up by its ID, which also tells you whether a customer who never returned to your site did in fact pay. See Webhooks.

Where do I see payments, refunds and settlements now?

In the Business Portal (and the Teya app), alongside your in-store payments if you have Teya card machines. Refunds can be issued there without a developer.

Settlement does not change: same merchant account, same payout timing and statements. New-platform transactions simply appear in the Business Portal as well.

Do refunds, captures and cancellations work the same way?

Refunds are full or partial, and can be repeated up to the original amount. A payment is captured at most once per authorisation, and the remainder is released. There is no separate "cancel": a refund on an uncaptured payment reverses the authorisation. See Payment Actions.

Can I still take pre-authorisations?

Yes — and if you were on SecurePay this is new. Create the session with type PRE_AUTHORISATION and capture later, including a partial amount. 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.

Does currency conversion (DCC) still work?

Yes, automatically: when a cardholder's currency differs from the transaction currency, the checkout offers to pay in their own currency. There is nothing to implement. DCC is not offered on pre-authorisations.

Which currencies and languages are supported?

The same ISO currency codes as before (ISK, EUR, GBP, CZK, HUF, USD and others). HRK is no longer used; Croatia settles in EUR. The checkout language is auto-detected from the customer's browser or set per session with a BCP 47 code such as is-IS or cs-CZ.

Checkout languages include English, Icelandic, Czech, Slovak, Croatian and Hungarian, among others.

What is different about security and PCI?

Card data is entered on Teya-hosted pages or inside Teya-hosted iframes, so your systems never see it and you stay in the lightest PCI category (SAQ A). Per-request signatures (checkhash) are replaced by short-lived access tokens, and payment confirmations are RSA-signed webhooks you verify with a public key rather than a shared secret. Sending raw card data to Teya from your own servers is restricted to merchants with PCI DSS (SAQ D) evidence.

If you use B-Gateway today, card numbers pass through your own servers. Moving to Embedded Components or Hosted Checkout takes them out entirely, which is usually the biggest reduction in compliance work of any migration path.

I use other B-Gateway features. What happens to them?

Some B-Gateway features work differently on the new platform or depend on your set-up: batches (settlement is now automatic), partial reversals (capture the final amount on a pre-authorisation, or refund the difference), Dynamic Descriptors, invoice detail data (sendDetailData), passenger itinerary data, batch authorisations, and telephone, keyed (B-Online) or card-reader payments. If you use any of them, contact Teya support with your specific case before you migrate. The B-Gateway reference lists every operation and where it lives now.

Are the test cards the same?

No. Use the official list on the Testing page; it covers frictionless and challenge 3D Secure, declines and specific error cases.

Migrating a merchant base

How do I get credentials for merchants I already serve, without asking them to re-onboard?

For merchants on RPG: exchange each merchant's RPG private key for new credentials with one API call per store — see RPG Credentials Migration. For merchants on SecurePay: register each merchant through the registration flow, where the merchant approves the connection once from their Teya account. To migrate a large merchant base, talk to your Teya partner contact about the approach.

Can I migrate merchants gradually?

Yes. Credentials are per store and every merchant switches independently, so you can move a handful, verify, and continue. Keep the legacy route available per merchant for a short rollback window.

Who tells the merchants, and what do they need to do?

Teya notifies every legacy merchant directly about the retirement. The preparation is managed by you; merchants may need to reconnect their store or approve the connection between your platform and their Teya account when the switch happens, so tell them what to expect. You know your merchants and your migration plan, so you decide how and when to communicate with them — most partners tell merchants in advance that the migration is handled by the platform, so nobody acts on Teya's notice by looking for a new provider. If you would like support or guidance with that communication, ask your Teya partner contact.

Which of my merchants are on the legacy platform?

Every merchant that pays through your legacy integration is on it — your own merchant records are the list. Use them to plan and track the migration store by store. If you need help identifying stores or reconciling them with Teya's records, ask your Teya partner contact.

A merchant uses my platform and their own direct integration. Who migrates what?

Each integration is migrated by whoever built it: you migrate your platform's integration for that merchant's store, the merchant's own developer follows the Businesses guides for theirs. Credentials are per store, so both can proceed independently.

Do I need to be certified, or identify my plugin?

No certification. If your merchants integrate through a plugin, Teya assigns a plugin_type for you to send with each credential exchange or registration; it identifies your integration for support and debugging.

How does the stored-card migration work across many merchants?

The four phases run per merchant, and the import in Phase 3 is per merchant. Agree phase dates with your Teya partner contact in batches, and confirm a merchant for import only once Phase 1 is complete for every store and code path. See the stored cards guide.

What if some of my merchants haven't moved by the retirement date?

Any merchant still on your legacy integration when the platform is retired will stop being able to take payments through it. Agree a migration plan and completion date with your Teya partner contact; we will contact you before any of your merchants is affected, and work with you to avoid it.

```admonition icon: fas fa-life-ring title: Question not answered here? content: Contact your Teya partner contact. For integration questions, see Support. ```

Next steps