Migrating from SecurePay to Embedded Components

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

Who this guide is for

  • Your SecurePay form is embedded today — an iframe, or displaymode=iframe. Hosted Checkout does not support iframe embedding; Embedded Components is the path that keeps the form on the page.
  • You redirect to SecurePay today but want to upgrade — keep customers on the site through payment, in your layout, with Apple Pay built in.
Not you?
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.

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.

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

Developer guide

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

How it works: before and after

Today (SecurePay):

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

After (Embedded Components):

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

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

Before you start

1. Credentials

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

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

2. Environments

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

3. Webhook

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

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

4. Find the legacy code

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

5. Ground rules

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

What replaces what

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

Step-by-step

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

Step 1 — Get an access token (server)

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

curl -X POST https://id.teya.xyz/oauth/v2/oauth-token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$TEYA_CLIENT_ID" \
  -d "client_secret=$TEYA_CLIENT_SECRET" \
  -d "scope=checkout/sessions/create checkout/sessions/id/get refunds/create"
{ "access_token": "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.

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" }
  }'
{
  "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.

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.

curl https://api.teya.xyz/v2/checkout/sessions/$SESSION_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "session_id": "14411456-79c8-4d8c-8498-f01907093b83",
  "session_status": "COMPLETED",
  "amount": { "value": 5443, "currency": "EUR" },
  "type": "SALE",
  "payment_status": "SUCCESS"
}

Things to get right: session_status runs ACTIVE → PROCESSING → COMPLETED / EXPIRED; trust payment_status only once the session is COMPLETED. Store the transaction_id from the completed session or the webhook — you need it for refunds and receipts. Full reference: API reference.

Step 5 — Receive and verify the webhook (server)

Teya posts a signed payment.succeeded.v1 event to your webhook URL when a payment succeeds — your fulfilment trigger, replacing returnurlsuccessserver, and one that no longer depends on the customer's browser. Verify the x-teya-signature header (SHA256 with RSA, Base64) against the public key for this webhook, over the entire raw body, before parsing. Match the order on data.merchant_reference or data.session_id.

Things to get right:

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

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

After the payment

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

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

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

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

Pre-authorisations (new). SecurePay never offered holds; the new platform does. Pre-authorisations are currently available for certain tourism-related business activities; if your business needs them for another use case, contact your Teya account manager. Create the session with "type": "PRE_AUTHORISATION"; the component flow is otherwise identical (no DCC on pre-authorisations). Capture once per authorisation, any amount up to the total, with POST /v1/transactions/{transaction_id}/capture (scope captures/create); the remainder is released. To release a hold early, refund the uncaptured payment — that reverses it. See Payment actions.

Testing and going live

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

Definition of done

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

Go live

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

Common errors

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

Next steps