Payment Links
A payment link lets you create a shareable URL for a single payment request without building Card Handoff integration. The buyer opens this URL in their own browser, picks an installment count (if offered), and completes the payment themselves; your checkout does not collect the card. It is an alternative to the POST /v1/payments + handoff flow, useful when you want to avoid the card surface entirely (for example, sharing an order by phone or message).
POST /v1/payment-linksAuthorization: Bearer ldg_live_…Content-Type: application/jsonRequest body
Section titled “Request body”{ "amount": 125050, "terminalId": "trm_3Kd9x", "customerCommissionShareBps": 0, "description": "Order #8841", "redirectUrl": "https://yourstore.example/payment"}| Field | Required | Description |
|---|---|---|
amount |
Yes | The base amount you are requesting, integer in minor units (kuruş) (1,250.50 TRY → 125050). See Amounts and Currency. |
terminalId |
Yes | The terminal the payment is routed to. Must belong to you and be active; otherwise terminal_not_allowed is returned. |
customerCommissionShareBps |
No | The share of the commission passed on to the buyer, integer basis points (0-10000; 2.5% → 250). Defaults to 0 (you absorb the entire commission). |
description |
No | Link description. |
redirectUrl |
No | The base address the buyer is redirected to after the payment is settled; validated at creation time (see redirectUrl behavior). |
allowedInstallmentCounts |
No | The list of installment counts accepted for this link. If omitted (or null), there is no restriction and your terminal’s effective installment set (Installment Options) applies. If you supply it, the list cannot be empty and must be a subset of your terminal’s effective installment set; otherwise the request is rejected with 422 installment_not_allowed. This restriction cannot be changed once the link is created. |
items |
No | Display items listed above the “Total” line on the payment page. For the shape, the total-matching rule, and a sterility caution, see Display items and payer name. |
payerName |
No | The buyer’s name; you supply this field, the buyer does not enter it. Up to 120 characters. Shown masked on the payment page; see Display items and payer name. |
Response
Section titled “Response”201 Created:
{ "id": "01J9K5QATN6M1PXG7VYSD3HZWB", "token": "plk_EXAMPLE0000000000…", "url": "https://pay.lydiagate.example/link/plk_EXAMPLE0000000000…", "partnerReference": 42, "status": "pending", "amount": 125050, "customerCommissionShareBps": 0, "description": "Order #8841", "redirectUrl": "https://yourstore.example/payment", "firstOpenedAt": null, "lastOpenedAt": null, "expiresAt": "2026-06-21T12:00:00Z", "createdAt": "2026-06-14T12:00:00Z", "updatedAt": "2026-06-14T12:00:00Z", "allowedInstallmentCounts": null, "items": null, "payerName": null}| Field | Description |
|---|---|
id |
The link’s permanent identifier; used in the single-item query, cancel calls, and the paymentLinkId field in the webhook body. |
token |
The secret component of url. You do not need to store it separately; url already contains it. |
url |
The full address you share with the buyer. |
partnerReference |
An increasing sequence number scoped to your account (for display/reference only). |
status |
The link’s current status. See Lifecycle. |
amount |
The base amount you requested (kuruş). |
firstOpenedAt / lastOpenedAt |
The instant the buyer first/last opened the link; null if not opened yet. |
expiresAt |
The link’s expiry instant. See Window (TTL). |
createdAt / updatedAt |
Timestamps (UTC). |
allowedInstallmentCounts |
The installment restriction you sent in the request, sorted ascending; null if there is no restriction. |
items |
The display items you sent in the request, in the same order; null if you did not send any. |
payerName |
The payer name you sent in the request, in raw form; null if you did not send one. The payment page shows the masked form instead; see Display items and payer name. |
Lifecycle
Section titled “Lifecycle”| Status | Meaning |
|---|---|
pending |
The link was created; the buyer has not opened it yet. |
launched |
The buyer opened the link. |
initiated |
A payment attempt is in 3D Secure; in flight. No new attempt can be started while in this status. If the attempt fails without completing, the link becomes payable again. |
completed |
The payment was completed (terminal — no further transition). |
expired |
The window (TTL) elapsed; the link became invalid unpaid (terminal). |
failed |
A payment attempt failed; not terminal — the link can be paid again. |
cancelled |
You cancelled it (terminal). |
Window (TTL)
Section titled “Window (TTL)”A link’s default validity window is 7 days from creation. Once expiresAt has passed, the link moves to expired and can no longer be paid.
Listing links
Section titled “Listing links”GET /v1/payment-links?status=pending&status=launched&page=0&size=20Authorization: Bearer ldg_live_…| Parameter | Description |
|---|---|
status |
Filter by one or more statuses; repeat the parameter (status=pending&status=launched). |
minAmount / maxAmount |
Amount range (kuruş). |
createdFrom / createdTo |
Creation time range (UTC, ISO 8601). |
page / size |
Pagination; defaults are page=0, size=20. Out-of-range values are corrected silently: a negative page becomes 0 and size is clamped to 1..200 (e.g. size=500 → 200). The response always echoes the applied page/size. The one hard bound is page × size ≤ 100000; exceeding it returns 400 validation_error (no such deep page exists). |
Response (200 OK):
{ "items": [ { "id": "01J9K5QATN6M1PXG7VYSD3HZWB", "token": "plk_EXAMPLE0000000000…", "url": "https://pay.lydiagate.example/link/plk_EXAMPLE0000000000…", "partnerReference": 42, "status": "pending", "amount": 125050, "customerCommissionShareBps": 0, "description": "Order #8841", "redirectUrl": "https://yourstore.example/payment", "firstOpenedAt": null, "lastOpenedAt": null, "expiresAt": "2026-06-21T12:00:00Z", "createdAt": "2026-06-14T12:00:00Z", "updatedAt": "2026-06-14T12:00:00Z", "allowedInstallmentCounts": null, "items": null, "payerName": null } ], "page": 0, "size": 20, "hasMore": false}| Field | Description |
|---|---|
items |
The links on this page; each item carries the same fields as the creation response. ⚠ This outer envelope’s items is the list of links; each link’s own display items live in the items field inside that link’s object (see Display items and payer name) — the two fields share a name but mean different things. |
hasMore |
true when the page is full (at the applied size) and the next page’s offset remains within the 100000 safety horizon. Increment page only when this is true; a full page at the horizon returns false. |
Query a single link
Section titled “Query a single link”GET /v1/payment-links/{id}Authorization: Bearer ldg_live_…{id} is the id field from the creation or listing response (not token).
Cancelling a link
Section titled “Cancelling a link”DELETE /v1/payment-links/{id}Authorization: Bearer ldg_live_…Only links in a non-terminal status (pending / launched / initiated / failed) can be cancelled; a successful call moves the link to cancelled and returns the updated link record. Trying to cancel a link that has already completed, expired, or was already cancelled fails (see below).
Rate limits
Section titled “Rate limits”The public payment-link endpoints use two separate API-key budgets:
POST /v1/payment-linksandDELETE /v1/payment-links/{id}share a 30/minute create/cancel budget.- Single-item
GET /v1/payment-links/{id}and listGET /v1/payment-linksshare a 120/minute read budget.
All four endpoints also share a 120/minute limit for the same source IP. Different API keys under the same account have independent API-key budgets, but they still share the IP budget when requests come from the same source IP. Read usage does not consume the create/cancel budget, and create/cancel usage does not consume the read budget.
When a limit is exceeded, Lydia Gate returns 429 rate_limited before reading, creating, or changing a link.
The Retry-After: 60 response header gives the wait time in seconds.
Before these budgets, all API-key /v1 endpoints pass through a coarse abuse-control gate before the key is
looked up: a shared 240/minute budget for the same source IP and a 600/minute platform-wide budget.
This upstream protection is not a payment-link business quota. Different /v1 endpoints share its IP budget,
so abnormal aggregate load can produce an earlier 429 rate_limited response. For a clean source, the
240/minute ceiling does not shadow the payment-link IP budget of 120/minute.
redirectUrl behavior
Section titled “redirectUrl behavior”If you supply a redirectUrl, Lydia Gate validates it at link creation time:
- The address must be an absolute URL with the
httporhttpsscheme; otherwise the request is rejected with400 validation_error. - If your account has an allowed host list configured, the
redirectUrlhost must appear on that list; a host that is not on the list is rejected the same way, with400 validation_error. If no such list is configured, only the scheme is checked. This is the same open-redirect protection as thereturnUrlvalidation onPOST /v1/payments.
Once redirectUrl passes validation, after the buyer sees the payment result, they are redirected to:
- On a completed payment →
{redirectUrl}/payment/success, - On a failed attempt →
{redirectUrl}/payment/failure.
If you do not send a redirectUrl, the buyer sees the result on a Lydia Gate-hosted page.
Display items and payer name
Section titled “Display items and payer name”items and payerName are extra information shown on the payment page; both are optional, and neither one enters the amount calculation or the request sent to the bank.
Items (items)
Section titled “Items (items)”If you send items, their amounts must add up to exactly the amount field:
{ "amount": 125050, "terminalId": "trm_3Kd9x", "items": [ { "name": "Annual membership", "amount": 100000 }, { "name": "Shipping", "note": "Standard delivery", "amount": 25050 } ]}| Field | Required | Description |
|---|---|---|
items[].name |
Yes | Item name, up to 120 characters. |
items[].note |
No | A subtitle for the item, up to 200 characters. |
items[].amount |
Yes | Item amount, integer in minor units (kuruş), at least 1. |
If the total does not match amount, the request is rejected with 422 payment_link_items_amount_mismatch. Partial itemization is not supported: if you cannot send every item in the order, do not send items at all. The order you send is preserved; items are listed on the payment page in the same order. The list can hold 1 to 50 items; an empty list ([]) is rejected with 400 validation_error.
Payer name (payerName)
Section titled “Payer name (payerName)”payerName is information about the buyer, and you supply this field; the buyer does not enter it themselves. It can be up to 120 characters.
The payment page shows payerName masked: the first two characters of each word stay visible (one character for a single-letter word), the rest are replaced with *, and word length is preserved — for example, MUSTAFA ALI AYDIN is displayed as MU***** AL* AY***. In this API’s create, query, and list responses, payerName comes back in raw form instead, because only you (with your API key) or your merchant panel can reach those responses — you already sent the value yourself. Masking applies only on the buyer’s side, where you share the link’s address (url); anyone who obtains that address should not see the buyer’s full name.
Sterility check
Section titled “Sterility check”If you put a value that looks like a card number or an IBAN into items[].name, items[].note, or payerName on its own (for example, "4111111111111111"), the request is rejected with 400 sensitive_data_not_allowed. This check only triggers when the entire field matches that pattern; a card number or IBAN embedded inside other text (for example, "Membership fee, card: 4111111111111111") is not rejected. You should not put card or bank account data in these fields anyway; this check is only an extra safety layer.
Possible errors
Section titled “Possible errors”| Code | HTTP | Reason |
|---|---|---|
rate_limited |
429 |
The API-key or shared source-IP budget was exceeded. Retry after the Retry-After duration. |
payment_link_not_found |
404 |
The link was not found or does not belong to this partner. |
payment_link_already_paid |
409 |
The link is already completed. (Rarely, the same error is returned if your cancellation races with a payment completing at that exact moment.) |
payment_link_expired |
409 |
The link’s window has expired; it can no longer be cancelled. |
payment_link_cancelled |
409 |
The link was already cancelled. |
payment_link_items_amount_mismatch |
422 |
items was sent, but the amounts do not add up to amount. |
installment_not_allowed |
422 |
allowedInstallmentCounts was sent empty, or is not a subset of your terminal’s effective installment set. |
Full list: Error Codes.
Webhooks
Section titled “Webhooks”When a link completes or expires, Lydia Gate sends you a webhook (payment_link.completed / payment_link.expired); the body carries this page’s id field (named paymentLinkId) instead of paymentCode. Detail and a sample body: Webhooks.
Related
Section titled “Related”- Create Payment: the direct S2S flow with card handoff
- Webhooks
- Error Codes