<div id="integration-guide">

<div class="ig-section" id="ticket-api-payment-links">
<div class="ig-section-header">
<div class="ig-section-icon" style="background:#fef3c7;color:#d97706;"><i class="fas fa-link"></i></div>
<h2>Migrating from the Ticket API to Payment Links</h2>
</div>

<p class="ig-section-desc">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.</p>

<div class="ig-subsection">
<h3>Who this guide is for</h3>
<ul>
<li>Your systems or staff create a payment URL ("ticket") for a known amount before the customer pays.</li>
<li>The URL is delivered outside a website checkout — email, SMS, invoice, phone order.</li>
<li>You learn that a ticket was paid through the legacy confirmation, or by checking manually.</li>
</ul>

```admonition
icon: fas fa-random
title: Not you?
content: If customers pay in a checkout on your website, start from the migration overview. If you use both, migrate both — they share the same credentials.
link: /online-payments/migration/overview
```

</div>

<div class="ig-subsection">
<h3>Why migrate, and what you gain</h3>
<p>The Ticket API runs on Teya's legacy platform, which is retiring in <strong>early 2027</strong>. Its replacement is <strong>Pay By Link</strong>: the same idea — a URL you create and share, opening a Teya-hosted payment page — on the new platform.</p>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>More completed payments</strong> — a modern payment page with built-in 3D Secure that fewer customers abandon</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span><strong>No code, if you don't want any</strong> — create and send links from the Business Portal or the Teya app</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>An API for links created in bulk or from your own systems</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>3D Secure handled on the payment page; Apple Pay available</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Payment confirmations that can't get lost — a signed webhook carrying your own reference</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Link expiry you control; links you can extend, resend or cancel</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Full and partial refunds in the Business Portal or by API</span></div>
</div>
</div>

<div class="ig-subsection">
<h3>Your options</h3>
<p><strong>Business Portal / Teya app</strong> <span class="ig-badge ig-badge-recommended">Recommended for a handful of links a week</span> — 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 <a href="/online-payments/pay-by-link/app-portal">Pay By Link in the Portal</a>.</p>
<p><strong>Pay By Link API</strong> — if your systems generate the URLs (invoicing runs, booking confirmations, reminders), your developer replaces each Ticket API call with one <a href="/online-payments/pay-by-link/api">Payment Links</a> call and handles a webhook. See the developer guide below.</p>
<p>Both work side by side on the same store.</p>
</div>

<div class="ig-subsection">
<h3>What the migration involves</h3>
<div class="ig-flow-steps">
<div class="ig-flow-step"><span class="ig-flow-step-num">1</span> Decide: Portal only, or API</div>
<span class="ig-flow-arrow"><i class="fas fa-arrow-right"></i></span>
<div class="ig-flow-step"><span class="ig-flow-step-num">2</span> Credentials</div>
<span class="ig-flow-arrow"><i class="fas fa-arrow-right"></i></span>
<div class="ig-flow-step"><span class="ig-flow-step-num">3</span> Build and test</div>
<span class="ig-flow-arrow"><i class="fas fa-arrow-right"></i></span>
<div class="ig-flow-step"><span class="ig-flow-step-num">4</span> Go live</div>
<span class="ig-flow-arrow"><i class="fas fa-arrow-right"></i></span>
<div class="ig-flow-step"><span class="ig-flow-step-num">5</span> Retire the Ticket API</div>
</div>
<p>Portal only: done after a team walkthrough. API: days including testing, not weeks — a fraction of a checkout migration.</p>

```admonition
icon: fas fa-paper-plane
title: What to send your developer
content: (1) The link to this page. (2) Which systems create tickets today, and where the paid confirmation needs to land (invoicing, CRM, booking). (3) Whether links should expire, be resent, or accept Apple Pay.
```

</div>
</div>

<div class="ig-divider"></div>

<div class="ig-section" id="developer-guide">
<div class="ig-section-header">
<div class="ig-section-icon" style="background:#eff6ff;color:#2563eb;"><i class="fas fa-code"></i></div>
<h2>Developer guide</h2>
</div>
<p class="ig-section-desc">Replace each Ticket API call with one Pay By Link call, and the legacy paid confirmation with a signed webhook.</p>

<div class="ig-subsection">
<h3>How it works: before and after</h3>
<p><strong>Today (Ticket API)</strong></p>
<ol>
<li>Your server calls the legacy API to pre-create a ticket for an amount and reference.</li>
<li>You deliver the ticket URL to the customer.</li>
<li>You learn of the payment through the legacy confirmation, or by checking.</li>
</ol>
<p><strong>After (Pay By Link)</strong></p>
<ol>
<li>Your server obtains an OAuth token and calls <code>POST /v2/payment-links</code>. Teya returns a <code>payment_link</code> URL and a <code>payment_link_id</code>.</li>
<li>You deliver the URL as before — or set <code>customer.email</code> and let Teya send it.</li>
<li>The customer pays on a Teya-hosted page; 3D Secure is handled there.</li>
<li>Teya sends a signed <code>payment.succeeded.v1</code> webhook carrying <code>payment_link_id</code>, <code>merchant_reference</code> and <code>transaction_id</code>. You mark the invoice paid.</li>
</ol>
<p><strong>Biggest change:</strong> confirmation arrives as a signed webhook you verify server-side, not through a legacy callback.</p>
</div>

<div class="ig-subsection">
<h3>Before you start</h3>

<h4>1. Credentials</h4>

<p>Ask your Teya contact for a development store. In the <a href="https://business.teya.xyz" target="_blank" rel="noopener">Staging Business Portal</a> open the store → <strong>Integrations</strong> and create ecommerce API credentials: you get a <strong>Client ID</strong>, <strong>Client Secret</strong> and <strong>Store ID</strong>. Repeat in production at go-live.</p>

<h4>2. Environments and scopes</h4>
<p>Staging <code>api.teya.xyz</code> / <code>id.teya.xyz</code> / <code>business.teya.xyz</code>; production <code>.com</code> — full table on the <a href="/online-payments/pay-by-link/api">Pay By Link API page</a>. Scopes: <code>payment-links/create payment-links/id/get payment-links/id/update</code>, plus <code>refunds/create</code> if you refund by API. Cache tokens by <code>expires_in</code> (currently 15 minutes in production, 24 hours in staging).</p>

<h4>3. Webhook</h4>

<p>Register a webhook URL under the store's <strong>Integrations</strong>. The Portal issues a public key <em>for that webhook</em>; keep it with the credentials. See <a href="/online-payments/webhooks">Webhooks</a>.</p>

<h4>4. Find the legacy code</h4>
<p>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 <code>POST /v2/payment-links</code>; the confirmation consumer becomes your webhook handler.</p>

<h4>5. Ground rules</h4>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span><code>amount.value</code> is an integer in minor units — €54.43 is <code>5443</code>; ISK has no subunit, so 100 ISK is <code>100</code></span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Send an <code>Idempotency-Key</code> on every create; a retry with the same key returns the same link</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Links are single-use — one link, one payment</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Branding and payment-page text are Portal settings, not request fields — see <a href="/online-payments/pay-by-link/customize">Customize</a></span></div>
</div>
</div>

<div class="ig-subsection">
<h3>What replaces what</h3>
<div style="overflow-x:auto;">
<table class="ig-comparison">
<thead><tr><th>Ticket API today</th><th>Pay By Link</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>Ticket-creation call</td><td><code>POST /v2/payment-links</code></td><td>Or <strong>Create Payment Link</strong> in the Portal</td></tr>
<tr><td>Legacy credentials / per-request signature</td><td>OAuth <code>client_credentials</code> bearer token</td><td>Scope <code>payment-links/create</code></td></tr>
<tr><td>Ticket URL</td><td><code>payment_link</code></td><td>Deliver it as today, or set <code>customer.email</code> / <code>phone_number</code></td></tr>
<tr><td>Ticket identifier</td><td><code>payment_link_id</code></td><td>Store it against the invoice or order</td></tr>
<tr><td>Amount and currency</td><td><code>amount.value</code>, <code>amount.currency</code></td><td>Minor units</td></tr>
<tr><td>Your order / invoice reference</td><td><code>merchant_reference</code></td><td>Returned in the webhook and on lookup</td></tr>
<tr><td>Description shown to the customer</td><td><code>line_items</code></td><td>Portal: <strong>Description</strong></td></tr>
<tr><td>Ticket validity / expiry</td><td><code>expires_at</code></td><td>Extend with <code>PATCH /v2/payment-links/{id}</code></td></tr>
<tr><td>Cancelling a ticket</td><td><code>PATCH</code> with a past <code>expires_at</code></td><td>Portal: <strong>Cancel</strong></td></tr>
<tr><td>Legacy paid confirmation</td><td><code>payment.succeeded.v1</code> webhook</td><td>Signed; verify with the per-webhook key</td></tr>
<tr><td>Checking whether a ticket was paid</td><td><code>GET /v1/payment-links/{id}</code></td><td>Scope <code>payment-links/id/get</code></td></tr>
<tr><td>Refund</td><td><code>POST /v3/refunds</code></td><td>Uses the webhook's <code>transaction_id</code>; or the Portal</td></tr>
<tr><td>3D Secure</td><td>Handled on the hosted page</td><td>Nothing to build</td></tr>
</tbody>
</table>
</div>
</div>

<div class="ig-subsection">
<h3>Step-by-step</h3>
<p>cURL only. Other languages: see the <a href="/online-payments/pay-by-link/api">Pay By Link API page</a>.</p>

<h4>1. Get an access token</h4>
<p>Replaces the legacy per-request credentials. One token serves every call until it expires.</p>

```bash
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"
```

```json
{ "access_token": "eyJhbGciOiJSUzI1NiJ9...", "token_type": "Bearer", "expires_in": 900 }
```

<ul>
<li>Cache the token; refresh a minute before <code>expires_in</code> runs out.</li>
<li>Separate credentials and token caches per environment and per store.</li>
</ul>
<p>Full reference: <a href="/online-payments/pay-by-link/api">Pay By Link API</a></p>

<h4>2. Create the link</h4>
<p>Replaces the ticket-creation call. Send the amount, your reference and, if Teya should deliver the link, the customer's details.</p>

```bash
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" }
  }'
```

```json
{
  "payment_link_id": "20eb276c-5b6b-4474-a5a1-49bcd6193148",
  "payment_link": "https://checkout.teya.com/pbl/20eb276c-5b6b-4474-a5a1-49bcd6193148"
}
```

<ul>
<li>Store <code>payment_link_id</code> against the invoice before you send anything.</li>
<li>Set <code>expires_at</code> to the invoice due date; an expired link shows the customer an error.</li>
<li>Optional fields (<code>metadata</code>, receipt emails, <code>supported_payment_methods</code>): see the reference.</li>
</ul>
<p>Full reference: <a href="/online-payments/pay-by-link/api">Pay By Link API</a> · <a href="/apis/online-payments/apis">API Reference</a></p>

<h4>3. Receive the payment webhook</h4>
<p>Replaces the legacy paid confirmation. Verify the <code>x-teya-signature</code> header (SHA256withRSA, Base64) over the raw body with the webhook's public key, then match <code>data.payment_link_id</code> or <code>data.merchant_reference</code> to your invoice and mark it paid.</p>

```json
{
  "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"
  }
}
```

<ul>
<li>Return <code>200</code> quickly, process asynchronously, and treat a replayed event as already handled.</li>
<li>The webhook fires only for successful payments — there is no failed or expired event.</li>
<li>Keep <code>transaction_id</code>: refunds and receipts need it.</li>
</ul>
<p>Full reference: <a href="/online-payments/webhooks">Webhooks</a> — payload fields and the signature validator</p>
</div>

<div class="ig-subsection">
<h3>After the payment</h3>
<p><strong>Status lookup.</strong> <code>GET /v1/payment-links/{payment_link_id}</code> returns the link's <code>status</code>, amount, <code>merchant_reference</code> and <code>expires_at</code> — use it before resending a reminder. The webhook remains the source of truth. To extend or cancel, <code>PATCH /v2/payment-links/{payment_link_id}</code> with a new <code>expires_at</code> (the only updatable field). See <a href="/online-payments/pay-by-link/api">Pay By Link API</a>.</p>
<p><strong>Refunds.</strong> <code>POST /v3/refunds</code> with the webhook's <code>transaction_id</code>; partial and multiple refunds are allowed up to the original amount (scope <code>refunds/create</code>). Or refund in the Business Portal with no code. See <a href="/online-payments/payment-actions">Payment actions</a>.</p>
<p><strong>Receipts.</strong> The customer receives a receipt automatically. To send one yourself: <code>POST /v1/transactions/{transaction_id}/receipts</code>.</p>
<p><strong>Pre-authorisation.</strong> Not available on payment links — <code>transaction_type</code> is <code>SALE</code>. To reserve funds and capture later, use <a href="/online-payments/hosted-checkout/get-started">Hosted Checkout</a> with a <code>PRE_AUTHORISATION</code> session.</p>
</div>

<div class="ig-subsection">
<h3>Testing and going live</h3>
<p>Test in staging with the cards on the <a href="/online-payments/testing">Testing</a> page. Definition of done:</p>
<div class="ig-features">
<div class="ig-feature"><i class="fas fa-check"></i> <span>A link created by API opens a Teya page showing the right amount</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Paid with a frictionless card: webhook received, signature verified, invoice marked paid once</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Declined card: customer can retry; nothing marked paid. Replayed webhook does not double-mark</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>A paid link cannot be paid again; an expired link shows an error</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Token refresh works across the 15-minute expiry</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Full and partial refund succeed; over-refund is rejected</span></div>
<div class="ig-feature"><i class="fas fa-check"></i> <span>Staff can create, resend and cancel a link in the Portal without the API</span></div>
</div>
<p><strong>Go live:</strong> production credentials and webhook (new key), swap <code>.xyz</code> hosts for <code>.com</code>, 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.</p>
</div>

<div class="ig-subsection">
<h3>Common errors</h3>
<div style="overflow-x:auto;">
<table class="ig-comparison">
<thead><tr><th>Symptom</th><th>Cause</th><th>Fix</th></tr></thead>
<tbody>
<tr><td><code>403</code> on create</td><td>Token lacks <code>payment-links/create</code></td><td>Request the scopes above when fetching the token</td></tr>
<tr><td><code>422</code>, amount rejected</td><td>Major units sent (<code>54.43</code>)</td><td>Send minor units (<code>5443</code>); ISK at face value</td></tr>
<tr><td>Duplicate links after a retry</td><td>No <code>Idempotency-Key</code></td><td>One key per invoice attempt, reused on retry</td></tr>
<tr><td>Signature never verifies</td><td>Body re-serialised, or wrong key</td><td>Verify the raw bytes with <em>this webhook's</em> key</td></tr>
<tr><td>Invoice never marked paid</td><td>Still waiting on the legacy callback</td><td>Confirmation now arrives only by webhook — check the endpoint is public HTTPS and returns <code>200</code></td></tr>
<tr><td>Customer reports "link expired"</td><td><code>expires_at</code> passed</td><td><code>PATCH</code> a later <code>expires_at</code>, or create a new link</td></tr>
</tbody>
</table>
</div>

```admonition
icon: fas fa-life-ring
title: Need help?
content: Your Teya account manager can provide a development store and plan the switch-over. For integration questions, see <a href="/support">Support</a>; for links without code, see <a href="/online-payments/pay-by-link/app-portal">Pay By Link — App / Portal</a>.
```

</div>

<div class="ig-subsection">
<h3>Next steps</h3>
<ul>
<li><a href="/online-payments/pay-by-link/app-portal">Pay By Link — App / Portal</a> - Create and manage links with no code</li>
<li><a href="/online-payments/pay-by-link/api">Pay By Link — API</a> - Full request options and examples in other languages</li>
<li><a href="/online-payments/webhooks">Webhooks</a> - Payload fields and signature validator</li>
<li><a href="/online-payments/testing">Testing</a> - Test cards and integration checklist</li>
<li><a href="/apis/online-payments/apis">API Reference</a> - Full endpoint documentation</li>
</ul>
</div>

</div>
</div>
