Payment Orchestrator
Payment Links
Managing Links

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.

MethodPathScopeIdempotency-Key
GET/api/v1/payments/linkspayment-link:view—
GET/api/v1/payments/links/:idpayment-link:view—
PATCH/api/v1/payments/links/:idpayment-link:manageOptional
POST/api/v1/payments/links/:id/pause · /resume · /deactivatepayment-link:manageOptional
POST/api/v1/payments/links/:id/sendpayment-link:sendRequired
GET/api/v1/payments/links/:id/qrpayment-link:view—
GET/api/v1/payments/links/:id/attemptspayment-link:view—
GET/api/v1/payments/links/:id/paymentspayment-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 parameterTypeDescription
limitintegerRecords per page, 1–100. Default 25.
cursorstringpageInfo.nextCursor from the previous response. Not sent on the first page.
includeTotalbooleanIf true, pageInfo.totalCount is filled. Slower; do not send it unless you need it.
statestringComma-separated states, e.g. ACTIVE,PAUSED.
usagestringComma-separated: SINGLE_USE,MULTI_USE.
amountModestringComma-separated: FIXED,OPEN,ITEMIZED.
tagstringLinks carrying this tag.
branchCodestringLinks with this branch code.
createdByIdstringLinks created by this id (the key id for links created through the API).
querystringAn exact match on the short code or a prefix search on the title / reference. The code matches regardless of case, dashes and spaces.
createdFrom, createdTostring (ISO 8601)Creation range; createdFrom inclusive, createdTo exclusive.
environmentstringResults 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.

RuleDetail
expectedRowVersion is requiredThe 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 changeEvery key in the body is a field to change; fields you do not send stay as they are.
null clearse.g. "expiresAt": null removes the expiry, "successRedirectUrl": null removes the post-payment redirect.
Objects and arrays are replaced wholeitems, 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 fieldsusage, amountMode, currency → 422 payment_link.link.immutable_field
Amount lockWhile 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 link409 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.

FieldTypeRequiredDescription
expectedRowVersionintegerNoIf sent, it must match the link's rowVersion (409 payment_link.link.version_conflict). If omitted or 0, no check is made.
reasonstringNoUp to 500 characters; written to the link's activity history, not shown to the buyer.
ActionTransitionEvent
pauseACTIVE → PAUSEDpayment.link.paused
resumePAUSED → ACTIVE — expiresAt must be in the futurepayment.link.resumed
deactivateACTIVE or PAUSED → DEACTIVATED — permanentpayment.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.

FieldTypeRequiredDescription
channelstringYesEMAIL | SMS
recipientEmailstringNoIf empty, the link's recipient.email is used.
recipientPhonestringNoE.164. If empty, the link's recipient.phone is used.
localestringNoMessage 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.

failureReasonMeaningWhat to do
channel_not_availableThe chosen channel is not available for your account yetShare the link through your own channel or by QR
test_mode_recipient_not_allowedIn 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_recipientThe address or number is invalidFix the recipient and send with a new Idempotency-Key
provider_rejectedThe delivery provider rejected the messageCheck the recipient details
suppressedThe recipient address is on the delivery suppression listUse another channel or address
mock_deliveryThe delivery was simulated; the message did not reach the recipientShare the link through your own channel
render_failed, dispatch_failedThe message could not be prepared or deliveredRetry 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 with recipient set 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 parameterValue
formatsvg (default) | png
sizePixels, 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.png

Payment 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 stateMeaning
OPENThe attempt was opened; the payment form is being prepared
BOUNDThe payment form opened; the buyer is paying or the bank's result is pending
SUCCEEDEDThe payment was captured
FAILEDThe payment failed definitively
EXPIREDThe buyer did not complete the payment in time
ABANDONEDThe payment form never opened
NEEDS_REVIEWThe 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.

FieldDescription
idThe attempt id — paymentLinkAttemptId in payment webhooks
stateSee the table above
linkVersionThe link version the attempt is bound to
paymentIdThe payment id (intentId in webhooks); null if the form never opened
amountMinor, currency, quantityThe amount and quantity to collect
lines[]On itemized links, { itemKey, name, quantity, unitPriceMinor, lineTotalMinor }
buyerName, buyerEmail, buyerPhoneDetails the buyer entered
customFields[]{ key, value } — answers to custom fields
failureCodeThe canonical decline key on a failed attempt, e.g. card.insufficient_funds
outcomeCertaintyDEFINITIVE_FAILURE | UNKNOWN | null
reservationStateHELD | CONVERTED (turned into a sale) | RELEASED
createdAt, boundAt, finalizedAtTimestamps

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.

FieldDescription
attemptId, paymentIdThe attempt and payment ids
paymentStatusCAPTURED | PARTIALLY_REFUNDED | REFUNDED
amountMinorThe payment amount (excluding installment interest)
capturedAmountMinor, refundedAmountMinorThe captured and refunded amounts
currency, paidAtCurrency and payment time
buyerEmailThe buyer's email
receiptNumberThe 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

StatusCodeWhat to do
400INVALID_CURSORSend pageInfo.nextCursor from the previous response as is; if you changed the filters, start over
400payment_link.idempotency.key_requiredSend an Idempotency-Key for send
403API_INSUFFICIENT_SCOPEAdd the matching payment-link:* scope to your key
404payment_link.link.not_foundThe link does not exist, or belongs to another organization or the other environment
409payment_link.link.version_conflictRead the link again and retry with the current rowVersion
409payment_link.link.invalid_transitionCheck the link's current state; an ended link does not reopen
409payment_link.link.amount_lockedCreate a new link to change the amount
409payment_link.link.taken_downLunixi took the link down; it cannot be changed or sent
409payment_link.idempotency.conflictGenerate a new Idempotency-Key for a different delivery
422payment_link.link.immutable_fieldusage, amountMode and currency cannot change; create a new link
422payment_link.validation.invalidFix 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 + cursor standard