Managing Links
You manage the links you created with the endpoints on this page. Every endpoint is called with the signed request described on Creating Links and requires the matching payment-link:* scope on your key. A key only sees links in its own environment (TEST or LIVE); a link that belongs to another organization or the other environment returns 404. You can do the same from the dashboard.
All amounts are integers in minor units (kuruş): "collectedAmountMinor": 150000 = ₺1,500.00.
| Method | Path | Scope | Idempotency-Key |
|---|---|---|---|
GET | /api/v1/payments/links | payment-link:view | — |
GET | /api/v1/payments/links/:id | payment-link:view | — |
PATCH | /api/v1/payments/links/:id | payment-link:manage | Optional |
POST | /api/v1/payments/links/:id/pause · /resume · /deactivate | payment-link:manage | Optional |
POST | /api/v1/payments/links/:id/send | payment-link:send | Required |
GET | /api/v1/payments/links/:id/qr | payment-link:view | — |
GET | /api/v1/payments/links/:id/attempts | payment-link:view | — |
GET | /api/v1/payments/links/:id/payments | payment-link:view | — |
The Node.js examples use the signedHeaders helper from Creating Links. The cURL examples use the same signing steps as a shell function:
# lunixi-sign.sh — requires OpenSSL 3.0+.
# Usage: source lunixi-sign.sh
# lunixi_sign GET "/api/v1/payments/links?limit=25"
# lunixi_sign POST "/api/v1/payments/links/<id>/pause" "$BODY"
LUNIXI_KEY_ID="mk_test_sk_..."
LUNIXI_KEY_FILE="lunixi-signing.key"
lunixi_sign() {
local method="$1" req_path="$2" body="${3-}"
DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
NONCE=$(openssl rand -hex 16)
if [ -n "$body" ]; then
DIGEST="SHA-256=$(printf '%s' "$body" | openssl dgst -sha256 -binary | base64)"
printf '%s\n%s\nX-Date:%s\nX-Nonce:%s\nDigest:%s' "$method" "$req_path" "$DATE" "$NONCE" "$DIGEST" > canonical.txt
else
DIGEST=""
printf '%s\n%s\nX-Date:%s\nX-Nonce:%s' "$method" "$req_path" "$DATE" "$NONCE" > canonical.txt
fi
SIGNATURE=$(openssl pkeyutl -sign -inkey "$LUNIXI_KEY_FILE" -rawin -in canonical.txt | base64 | tr -d '\n')
}Listing
GET /api/v1/payments/links returns your links newest first, in cursor-based pages.
| Query parameter | Type | Description |
|---|---|---|
limit | integer | Records per page, 1–100. Default 25. |
cursor | string | pageInfo.nextCursor from the previous response. Not sent on the first page. |
includeTotal | boolean | If true, pageInfo.totalCount is filled. Slower; do not send it unless you need it. |
state | string | Comma-separated states, e.g. ACTIVE,PAUSED. |
usage | string | Comma-separated: SINGLE_USE,MULTI_USE. |
amountMode | string | Comma-separated: FIXED,OPEN,ITEMIZED. |
tag | string | Links carrying this tag. |
branchCode | string | Links with this branch code. |
createdById | string | Links created by this id (the key id for links created through the API). |
query | string | An exact match on the short code or a prefix search on the title / reference. The code matches regardless of case, dashes and spaces. |
createdFrom, createdTo | string (ISO 8601) | Creation range; createdFrom inclusive, createdTo exclusive. |
environment | string | Results are always limited to your key's environment. |
source lunixi-sign.sh
REQ_PATH="/api/v1/payments/links?limit=25&state=ACTIVE,PAUSED"
lunixi_sign GET "$REQ_PATH"
curl "https://api-gateway.lunixi.com$REQ_PATH" \
-H "X-Key-Id: $LUNIXI_KEY_ID" \
-H "X-Date: $DATE" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE"Response
pageInfo sits next to data; the list is in data.items.
{
"status": "success",
"code": "SUCCESS",
"data": {
"items": [
{
"id": "5f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f",
"shortCode": "7KQ2M9XAPT",
"url": "https://pay.lunixi.com/7KQ2M9XAPT",
"organizationId": "b1d2c3e4-…",
"environment": "TEST",
"state": "ACTIVE",
"usage": "SINGLE_USE",
"amountMode": "FIXED",
"currency": "TRY",
"amountMinor": 150000,
"title": "November consulting fee",
"reference": "INV-2026-0042",
"tags": [],
"paidCount": 0,
"collectedAmountMinor": 0,
"expiresAt": "2026-10-31T20:59:59.000Z",
"createdByType": "API_KEY",
"createdById": "mk_test_sk_…",
"createdAt": "2026-09-25T09:00:00.000Z",
"version": 1,
"rowVersion": 1
}
]
},
"pageInfo": { "hasMore": false, "nextCursor": null, "totalCount": null },
"statusCode": 200,
"requestId": "req_7f3d9c1a4b2e"
}A list row is a summary; amountMinor is filled only on FIXED links. Call GET /api/v1/payments/links/:id for the full record.
A single link
GET /api/v1/payments/links/:id returns the link's full record: every field including recipient and prefill, the counters (counters), stock status (availability) and the lock flags. The shape is the same as the create response — see Creating Links › Response.
Updating
PATCH /api/v1/payments/links/:id changes the link's definition and produces a new version (version). Payments already in progress complete with the amount and rules of the version they started on.
| Rule | Detail |
|---|---|
expectedRowVersion is required | The rowVersion you last read. If the link changed in between, 409 payment_link.link.version_conflict is returned: read the link again and re-apply your change. |
| Only the fields you send change | Every key in the body is a field to change; fields you do not send stay as they are. |
null clears | e.g. "expiresAt": null removes the expiry, "successRedirectUrl": null removes the post-payment redirect. |
| Objects and arrays are replaced whole | items, buyerFields, customFields, recipient, prefill, verification, reminders, checkout, tags, metadata are not merged; send the complete new value. An existing item's itemKey cannot be changed. |
| Immutable fields | usage, amountMode, currency → 422 payment_link.link.immutable_field |
| Amount lock | While a payment is in progress or once the link has taken a payment (amountLocked: true), amount fields cannot change: amountMinor, minAmountMinor, maxAmountMinor, suggestedAmountsMinor, quantityEnabled, minQuantity, maxQuantity, items, capacity → 409 payment_link.link.amount_locked. Exception: capacity and item capacity can be increased (never below the units sold plus those held by in-progress payments). To raise an item's capacity, send the items array with only the capacity values changed. |
| Taken-down link | 409 payment_link.link.taken_down on a TAKEN_DOWN link |
Non-amount fields such as the title, description, expiry, buyer fields and attribution fields are not subject to the amount lock. A successful update publishes payment.link.updated.
source lunixi-sign.sh
LINK_ID="5f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f"
REQ_PATH="/api/v1/payments/links/$LINK_ID"
BODY='{"expectedRowVersion":1,"title":"November consulting fee (revised)","expiresAt":"2026-11-15T20:59:59.000Z"}'
lunixi_sign PATCH "$REQ_PATH" "$BODY"
curl -X PATCH "https://api-gateway.lunixi.com$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Key-Id: $LUNIXI_KEY_ID" \
-H "X-Date: $DATE" \
-H "X-Nonce: $NONCE" \
-H "Digest: $DIGEST" \
-H "X-Signature: $SIGNATURE" \
--data-raw "$BODY"Pausing, resuming and deactivating
POST /api/v1/payments/links/:id/pause, /resume and /deactivate change the link's state and return the current link.
| Field | Type | Required | Description |
|---|---|---|---|
expectedRowVersion | integer | No | If sent, it must match the link's rowVersion (409 payment_link.link.version_conflict). If omitted or 0, no check is made. |
reason | string | No | Up to 500 characters; written to the link's activity history, not shown to the buyer. |
| Action | Transition | Event |
|---|---|---|
pause | ACTIVE → PAUSED | payment.link.paused |
resume | PAUSED → ACTIVE — expiresAt must be in the future | payment.link.resumed |
deactivate | ACTIVE or PAUSED → DEACTIVATED — permanent | payment.link.deactivated |
An invalid transition (e.g. pausing a COMPLETED link) returns 409 payment_link.link.invalid_transition.
Pausing and deactivating do not stop a payment already in progress. If the buyer is on the payment form, the payment can still complete; in that case payment.captured still arrives and the link's counters are updated. Always base the order on payment.captured.
source lunixi-sign.sh
REQ_PATH="/api/v1/payments/links/$LINK_ID/pause"
BODY='{"expectedRowVersion":2,"reason":"Stock count"}'
lunixi_sign POST "$REQ_PATH" "$BODY"
curl -X POST "https://api-gateway.lunixi.com$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Key-Id: $LUNIXI_KEY_ID" -H "X-Date: $DATE" -H "X-Nonce: $NONCE" \
-H "Digest: $DIGEST" -H "X-Signature: $SIGNATURE" \
--data-raw "$BODY"Sending
POST /api/v1/payments/links/:id/send sends the link through Lunixi by email or SMS. Idempotency-Key is required: a replay with the same key does not send again and returns the first delivery with replayed: true; the same key with a different body receives 409 payment_link.idempotency.conflict.
| Field | Type | Required | Description |
|---|---|---|---|
channel | string | Yes | EMAIL | SMS |
recipientEmail | string | No | If empty, the link's recipient.email is used. |
recipientPhone | string | No | E.164. If empty, the link's recipient.phone is used. |
locale | string | No | Message language: tr | en. |
{
"status": "success",
"code": "SUCCESS",
"data": {
"delivery": {
"id": "0e8a7c55-…",
"kind": "REQUEST",
"channel": "EMAIL",
"recipientMasked": "a***@example.com",
"state": "QUEUED",
"failureReason": null,
"attemptId": null,
"createdAt": "2026-09-25T09:05:00.000Z",
"updatedAt": "2026-09-25T09:05:00.000Z"
},
"replayed": false
},
"statusCode": 200,
"requestId": "req_…"
}The delivery is recorded as QUEUED or returns FAILED immediately. Later state changes (SENT, DELIVERED, FAILED) appear in the link's delivery history in the dashboard.
failureReason | Meaning | What to do |
|---|---|---|
channel_not_available | The chosen channel is not available for your account yet | Share the link through your own channel or by QR |
test_mode_recipient_not_allowed | In TEST, the recipient is not a verified member of your organization (no SMS is sent in TEST) | Send test deliveries to your own team's addresses |
invalid_recipient | The address or number is invalid | Fix the recipient and send with a new Idempotency-Key |
provider_rejected | The delivery provider rejected the message | Check the recipient details |
suppressed | The recipient address is on the delivery suppression list | Use another channel or address |
mock_delivery | The delivery was simulated; the message did not reach the recipient | Share the link through your own channel |
render_failed, dispatch_failed | The message could not be prepared or delivered | Retry with a new Idempotency-Key |
- Sending a taken-down link returns
409 payment_link.link.taken_down. - Sending also clears the link's access verification lock (
verificationLocked: false): if the buyer entered too many wrong codes, send the link again so they can request a new code. - Reminders (
reminders) need no separate call; on a single-use link withrecipientset they are sent at the interval you defined.
QR code
GET /api/v1/payments/links/:id/qr returns the QR code containing the link's address (https://pay.lunixi.com/<CODE>) as an image — not JSON.
| Query parameter | Value |
|---|---|
format | svg (default) | png |
size | Pixels, 128–2048. Default 512. |
The response carries Content-Type: image/svg+xml or image/png and Cache-Control: no-store. On failure the standard JSON error envelope is returned. A taken-down link returns 409 payment_link.link.taken_down.
This is a URL QR; it is not a TR Karekod. The buyer scans it with their phone camera to open the payment page.
source lunixi-sign.sh
REQ_PATH="/api/v1/payments/links/$LINK_ID/qr?format=png&size=1024"
lunixi_sign GET "$REQ_PATH"
curl "https://api-gateway.lunixi.com$REQ_PATH" \
-H "X-Key-Id: $LUNIXI_KEY_ID" -H "X-Date: $DATE" -H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE" \
-o link-qr.pngPayment attempts
GET /api/v1/payments/links/:id/attempts returns every payment attempt started on the link, newest first. Parameters: limit, cursor and a comma-separated state.
Attempt state | Meaning |
|---|---|
OPEN | The attempt was opened; the payment form is being prepared |
BOUND | The payment form opened; the buyer is paying or the bank's result is pending |
SUCCEEDED | The payment was captured |
FAILED | The payment failed definitively |
EXPIRED | The buyer did not complete the payment in time |
ABANDONED | The payment form never opened |
NEEDS_REVIEW | The outcome could not be applied automatically; the Lunixi operations team reviews it |
The stock an attempt holds while in progress (on a single-use link, the link itself) is released only on the payment's definitive outcome. For a payment whose bank result has not arrived yet, the stock stays held even after a timeout; that is why reservedQuantity can stay non-zero for a while.
| Field | Description |
|---|---|
id | The attempt id — paymentLinkAttemptId in payment webhooks |
state | See the table above |
linkVersion | The link version the attempt is bound to |
paymentId | The payment id (intentId in webhooks); null if the form never opened |
amountMinor, currency, quantity | The amount and quantity to collect |
lines[] | On itemized links, { itemKey, name, quantity, unitPriceMinor, lineTotalMinor } |
buyerName, buyerEmail, buyerPhone | Details the buyer entered |
customFields[] | { key, value } — answers to custom fields |
failureCode | The canonical decline key on a failed attempt, e.g. card.insufficient_funds |
outcomeCertainty | DEFINITIVE_FAILURE | UNKNOWN | null |
reservationState | HELD | CONVERTED (turned into a sale) | RELEASED |
createdAt, boundAt, finalizedAt | Timestamps |
Attempt records contain the buyer's personal data; store them accordingly in your own systems.
Collected payments
GET /api/v1/payments/links/:id/payments returns the link's successful payments with their refund totals. Parameters: limit, cursor.
| Field | Description |
|---|---|
attemptId, paymentId | The attempt and payment ids |
paymentStatus | CAPTURED | PARTIALLY_REFUNDED | REFUNDED |
amountMinor | The payment amount (excluding installment interest) |
capturedAmountMinor, refundedAmountMinor | The captured and refunded amounts |
currency, paidAt | Currency and payment time |
buyerEmail | The buyer's email |
receiptNumber | The receipt number, e.g. PL-20260925-7KQ2M9XAPT |
To refund, use the existing Refunds endpoint with the paymentId. A refund does not change the link's state: a single-use link stays COMPLETED, and sold stock is not added back on a multi-use link.
Errors
| Status | Code | What to do |
|---|---|---|
400 | INVALID_CURSOR | Send pageInfo.nextCursor from the previous response as is; if you changed the filters, start over |
400 | payment_link.idempotency.key_required | Send an Idempotency-Key for send |
403 | API_INSUFFICIENT_SCOPE | Add the matching payment-link:* scope to your key |
404 | payment_link.link.not_found | The link does not exist, or belongs to another organization or the other environment |
409 | payment_link.link.version_conflict | Read the link again and retry with the current rowVersion |
409 | payment_link.link.invalid_transition | Check the link's current state; an ended link does not reopen |
409 | payment_link.link.amount_locked | Create a new link to change the amount |
409 | payment_link.link.taken_down | Lunixi took the link down; it cannot be changed or sent |
409 | payment_link.idempotency.conflict | Generate a new Idempotency-Key for a different delivery |
422 | payment_link.link.immutable_field | usage, amountMode and currency cannot change; create a new link |
422 | payment_link.validation.invalid | Fix the fields listed in details.errors |
For signature errors (401): Creating Links › Errors. On 5xx responses, retry with exponential backoff; if you repeat a send call with the same Idempotency-Key, no second message is sent.
Next steps
- Payment Page — the page the buyer sees and access verification
- Webhooks — link events and order fulfilment
- Pagination — the
limit+cursorstandard