Migrating from SecurePay to Hosted Checkout
For partners and platforms that redirect the customers of the merchants they serve to a SecurePay payment page. One server-side call replaces the signed form post; customers keep paying on a Teya-hosted page.
Who this guide is for
- Your platform sends merchants' customers to
securepay.borgun.is(ortest.borgun.is/SecurePay) to pay, and they come back to the merchant's site afterwards. - The integration posts an HTML form signed with a
checkhashand confirms payment from anorderhash. - The form is not embedded in your own page (
displaymode=iframe) — Hosted Checkout does not support iframes.
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.
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
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).
displaymode=iframe, the Ticket API or recurring payments — those have their own guides.
Developer guide
What replaces each SecurePay form parameter, and the one request you need at each step.
How it works: before and after
Today (SecurePay)
- The customer clicks Pay; the browser posts a
checkhash-signed form straight to SecurePay. - The customer pays on the SecurePay page.
- SecurePay redirects to
returnurlsuccessand posts anorderhashtoreturnurlsuccessserver.
After (Hosted Checkout)
- The customer clicks Pay; your server gets an access token and creates a checkout session, receiving a
session_idandsession_url. - You redirect the customer to the
session_url; they pay there, 3D Secure included. - Teya redirects to your
success_url(orcancel_url) and sends a signed webhook to your server. - Your server verifies the webhook (or looks the session up) and fulfils the order.
The biggest change: the browser no longer talks to Teya directly — your server makes one API call first, and confirmation comes from the server side, never the redirect.
Before you start
1. Credentials
Credentials are issued per store through the merchant registration flow: the merchant approves once from their Teya account, and Teya posts store_id, client_id, client_secret and payment_webhook_public_key to your registration webhook. Develop against a staging partner account and development store from your Teya partner contact; staging and production credentials never mix.
SecurePay equivalent: merchantid + paymentgatewayid become the Store ID; the secret key becomes the OAuth client credentials.
2. Environments
Staging is api.teya.xyz, id.teya.xyz and business.teya.xyz; production is the same hosts on .com. Full table: Hosted Checkout Get Started.
3. Webhook
Teya sends a signed webhook when a payment succeeds. Pass a public HTTPS endpoint with no authentication as paymentsWebhook in the registration URL; its public key arrives as payment_webhook_public_key in the registration callback, per webhook and per environment.
SecurePay equivalent: returnurlsuccessserver and its orderhash HMAC — same purpose, verified with an RSA signature instead. The webhook fires only for successful payments (payment.succeeded.v1); failures are covered by the session lookup in Step 5. See Webhooks.
4. Find the SecurePay code
Search your codebase for borgun.is, securepay, checkhash, orderhash, merchantid, paymentgatewayid, returnurlsuccess, returnurlsuccessserver, returnurlcancel, returnurlerror, itemdescription_, itemcount_, itemunitamount_, itemamount_, skipreceiptpage, merchantlogo, pagetype, displaymode. Each hit is a line on your to-do list. Found displaymode=iframe? Use SecurePay → Embedded Components instead.
5. Ground rules
What replaces what
| SecurePay | Hosted Checkout | What it means for you |
|---|---|---|
merchantid + paymentgatewayid | store_id | One UUID replaces both. |
Secret key + HMAC checkhash per request | client_id + client_secret → OAuth Bearer token | No 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 browser | POST /v2/checkout/sessions from your server, then redirect to session_url | The 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. |
currency | amount.currency | Same ISO 4217 codes. HRK is gone; Croatia uses EUR. |
orderid (12 alphanumeric) | merchant_reference + optional metadata | Free-form. Keep the legacy orderid in metadata during transition. |
reference | metadata keys | Up to 10 key-value pairs. |
language (IS, EN, CZ…) | language, BCP 47 (is-IS, cs-CZ) | Optional; browser language if omitted. |
buyername, buyeremail | customer.name, .email, .phone_number | Prefill only; they don't force collection. |
pagetype=1 | required_customer_fields: ["SHIPPING_ADDRESS", "PHONE", "EMAIL"] | Explicit per field; returned in the session lookup. |
itemdescription_0, itemcount_0, itemunitamount_0 | line_items[].description, .quantity, .unit_price | JSON array; unit price in minor units. Shown on the payment page. |
returnurlsuccess | success_url | Still a browser redirect; never a payment confirmation. |
returnurlcancel | cancel_url | Rendered as a Back option on the checkout. |
returnurlerror | failure_url | Terminal errors only. Declined cards stay on the checkout to retry. |
returnurlsuccessserver + orderhash POST | Webhook payment.succeeded.v1, RSA-signed (x-teya-signature) | Verify the signature, then fulfil. Success-only. |
Reading orderhash on the success return | Signed webhook, or GET /v2/checkout/sessions/{session_id} | Never trust the redirect to confirm success. |
skipreceiptpage=1 | post_success_payment: "REDIRECT" | Straight to your success page. SHOW_SUCCESS_PAGE shows Teya's receipt first. |
merchantlogo | Business Portal setting | Logo, checkout and receipt messages, privacy and T&C URLs are configured once — see Customize. |
displaymode=iframe | Not supported | Use Embedded Components. |
| Verified by Visa / SecureCode setup | Automatic 3D Secure | No code, no configuration. |
| Dynamic Currency Conversion | Automatic | Offered when the card currency differs from the transaction currency. |
| (no equivalent) | Pre-authorisation + capture | New. Session type: "PRE_AUTHORISATION"; see After the payment. |
| Ticket API | Payment Links (POST /v2/payment-links) | See Ticket API → Payment Links. |
| Recurring / stored cards | Card on File | One-off payments first, then Stored cards & recurring. |
| Refunds by manual process | POST /v3/refunds, or the Business Portal | Full, 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.valueis an integer in minor units: €54.43 is5443; 100 ISK is100. The most common bug in SecurePay migrations.Idempotency-Keyis 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_idagainst 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-signatureheader (SHA256withRSA, Base64) against the webhook's public key over the entire raw body, before parsing. - Match the order on
data.session_idordata.merchant_reference; storedata.transaction_id. - Be idempotent — the same event can arrive twice. Return
200promptly; 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
COMPLETED/SUCCESS; order fulfilled oncesuccess_urlcancel_url; no webhook; not fulfilled401 by refreshingGoing live
- Run the registration flow against production for the store; store its production credentials and webhook public key.
- Configure the Portal checkout settings: logo, messages, privacy and T&C URLs.
- Swap hosts via config:
id.teya.xyz → id.teya.com,api.teya.xyz → api.teya.com. - Process one small real transaction end to end: payment → webhook → refund.
- 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.
- 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.
| Symptom | Likely cause | Fix |
|---|---|---|
| Charged amount is 100× wrong | Minor-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 key | TTL from expires_in; refresh on 401 |
| Customer sees "Session expired" | Session created on cart load | Create it at the Pay click |
| Orders marked paid that weren't, or never marked paid | Fulfilling from the redirect, as with orderhash | Fulfil from the webhook; back it with the session lookup |
| Webhook never arrives | Endpoint not public, has auth, bad certificate, or returns non-200 | Public HTTPS, no auth; allowlist Teya's webhook IPs if you filter inbound traffic; always return 200 |
| Signature verification fails | Wrong key, or body parsed before verifying | Key for this webhook in this environment; verify the raw body; use the Signature Validator |
| Payment succeeded but not in the Portal | Wrong environment | Staging in business.teya.xyz, production in business.teya.com |
| "No compatible stores found" when creating credentials | Store lacks the ecommerce subscription | Ask your Teya contact to add it |
Next steps
- Hosted Checkout Get Started - The canonical guide, with code in other languages
- Customize - Branding, messages and session options
- Webhooks - Payload and signature verification
- Testing - Test cards and integration checklist
- API Reference - Full API endpoint documentation