Payment Link Webhooks
Payment links produce two kinds of webhooks:
- 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. - 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:
eventTypes | Covers |
|---|---|
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).
| Field | Value |
|---|---|
source | "PAYMENT_LINK" |
paymentLinkId | The link's id |
paymentLinkCode | The link's 10-character code |
paymentLinkAttemptId | The 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
| Event | Fired when |
|---|---|
payment.link.created | The link was created |
payment.link.updated | The link was updated; new version |
payment.link.paused | The link was paused |
payment.link.resumed | The link was reopened |
payment.link.deactivated | The link was deactivated |
payment.link.expired | expiresAt passed |
payment.link.completed | A single-use link was paid; carries paymentId and attemptId |
payment.link.taken_down | Lunixi took the link down; carries takedownReasonCode |
payment.link.takedown_reverted | Lunixi 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 field | Description |
|---|---|
paymentLinkId, shortCode, url | The link's id, code and address |
state, previousState | The state at the time of the event and the state before it |
version | The link's version at the time of the event |
usage, amountMode, currency | Link type and currency |
amountMinor | Minor-unit string; filled only on FIXED links, otherwise null |
title, reference | Title and your reference (null if there is no reference) |
metadata | The key-value pairs you set on the link |
paidCount, soldQuantity | Number of successful payments and units sold |
collectedAmountMinor | Total collected, minor-unit string |
expiresAt | Expiry time; null if none |
paymentId, attemptId | Only on payment.link.completed |
takedownReasonCode | Only 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-signatureheader before trusting the body. - Retried — if no
2xxis received, delivery is retried with backoff; every retry carries the sameid. - 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
stateandversionvalues, 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