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.
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.
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 Checkout | Embedded Components | |
|---|---|---|
| Customer experience | Redirect to a Teya-hosted page and back | Pays on your page, no redirect |
| Development effort | Lower — one server-side call and a redirect | Moderate — the same call plus a front-end SDK integration |
| Branding and layout | Logo and messages on Teya's page | Your page, your layout; themeable fields |
| Apple Pay | — | Built into the Checkout component |
| 3DS, DCC, refunds, webhooks, PCI | Identical | |
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.
- Card component — one card field in your own form with your own pay button. See Embedded Components.
- Hosted Checkout — the redirect path. See SecurePay → Hosted Checkout.
- Payment links — payment URLs, no website needed. See Ticket API → Payment Links.
What the migration involves
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.
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):
- Your page posts a form signed with a
checkhashto SecurePay — a redirect or an iframe (displaymode=iframe). - The customer pays on SecurePay's page or frame; 3D Secure runs there.
- SecurePay redirects to
returnurlsuccessand posts anorderhashtoreturnurlsuccessserver. - Your server verifies the
orderhashand fulfils the order.
After (Embedded Components):
- Your server gets an OAuth access token and creates a checkout session, receiving a
session_token. - 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.
- The customer pays in place; 3D Secure runs inside the component.
- Your
onSuccess/onErrorcallback 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
| Staging | Production | |
|---|---|---|
| API | https://api.teya.xyz | https://api.teya.com |
| OAuth | https://id.teya.xyz/oauth/v2/oauth-token | https://id.teya.com/oauth/v2/oauth-token |
| SDK | developmentMode: true | flag 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
session_tokenonSuccess is for UX; the webhook is the source of truthWhat replaces what
| SecurePay (today) | Embedded Components (new) | What it means for you |
|---|---|---|
merchantid + paymentgatewayid | store_id | One UUID replaces both. |
Secret key + HMAC checkhash | client_id + client_secret → OAuth Bearer token | No per-request HMAC. Cache and refresh from expires_in (currently 15 min in production, 24 h in staging). |
| HTML form posted to SecurePay | POST /v2/checkout/sessions → SDK mounts the component with the session_token | The browser never calls Teya's API; it only hosts the component. |
displaymode=iframe + iframe result plumbing | The component is the embedded experience | Teya-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. |
currency | amount.currency — ISO 4217 alphabetic | ISK, EUR, CZK, HUF, GBP… |
orderid (max 12 alphanumeric) | merchant_reference + optional metadata | Write the legacy orderid into metadata during transition if it helps reconciliation. |
language | SDK locale option | Set on initTeyaBlocks. |
buyername, buyeremail | customer.name, customer.email on the session | Prefill only. |
itemdescription_0 … | line_items[] on the session | Description, quantity, unit price in minor units. |
returnurlsuccess / returnurlcancel / returnurlerror | onSuccess / onError callbacks | No redirects; your page decides what happens next. |
returnurlsuccessserver + orderhash | Webhook payment.succeeded.v1, RSA-signed | Verify the signature over the raw body, then fulfil. |
skipreceiptpage, merchantlogo, pagetype | Not applicable — there is no Teya page | Theme the component to match your page. |
| Verified by Visa / MC SecureCode | Automatic 3D Secure inside the component | Challenge renders inline. Nothing to build. |
| Dynamic Currency Conversion | Automatic in the Checkout and Card components | Nothing to implement. |
| (no equivalent) | Apple Pay | In the Checkout component; one-off domain verification (Step 3). |
| (no equivalent) | Pre-authorisation + partial capture | See After the payment. |
| Ticket API | Payment links | Ticket API → Payment Links |
| Recurring payments / stored cards | Card on File | Migrate last — stored cards & recurring. |
| Refunds via portal / manual | POST /v3/refunds or the Business Portal | Partial 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.valueis an integer in minor units — €54.43 is5443; ISK face value as-is. The most common migration bug.Idempotency-Keyis required and unique per order attempt.- The
session_tokenis 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_tokento the front end through a small endpoint of your own (e.g.POST /api/teya-session) — never the access token or client secret. Store thesession_idagainst 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: trueis 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
200promptly; 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
onSuccess fires, webhook verified, session COMPLETED / SUCCESS, order fulfilled once401 by refreshingGo live
- Obtain each merchant's production credentials and webhook public key through the registration flow and store them against that merchant.
- Verify your domain for Apple Pay in production if you use it — staging verification doesn't carry over.
- Swap hosts via config (
id.teya.xyz→id.teya.com,api.teya.xyz→api.teya.com) and removedevelopmentMode: true. - Process one small real transaction end to end: payment → webhook → refund.
- Run SecurePay in parallel behind a flag for a day or two; watch logs, webhook deliveries and the Business Portal.
- 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
| Symptom | Likely cause | Fix |
|---|---|---|
| Staging session token rejected by the SDK | developmentMode: true missing | Add it in staging; remove in production |
| Payments land in the wrong environment | developmentMode shipped to production, or hosts mixed | Gate on environment config; audit hosts |
| Charged amount is 100× wrong | Decimal → minor-units conversion missed | €54.43 is 5443; ISK face value as-is |
| Component won't mount / token error | Session token already used, or session expired | One token per mount; create a fresh session |
401 on API calls | Access token expired | Refresh; derive cache lifetime from expires_in |
403 / 409 | Missing scope / same Idempotency-Key, different body | Add the scope (Step 1) / unique key per order attempt |
| Webhook never arrives | URL unreachable, firewall, or non-200 response | Public HTTPS, no auth; allowlist Teya IPs; return 200 |
| Signature verification fails | Wrong key, or a parsed body | Per-webhook, per-environment key; verify the raw body; use the Signature Validator |
| Apple Pay button doesn't render | Domain not verified in this environment | Verify staging and production separately |
Next steps
- Embedded Checkout - Interactive quickstart in HTML/JS, React and five backend languages
- SDK Initialization -
initTeyaBlocks()options anddevelopmentMode - Webhooks - Payload fields and signature verification
- Testing - Test cards and integration checklist
- Stored cards & recurring - The last stage of the migration