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.
Not you?
Embedded form, Ticket API, stored cards or recurring payments, or you sell through a partner platform — a different guide applies.

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

What to send your developer
(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.

curl -X POST https://id.teya.xyz/oauth/v2/oauth-token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$TEYA_CLIENT_ID" \
  -d "client_secret=$TEYA_CLIENT_SECRET" \
  -d "scope=checkout/sessions/create checkout/sessions/id/get refunds/create"
{ "access_token": "eyJhbGciOiJSUzI1NiJ9...", "token_type": "Bearer", "expires_in": 900 }

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.

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

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",
  "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
Need help?
Your Teya account manager can provide a development store, credentials and a named migration contact. For integration questions, see Support.

Next steps