Bank Accounts
Webhook Events

Webhook Events

The Bank Accounts product sends two event families:

EventWhen
bank_account.transaction.receivedA new transaction is read on your connected accounts.
bank_transfer.created, bank_transfer.completed, bank_transfer.failed, bank_transfer.returnedA money transfer is created and settled. → Transfer events

Delivery, signature verification and retries are the same for all products: Webhooks. The first part of this page covers the transaction event.

When it is sent

CaseEvent
A transaction seen for the first time in a regular read — incoming (CREDIT) or outgoing (DEBIT)Sent, one event per transaction
Past transactions read back when the connection is first set upNot sent
Transactions found when past data is read againNot sent
A transaction already read arriving from the bank againNot sent; the same transaction is not recorded twice
A transaction held for verification that later becomes visible in the listNot sent

Transactions without an event still appear on the Transactions endpoint. To get all transactions, use the webhook together with the list endpoint (see below).

Sample delivery

{
  "id": "00000000-0000-0000-0000-00000000e001",
  "type": "bank_account.transaction.received",
  "organizationId": "00000000-0000-0000-0000-00000000000a",
  "productCode": "BANK_CONNECT",
  "environment": "LIVE",
  "eventVersion": 1,
  "schemaVersion": "v1",
  "createdAt": "2026-01-01T07:05:01.000Z",
  "data": {
    "transactionId": "00000000-0000-0000-0000-000000001000",
    "accountId": "00000000-0000-0000-0000-000000000100",
    "connectionId": "00000000-0000-0000-0000-000000000010",
    "bankEftCode": "0000",
    "bankName": "ÖRNEK BANKA",
    "direction": "CREDIT",
    "amountMinor": 150000,
    "currency": "TRY",
    "bookedAt": "2026-01-01T07:00:00.000Z",
    "valueAt": null,
    "receivedAt": "2026-01-01T07:05:00.000Z",
    "canonicalType": "TRANSFER_IN",
    "description": "ORNEK ACIKLAMA",
    "counterpartyNameMasked": "Ö**** A****",
    "counterpartyIbanMasked": "TR00 **** **** **** **** **00 00",
    "receiptNo": null
  }
}

Envelope

FieldMeaning
idEvent ID; the same value as the x-lunixi-event-id header.
typebank_account.transaction.received
organizationIdYour organization.
productCodeBANK_CONNECT
environmentTEST or LIVE.
eventVersionEvent version; 1 today.
schemaVersionVersion of the data schema; v1 today.
createdAtWhen the event was created.
dataThe transaction fields; see below.

The envelope may contain fields not in this table; base your code only on the fields documented here.

data

FieldTypeDescription
transactionIdstring (UUID)The transaction's ID; the same as transactionId on the Transactions endpoint.
accountIdstring (UUID)The account the transaction belongs to.
connectionIdstring (UUID)The account's connection.
bankEftCodestringThe bank's EFT code.
bankNamestringThe bank's name.
directionstringCREDIT (in) or DEBIT (out).
amountMinorintegerAmount in minor units; always zero or positive.
currencystringCurrency (ISO 4217).
bookedAtstringBooking date.
valueAtstring | nullValue date.
receivedAtstringThe moment the transaction was recorded at Lunixi.
canonicalTypestringBank-independent transaction type. → Transaction types
descriptionstring | nullThe description as the bank wrote it, unmasked.
counterpartyNameMaskedstring | nullThe counterparty's masked name.
counterpartyIbanMaskedstring | nullThe counterparty's masked IBAN.
receiptNostring | nullReceipt number.

Unlike API responses, fields without a value arrive as null in the webhook; the field is still present in the body. Code that handles both the API and the webhook must cover both cases.

Subscription

To receive the event, create a webhook endpoint for the Bank Connections product:

FieldValue
productCodeBANK_CONNECT
eventTypes["bank_account.transaction.received"], or ["bank_account.*"] for all events in this family. Add ["bank_transfer.*"] for transfer events.
environmentA TEST endpoint receives transactions in the test environment, a LIVE endpoint those in the live environment.

Creating the endpoint, the signing secret and delivery history: Webhook Setup.

Signature, retries and deduplication

Verify the signature on every delivery; the signature format and retry policy are the same for all products: Webhooks.

Delivery is at least once; the same event may arrive more than once. Process the event idempotently by x-lunixi-event-id. The event ID is always the same for the same transaction; you can also deduplicate by data.transactionId.

Handling it in PHP

The PHP SDK verifies the signature and gives you the data fields typed:

use Lunixi\Sdk\BankConnect\BankAccountTransactionReceived;
use Lunixi\Sdk\BankConnect\BankConnectEvents;
use Lunixi\Sdk\Exception\WebhookVerificationException;
 
$rawBody = file_get_contents('php://input') ?: '';
 
try {
    $event = $lunixi->webhooks()->verify($rawBody, getallheaders() ?: [], getenv('LUNIXI_WEBHOOK_SECRET'));
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    return;
}
 
if ($event->type() === BankConnectEvents::TRANSACTION_RECEIVED) {
    $transaction = BankAccountTransactionReceived::fromEvent($event);
    // Deduplicate by $event->id() and store; e.g. $transaction->transactionId(),
    // $transaction->direction(), $transaction->amountMinor(), $transaction->description()
}
 
http_response_code(200);

The BankConnectEvents::ALL_EVENTS value (bank_account.*) can be used in the endpoint subscription.

Using the webhook together with the list endpoint

The webhook is the fast way to learn about a new transaction; it is not the only source. If your endpoint is unreachable for a while, or a transaction arrived through a path that emits no event (for example the initial read-back), fill the gap from the Transactions endpoint:

Keeping transactions in syncData flow: transactions arrive from the webhook (fast) and from regular catch-up reads of the list endpoint; both are deduplicated by transactionId before they are stored, and the newest stored receivedAt is the point where the next catch-up stops.FASTCATCH-UPSTORE ONCESTOP POINTWebhookbank_account.transaction.receivedList endpointGET …/transactionsDeduplicateby transactionIdYour recordsnewest receivedAtLEGENDSOURCEDEDUPLICATIONYOUR SYSTEMNEXT CATCH-UP STOPS HERE
Store every transaction from the webhook by transactionId, read the list endpoint from the first page at intervals and add what you have not stored, and deduplicate the two sources by transactionId.

Transfer events

When a money transfer is created and settled, Lunixi sends the events below to your endpoint. No webhook is sent for currency exchange orders; read their outcome from the Currency Exchange endpoint.

EventWhendata.status
bank_transfer.createdThe transfer is created. A transfer created through the API is SUBMITTED; one started in the panel and waiting for the verification code is PENDING_OTP.SUBMITTED or PENDING_OTP
bank_transfer.completedThe bank completed the transfer.COMPLETED
bank_transfer.failedThe transfer did not take place; the reason is in data.failureCode.FAILED
bank_transfer.returnedThe receiving bank returned a completed transfer.RETURNED
  • Each event is produced at most once per transfer; redeliveries carry the same event id.
  • A transfer started in the panel that is not confirmed in time, or is canceled, becomes CANCELED; no separate event is sent for it. Read its status from the transfer endpoint.
  • For a transfer whose outcome is not confirmed yet (outcomeUnknown), the event is sent once the outcome is settled.
  • Events can arrive out of order (for example completed before created). Write your code against data.status and the transfer's current status, not the order of events.

Sample delivery

{
  "id": "00000000-0000-0000-0000-00000000e101",
  "type": "bank_transfer.completed",
  "organizationId": "00000000-0000-0000-0000-00000000000a",
  "productCode": "BANK_CONNECT",
  "environment": "LIVE",
  "eventVersion": 1,
  "schemaVersion": "v1",
  "createdAt": "2026-01-01T07:00:21.000Z",
  "data": {
    "transferId": "00000000-0000-0000-0000-000000002000",
    "connectionId": "00000000-0000-0000-0000-000000000010",
    "accountId": "00000000-0000-0000-0000-000000000100",
    "bankEftCode": "0000",
    "bankName": "ÖRNEK BANKA",
    "status": "COMPLETED",
    "rail": "FAST",
    "amountMinor": 150075,
    "currency": "TRY",
    "receiverIbanMasked": "TR00 **** **** **** **** **00 00",
    "receiverNameMasked": "Ö**** T****** A.Ş.",
    "paymentPurpose": null,
    "description": "Invoice 2026-0042",
    "failureCode": null,
    "idempotencyKey": "payout-2026-0042",
    "createdAt": "2026-01-01T07:00:00.000Z",
    "completedAt": "2026-01-01T07:00:20.000Z",
    "failedAt": null,
    "returnedAt": null
  }
}

The envelope fields are the same as for the transaction event; type carries the event name.

data of a transfer event

FieldTypeDescription
transferIdstring (UUID)The transfer's id; the same as id on the transfer endpoint.
connectionIdstring (UUID)The account's connection.
accountIdstring (UUID)The account the money left from.
bankEftCodestringThe bank's EFT code.
bankNamestringThe bank's name.
statusstringThe transfer's status at the time of the event.
railstring | nullThe route the bank chose (INTERNAL, INTRABANK, FAST, EFT, UNKNOWN).
amountMinorintegerAmount in minor units.
currencystringCurrency (ISO 4217).
receiverIbanMaskedstringThe receiver's masked IBAN.
receiverNameMaskedstringThe receiver's masked name.
paymentPurposestring | nullPayment purpose code.
descriptionstring | nullTransfer description.
failureCodestring | nullReason code on failed and returned events. → Transfer result codes
idempotencyKeystringThe transfer's Idempotency-Key; use it to match your own payment record.
createdAtstringWhen the transfer was created.
completedAtstring | nullWhen it completed.
failedAtstring | nullWhen it failed.
returnedAtstring | nullWhen it was returned.

Fields without a value arrive as null.

Handling it in PHP

use Lunixi\Sdk\BankConnect\BankTransferEvent;
use Lunixi\Sdk\BankConnect\BankTransferEvents;
 
$event = $lunixi->webhooks()->verify($rawBody, getallheaders() ?: [], getenv('LUNIXI_WEBHOOK_SECRET'));
 
if (in_array($event->type(), BankTransferEvents::ALL, true)) {
    $transfer = BankTransferEvent::fromEvent($event);
    // Deduplicate with $event->id(); find your payment record with $transfer->idempotencyKey().
    if ($event->type() === BankTransferEvents::FAILED) {
        // $transfer->failureCode()
    }
}

BankTransferEvents::ALL_EVENTS (bank_transfer.*) can be used in the endpoint subscription. Requires PHP SDK 0.22.0 or later.