Payment Orchestrator
Payment Links
Webhooks

Payment Link Webhooks

Payment links produce two kinds of webhooks:

  1. Payment events — a payment taken through a link produces payment.captured, payment.failed, payment.refunded … just like your other payments. Fields identifying the link are added to these events.
  2. Link lifecycle events — payment.link.*: the link being created, updated, paused, expiring or completing.
⚠️

Fulfil the order on payment.captured. For a payment taken through a link there is still a single money signal: payment.captured. payment.link.completed only tells you that a single-use link has closed; it is not a second "paid" signal. The result page and the successRedirectUrl redirect are not proof of payment either.

Endpoint configuration, signature verification and the delivery contract are shared across all products — see Webhook Setup, Webhooks and Payment Webhooks.

Subscribing

Link events belong to the PAYMENT_GATEWAY product. Add one of the following to your webhook endpoint's eventTypes list:

eventTypesCovers
payment.link.*All nine link lifecycle events
payment.link.completed, payment.link.expired …Only the events you choose
payment.*All payment events and link events

Payment events (payment.captured and so on) need no separate subscription; your existing subscription also covers payments taken through a link.

Your plan decides which events you can subscribe to; the webhook screen in the dashboard shows whether each event is included in your plan. A request to create or update an endpoint with an event type your plan does not include is rejected with 403. Wildcard subscriptions are checked against the plan list too: payment.link.* or payment.* is accepted only if your plan covers that wildcard; otherwise add the link events by name. After a plan change, events that are no longer included are not delivered.

Link fields on payment events

On every payment.* event of a payment taken through a link, data also carries the following fields. On payments outside links these fields are absent (not null).

FieldValue
source"PAYMENT_LINK"
paymentLinkIdThe link's id
paymentLinkCodeThe link's 10-character code
paymentLinkAttemptIdThe payment attempt id — the id in payment attempts

On link payments, orderId is generated by Lunixi: PL-<CODE> on a single-use link, PL-<CODE>-<paymentLinkAttemptId> on a multi-use link. intentId is the same as paymentId in the link endpoints.

{
  "id": "2d8f4a1c-6b3e-5f70-9a2b-4c6d8e0f1a3b",
  "type": "payment.captured",
  "sourceService": "payment-service",
  "organizationId": "b1d2c3e4-…",
  "productCode": "PAYMENT_GATEWAY",
  "environment": "LIVE",
  "eventVersion": 1,
  "schemaVersion": "v1",
  "createdAt": "2026-09-25T10:14:02.000Z",
  "data": {
    "intentId": "9c6f2b1a-4e8d-4a3f-b5e6-2d7c1a9b8e30",
    "merchantId": "b1d2c3e4-…",
    "orderId": "PL-7KQ2M9XAPT",
    "currency": "TRY",
    "status": "CAPTURED",
    "capturedAmountMinor": "150000",
    "source": "PAYMENT_LINK",
    "paymentLinkId": "5f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f",
    "paymentLinkCode": "7KQ2M9XAPT",
    "paymentLinkAttemptId": "0b6f3e2a-1c4d-4e5f-8a9b-7c6d5e4f3a2b"
  }
}

Fulfilling the order

Verify the signature and acknowledge quickly

Verify x-lunixi-signature, write the event to your own queue and return 2xx.

Match the event to your link

If data.source === "PAYMENT_LINK", use paymentLinkId to find the record you stored in your own system when you created the link (order, invoice, customer). The reference and metadata you set on the link are not in the payment event; read them with GET /api/v1/payments/links/:id if needed, or take them from the payment.link.* events.

Process once

Delivery is at-least-once and ordering is not guaranteed. Use the event's id as the deduplication key; fulfil the order only once per payment attempt (paymentLinkAttemptId).

Fulfil

When payment.captured arrives, the money has been collected. On a multi-use link every successful payment produces its own payment.captured; the link stays open.

Link lifecycle events

EventFired when
payment.link.createdThe link was created
payment.link.updatedThe link was updated; new version
payment.link.pausedThe link was paused
payment.link.resumedThe link was reopened
payment.link.deactivatedThe link was deactivated
payment.link.expiredexpiresAt passed
payment.link.completedA single-use link was paid; carries paymentId and attemptId
payment.link.taken_downLunixi took the link down; carries takedownReasonCode
payment.link.takedown_revertedLunixi reverted the takedown; the link returned to its previous state

On a multi-use link no separate link event is produced for each payment; you track payments with payment.captured.

Payload

{
  "id": "7a1e9c3b-5d2f-5a84-b6c1-3e9f0a2d4b6c",
  "type": "payment.link.completed",
  "sourceService": "payment-link-service",
  "organizationId": "b1d2c3e4-…",
  "productCode": "PAYMENT_GATEWAY",
  "environment": "LIVE",
  "eventVersion": 1,
  "schemaVersion": "v1",
  "createdAt": "2026-09-25T10:14:03.000Z",
  "data": {
    "paymentLinkId": "5f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f",
    "shortCode": "7KQ2M9XAPT",
    "url": "https://pay.lunixi.com/7KQ2M9XAPT",
    "state": "COMPLETED",
    "previousState": "ACTIVE",
    "version": 1,
    "usage": "SINGLE_USE",
    "amountMode": "FIXED",
    "currency": "TRY",
    "amountMinor": "150000",
    "title": "November consulting fee",
    "reference": "INV-2026-0042",
    "metadata": { "crmId": "A-77" },
    "paidCount": 1,
    "collectedAmountMinor": "150000",
    "soldQuantity": 1,
    "expiresAt": "2026-10-31T20:59:59.000Z",
    "paymentId": "9c6f2b1a-4e8d-4a3f-b5e6-2d7c1a9b8e30",
    "attemptId": "0b6f3e2a-1c4d-4e5f-8a9b-7c6d5e4f3a2b"
  }
}
data fieldDescription
paymentLinkId, shortCode, urlThe link's id, code and address
state, previousStateThe state at the time of the event and the state before it
versionThe link's version at the time of the event
usage, amountMode, currencyLink type and currency
amountMinorMinor-unit string; filled only on FIXED links, otherwise null
title, referenceTitle and your reference (null if there is no reference)
metadataThe key-value pairs you set on the link
paidCount, soldQuantityNumber of successful payments and units sold
collectedAmountMinorTotal collected, minor-unit string
expiresAtExpiry time; null if none
paymentId, attemptIdOnly on payment.link.completed
takedownReasonCodeOnly on payment.link.taken_down: PHISHING, FRAUD, BRAND_IMPERSONATION, PROHIBITED_GOODS, LEGAL_REQUEST, OTHER

data reflects the state at the moment the event describes. For the link's current state, call GET /api/v1/payments/links/:id.

Delivery

Link events use the same delivery model as payment events:

  • Signed — verify the x-lunixi-signature header before trusting the body.
  • Retried — if no 2xx is received, delivery is retried with backoff; every retry carries the same id.
  • At-least-once, unordered — the same event can arrive more than once and events may not arrive in the order they happened. Base your logic on the state and version values, not on arrival order.

Refunds

Refunding a payment taken through a link produces payment.refunded or payment.partially_refunded; these events carry the link fields too. 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.

Next steps

  • Payment Webhooks — every payment.* event and its fields
  • Webhook Setup — creating endpoints, subscribing to events and the signing secret
  • Webhooks — delivery headers, signature verification and the retry policy
  • Managing Links — reading the link's current state