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.
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.
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
Portal only: done after a team walkthrough. API: days including testing, not weeks — a fraction of a checkout migration.
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)
- Your server calls the legacy API to pre-create a ticket for an amount and reference.
- You deliver the ticket URL to the customer.
- You learn of the payment through the legacy confirmation, or by checking.
After (Pay By Link)
- Your server obtains an OAuth token and calls
POST /v2/payment-links. Teya returns apayment_linkURL and apayment_link_id. - You deliver the URL as before — or set
customer.emailand let Teya send it. - The customer pays on a Teya-hosted page; 3D Secure is handled there.
- Teya sends a signed
payment.succeeded.v1webhook carryingpayment_link_id,merchant_referenceandtransaction_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 100Idempotency-Key on every create; a retry with the same key returns the same linkWhat replaces what
| Ticket API today | Pay By Link | Notes |
|---|---|---|
| Ticket-creation call | POST /v2/payment-links | Or Create Payment Link in the Portal |
| Legacy credentials / per-request signature | OAuth client_credentials bearer token | Scope payment-links/create |
| Ticket URL | payment_link | Deliver it as today, or set customer.email / phone_number |
| Ticket identifier | payment_link_id | Store it against the invoice or order |
| Amount and currency | amount.value, amount.currency | Minor units |
| Your order / invoice reference | merchant_reference | Returned in the webhook and on lookup |
| Description shown to the customer | line_items | Portal: Description |
| Ticket validity / expiry | expires_at | Extend with PATCH /v2/payment-links/{id} |
| Cancelling a ticket | PATCH with a past expires_at | Portal: Cancel |
| Legacy paid confirmation | payment.succeeded.v1 webhook | Signed; verify with the per-webhook key |
| Checking whether a ticket was paid | GET /v1/payment-links/{id} | Scope payment-links/id/get |
| Refund | POST /v3/refunds | Uses the webhook's transaction_id; or the Portal |
| 3D Secure | Handled on the hosted page | Nothing 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.
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"
{ "access_token": "eyJhbGciOiJSUzI1NiJ9...", "token_type": "Bearer", "expires_in": 900 }
- Cache the token; refresh a minute before
expires_inruns 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.
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" }
}'
{
"payment_link_id": "20eb276c-5b6b-4474-a5a1-49bcd6193148",
"payment_link": "https://checkout.teya.com/pbl/20eb276c-5b6b-4474-a5a1-49bcd6193148"
}
- Store
payment_link_idagainst the invoice before you send anything. - Set
expires_atto 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.
{
"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
200quickly, 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:
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
| Symptom | Cause | Fix |
|---|---|---|
403 on create | Token lacks payment-links/create | Request the scopes above when fetching the token |
422, amount rejected | Major units sent (54.43) | Send minor units (5443); ISK at face value |
| Duplicate links after a retry | No Idempotency-Key | One key per invoice attempt, reused on retry |
| Signature never verifies | Body re-serialised, or wrong key | Verify the raw bytes with this webhook's key |
| Invoice never marked paid | Still waiting on the legacy callback | Confirmation now arrives only by webhook — check the endpoint is public HTTPS and returns 200 |
| Customer reports "link expired" | expires_at passed | PATCH a later expires_at, or create a new link |
Next steps
- Pay By Link — App / Portal - Create and manage links with no code
- Pay By Link — API - Full request options and examples in other languages
- Webhooks - Payload fields and signature validator
- Testing - Test cards and integration checklist
- API Reference - Full endpoint documentation