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)

  1. Your server calls the legacy API to pre-create a ticket for an amount and reference.
  2. You deliver the ticket URL to the customer.
  3. You learn of the payment through the legacy confirmation, or by checking.

After (Pay By Link)

  1. Your server obtains an OAuth token and calls POST /v2/payment-links. Teya returns a payment_link URL and a payment_link_id.
  2. You deliver the URL as before — or set customer.email and let Teya send it.
  3. The customer pays on a Teya-hosted page; 3D Secure is handled there.
  4. Teya sends a signed payment.succeeded.v1 webhook carrying payment_link_id, merchant_reference and transaction_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 100
Send an Idempotency-Key on every create; a retry with the same key returns the same link
Links are single-use — one link, one payment
Branding and payment-page text are Portal settings, not request fields — see Customize

What replaces what

Ticket API todayPay By LinkNotes
Ticket-creation callPOST /v2/payment-linksOr Create Payment Link in the Portal
Legacy credentials / per-request signatureOAuth client_credentials bearer tokenScope payment-links/create
Ticket URLpayment_linkDeliver it as today, or set customer.email / phone_number
Ticket identifierpayment_link_idStore it against the invoice or order
Amount and currencyamount.value, amount.currencyMinor units
Your order / invoice referencemerchant_referenceReturned in the webhook and on lookup
Description shown to the customerline_itemsPortal: Description
Ticket validity / expiryexpires_atExtend with PATCH /v2/payment-links/{id}
Cancelling a ticketPATCH with a past expires_atPortal: Cancel
Legacy paid confirmationpayment.succeeded.v1 webhookSigned; verify with the per-webhook key
Checking whether a ticket was paidGET /v1/payment-links/{id}Scope payment-links/id/get
RefundPOST /v3/refundsUses the webhook's transaction_id; or the Portal
3D SecureHandled on the hosted pageNothing 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_in runs out.
  • Separate credentials and token caches per environment and per store.

Full reference: Pay By Link API

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_id against the invoice before you send anything.
  • Set expires_at to 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 200 quickly, 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:

A link created by API opens a Teya page showing the right amount
Paid with a frictionless card: webhook received, signature verified, invoice marked paid once
Declined card: customer can retry; nothing marked paid. Replayed webhook does not double-mark
A paid link cannot be paid again; an expired link shows an error
Token refresh works across the 15-minute expiry
Full and partial refund succeed; over-refund is rejected
Staff can create, resend and cancel a link in the Portal without the API

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

SymptomCauseFix
403 on createToken lacks payment-links/createRequest the scopes above when fetching the token
422, amount rejectedMajor units sent (54.43)Send minor units (5443); ISK at face value
Duplicate links after a retryNo Idempotency-KeyOne key per invoice attempt, reused on retry
Signature never verifiesBody re-serialised, or wrong keyVerify the raw bytes with this webhook's key
Invoice never marked paidStill waiting on the legacy callbackConfirmation now arrives only by webhook — check the endpoint is public HTTPS and returns 200
Customer reports "link expired"expires_at passedPATCH a later expires_at, or create a new link
Need help?
Your Teya account manager can provide a development store and plan the switch-over. For integration questions, see Support; for links without code, see Pay By Link — App / Portal.

Next steps