# Teya online payments — migrating from RPG & SecurePay (merchant edition) Source: https://docs.teya.com/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

If your website or systems have been taking online card payments through Borgun — or through Teya before the new platform — this section is for you. You may know the old setup as "the Borgun payment page", SecurePay, RPG, the Borgun Payment API, B-Gateway or the Ticket API; whichever name you recognise, find the row below that describes how you take payments today. That is your guide.

```admonition icon: fas fa-handshake title: Do you take payments on behalf of other merchants? content: Platforms, plugins and agencies that migrate several merchants at once have their own section, including how to obtain credentials for existing merchants without re-onboarding them. link: /partners/online-payments/migration/overview ```

What is changing

Teya's legacy online payment systems — the Borgun payment page (SecurePay), the Borgun payment API (RPG), the B-Gateway web service and the Ticket API — continue to work as normal today, and nothing changes for you right now. Onboarding to those systems will stop soon, and they will be retired in early 2027. Before then, your integration needs to move to the new Teya online payments platform.

The right route depends on your current setup. The effort varies from nothing at all (if you sell through a partner platform) to a small development project, typically days including testing. Every path below is live today and can be started immediately.

Find your path

If more than one row applies — a webshop checkout and payment links, for example — migrate each with its own guide.

How you take payments todayTypical signsYour migration path
Customers are redirected to a SecurePay payment page Your site posts a form to securepay.borgun.is; your developer set up a "payment form post" with a checkhash SecurePay → Hosted Checkout — the like-for-like path: lowest effort, same redirect experience for your customers
The SecurePay payment form is embedded in your own checkout displaymode=iframe in your integration; customers pay without leaving your site SecurePay → Embedded Components — keeps the payment form on your page (Hosted Checkout can't)
You redirect to SecurePay but want an on-site checkout You care about checkout conversion and branding, or want Apple Pay on your own page SecurePay → Embedded Components — read its "which is right for you" section first
Your systems call the RPG payment API directly Code calling /api/payment on borgun.is; a private access token; your own 3D Secure (MPI) integration RPG → Embedded Components
Your systems call the B-Gateway web service SOAP/XML requests such as getAuthorization to gateway01.borgun.is; a B-Gateway username and password; card numbers or virtual card numbers (9910…) reach your own servers; often no 3D Secure. Also known as BGateway or Greiðslugátt B-Gateway → Embedded Components: card numbers no longer pass through your systems
You create payment URLs and send them to customers Payment links made in advance for email, phone or invoice orders (the Ticket API) Ticket API → Payment Links — often no code at all; links can be created in the Business Portal
You store customers' cards or take recurring payments Multi-use tokens (tm_…) or virtual card numbers (16 digits starting 9910); subscriptions or instalments charged when the customer isn't present Migrate your one-off payments first with the row that matches you, then follow Stored cards & recurring — your customers' saved cards carry over without re-entering a card
You sell through a partner platform A booking system, webshop provider or till system built the integration — you never did The preparation is managed for you by your platform provider. Watch for their instructions — you may need to reconnect your store or sign in again when the switch happens. If in doubt, ask them or your Teya contact.
You use a Teya plugin for WooCommerce or Shopify A Teya or Borgun plugin installed in your shop admin Install the current Teya plugin and connect it to your Teya account — see Connect with e-commerce platforms and Set up and connect your plugin to Teya in the Help Centre. Watch for instructions from Teya; you may need to sign in again when your store switches over.
```admonition icon: fas fa-robot title: Planning to use an AI coding assistant? content: It can save your developer a lot of time on this migration. Migrate with an AI assistant has ready-made prompts for identifying your legacy integration, building the migration and raising questions with Teya, plus a single downloadable file containing this whole section for the assistant to read. ``` ```admonition icon: fas fa-question-circle title: Not sure which row is you? content: Ask your developer, or whoever built your site, three questions. "Where does the customer type their card number?" — a Teya/Borgun page means redirect; a frame inside your page means embedded; your own form means a direct API (RPG or B-Gateway). "Do any of our systems call an address containing borgun.is?" "Do we send JSON to /api/payment, or XML such as getAuthorization?" — JSON is RPG, XML is B-Gateway. Between them, the answers place you in one row. Still unsure? Contact your Teya account manager and we'll look at your setup together. ```

What you gain

This is more than a platform change. The new platform was built from the ground up and removes the most dated parts of the legacy integrations.

More completed payments — merchants on the new platform generally see higher conversion, thanks to modern 3D Secure with fewer challenges, less checkout drop-off, and higher authorisation rates on network tokens for stored cards
A smoother checkout — a clean, trusted payment flow with built-in 3D Secure, card retries without losing the sale, and wallets like Apple Pay
Flexibility in how you integrate — ready-to-use Shopify and WooCommerce plugins, a hosted checkout page, or embedded components on your own site
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 to your site
Less security and compliance complexity — Teya handles the sensitive parts of the payment flow; card data never touches your servers
One Teya setup across channels — online payments in the Teya app and Business Portal alongside the rest of your business, including refunds without a developer
A platform that keeps evolving — continued investment in checkout features, payment methods and ecommerce capabilities

Migration guides

Every guide has the same shape: a short first part for the business owner — why, what you gain, what to send your developer — and a developer guide with how it works before and after, a table of what replaces what, and the migration steps.

SecurePay → Hosted Checkout

The like-for-like path

One server-side call replaces the signed form post; your customer still pays on a Teya page and comes back to your site.

Lowest effort

SecurePay → Embedded Components

Keep the checkout on your site

For iframe integrations, or for anyone who wants an on-site checkout with Apple Pay. Includes a Hosted Checkout comparison.

Some code

RPG → Embedded Components

For direct API integrations

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

Some code

B-Gateway → Embedded Components

For SOAP/XML 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.

Some code

Ticket API → Payment Links

For pre-created payment URLs

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

No code option

Stored cards & recurring

Do this after your one-off payments

Your customers' saved cards move to the new platform without anyone re-entering a card. Four phases, no bulk conversion on your side.

Reference: what replaces what

Whichever path you take

Test first. Every guide walks through a full rehearsal in Teya's staging environment with test cards before any real traffic moves.
Nothing switches until you're ready. Your current integration keeps working in parallel while you build and test, and for a safety window after you switch.
Help is a message away. Your Teya account manager can set you up with a development store today — that is step one of every path. ```admonition icon: fas fa-life-ring title: Need help? content: Contact your Teya account manager, or reply to the email that pointed you here, to get a development store and a named contact for your migration. For integration questions, see Support. ```

Next steps

---

Migrating from SecurePay

This page is for businesses whose website posts 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: New credentials content: Every integration below authenticates with new ecommerce credentials, created per store in the Business Portal — see Authenticate. They replace your SecurePay merchant ID, gateway ID and secret key. ```

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: New credentials content: Every integration below authenticates with new ecommerce credentials, created per store in the Business Portal — see Authenticate. ```

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 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. The new platform uses OAuth credentials created per store in the Business Portal — see Authenticate. There is nothing to exchange: create the new credentials, then retire the old ones when you switch. ```

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 account manager 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 account manager 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 businesses whose website redirects customers to a SecurePay payment page. One server-side call replaces the signed form post; your customers keep paying on a Teya-hosted page.

Who this guide is for

  • Your website sends customers to securepay.borgun.is (or test.borgun.is/SecurePay) to pay, and they come back to your 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: /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 developer content: (1) Ask your Teya account manager for a development store. (2) Forward this page. (3) Ask them to confirm whether your site has a backend today, and whether you also use 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

Ask your Teya contact for a development store with an ecommerce subscription. In the Staging Business Portal, select the store, go to Settings → Integrations and create an ecommerce API credential: you get a Client ID, a Client Secret (shown once) and your Store ID. Repeat at business.teya.com for production; the two sets never mix. See Authenticate.

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. Register a public HTTPS endpoint with no authentication in the Business Portal (Settings → Integrations → Add webhook); the Portal issues a public key for that webhook, 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. Create production credentials and register your production webhook at business.teya.com; store the new 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 account manager can provide a development store, credentials and a named migration contact. For integration questions, see Support. ```

Next steps

---

Migrating from SecurePay to Embedded Components

For businesses that take payments through SecurePay today and want the new checkout to live on your own website — the payment form inside your page, no redirect. Part 1 is for the business owner; Part 2 is for your developer.

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

Ask your Teya contact for a development store. In the Staging Business Portal, select the store, go to Settings → Integrations and create an ecommerce API credential: you receive a Client ID, a Client Secret (shown once) and your Store ID. Repeat in the production Business Portal at go-live; never mix the two.

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 in the Business Portal (Settings → Integrations → Add webhook). The Portal issues a public key per webhook, 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. Create production credentials in the Business Portal and register your production webhook; store the public key it issues.
  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 account manager can provide a development store, confirm pre-authorisation eligibility and schedule your migration. For integration questions, see Support. ```

Next steps

---

Migrating from RPG to Embedded Components

For businesses whose website talks directly to RPG (Borgun Payment API). The first half is for the business owner; the second half is for your developer.

Who this guide is for

  • Your code calls URLs containing borgun.is, 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: /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 send your developer content: (1) A link to this page, pointing at the developer guide below. (2) A development store from your Teya account manager, for staging credentials. (3) Two questions to answer back: does the integration use pre-authorisations, and does it use stored cards or 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

Ask your Teya account manager for a development store. In the Staging Business Portal, select the store, go to Integrations and create ecommerce API credentials: a Client ID, a Client Secret (shown once) and your Store ID. Repeat at business.teya.com for production; never mix the two. This replaces your Private and Public Access Tokens. See Authenticate.

2. Environments

Staging is api.teya.xyz / id.teya.xyz / business.teya.xyz with developmentMode: true in the SDK; production is the same hosts on .com without the flag. Full table on Embedded Checkout.

3. Webhook

Teya sends a signed payment.succeeded.v1 event to a public HTTPS endpoint with no authentication on it.

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

Unlike RPG's PaymentCreate / Refund / Capture events, only successful customer payments produce a webhook today; refunds and captures you trigger return their outcome in the API response. See Webhooks.

4. Find the legacy code

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

5. Ground rules

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. Create production credentials and register the production webhook in the Business Portal at business.teya.com; store the new public key.
  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 account manager can provide a development store, confirm pre-authorisation availability and plan the stored-card migration. For integration questions, see Support. ```

Next steps

---

Migrating from B-Gateway to Embedded Components

For businesses whose systems send payments to B-Gateway directly: the Borgun payment gateway web service (SOAP/XML), also written BGateway and known in Iceland as Greiðslugátt or simply "the Borgun web service". The first half is for the business owner; the second half is for your developer.

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: /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 send your developer content: (1) A link to this page, pointing at the developer guide below. (2) A development store from your Teya account manager, for staging credentials. Card saving (tokenization) is enabled by default, but enrolment can take up to 48 hours, so ask for the store early. (3) Questions to answer back: do you charge cards without the customer present (subscriptions, instalments, top-ups), and are those cards virtual card numbers from B-Gateway or card numbers you store yourself; do you use pre-authorisations; and do you 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

Ask your Teya account manager for a development store. In the Staging Business Portal, select the store, go to Integrations and create ecommerce API credentials: a Client ID, a Client Secret (shown once) and your Store ID. Repeat at business.teya.com for production; never mix the two. This replaces your B-Gateway username, password, Processor and MerchantID. 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 in the Business Portal next to your credentials; the portal issues 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 account manager.
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 account manager 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. Create production credentials and register the production webhook in the Business Portal at business.teya.com; store the new public key. If you save cards, create the production store 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 account manager can provide a development store and plan the move of your saved cards. For integration questions and anything under "Using other features", see Support. ```

Next steps

---

Migrating from the Ticket API to Payment Links

For businesses that create payment URLs in advance with the Ticket API and send them to customers by email, invoice or phone. This guide moves that flow to Pay By Link — with or without code.

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

Ask your Teya contact for a development store. In the Staging Business Portal open the store → Integrations and create ecommerce API credentials: you get a Client ID, Client Secret and Store ID. Repeat in production at go-live.

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

Register a webhook URL under the store's Integrations. The Portal issues a public key for that webhook; keep it with the credentials. 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 account manager can provide a development store and plan the switch-over. For integration questions, see Support; for links without code, see Pay By Link — App / Portal. ```

Next steps

---

Migrating stored cards & recurring payments

For businesses that 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 checkout moves to the new platform using the guide for your integration, then — once that integration is built and live — your customers' stored cards move too, without anyone re-entering a card, in four phases that you control.

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: What to send your developer content: (1) This page. (2) The name of your Teya contact, who agrees the phase dates and confirms the import. (3) A list of every system that creates or charges stored cards — every store, every code path, every background job — because Phase 1 has to be complete everywhere before Phase 3 can start. ```

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 contact is the route for agreeing the phase dates, confirming the 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: what happens if you wait, what changes for your customers and your back office, 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?

If your integration is still on the legacy platform when it is retired, your website will no longer be able to take card payments through it. If you sell through a partner platform or use a Teya plugin, the migration preparation is managed for you — but watch for further instructions from your provider or from Teya, because you may need to reconnect your store or sign in again when the switch happens.

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 a B-Gateway username and password, each store gets OAuth API credentials (a Client ID and Client Secret). You create them yourself in the Business Portal under the store's Integrations settings. 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.

I no longer have a developer. What are my options?

Three, in increasing order of effort: switch to a Teya plugin if your shop runs on WooCommerce or Shopify; use Payment Links from the Business Portal or Teya app if you mostly invoice customers; or have any web developer or agency follow the Hosted Checkout guide — it is written so a developer who has never seen SecurePay can do it.

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.

My site is plain HTML with no backend. Can I still migrate?

Yes, but you need one small server-side component: the new platform uses credentials that must never reach the browser, so a session is created from your server before the customer is redirected. A single endpoint — a PHP file on your host or a serverless function — is enough. See Step 1 of the Hosted Checkout guide. If that is more than you want, Payment Links or a plugin need no backend at all.

I use a Teya or Borgun plugin for WooCommerce or Shopify.

Install the current Teya plugin for your platform and connect it to your Teya account — it already uses the new platform. Watch for instructions from Teya, as you may need to sign in again when your store switches over. See Connect with e-commerce platforms and the Help Centre article Set up and connect your plugin to Teya.

A partner platform or agency built my integration. Who does the work?

Your provider does. Partner platforms migrate their integration once for all their merchants; the preparation is managed for you, but watch for their instructions — you may be asked to reconnect your store or approve a connection to your Teya account when the switch happens. Agencies that built a custom integration for you follow the same guides as your own developer would.

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.

```admonition icon: fas fa-life-ring title: Question not answered here? content: Contact your Teya account manager, or reply to the email that pointed you here. For integration questions, see Support. ```

Next steps