Webhook events
This page is the wire contract. Before you read a body, verify it. See Securing your webhook endpoint.
Every webhook body has the same outer shape, the envelope, wrapped around a data block whose contents depend on the event type.
{
"id": "9f1c2f0e-4c3b-4b8a-9d2a-5f7c1e6b0a11",
"type": "inbound-payment.received.v1",
"occurredAt": "2026-09-17T09:14:22.431+00:00",
"data": { }
}| Field | Type | Meaning |
|---|---|---|
id | UUID | The identity of the business event, and your de-duplication key. Stable across retries, replays and event-type versions |
type | string | The versioned event type, for example inbound-payment.received.v1 |
occurredAt | timestamp | When the platform recorded the business fact |
data | object | Type-specific payload |
Notes that save time later:
occurredAtrenders its offset literally:+00:00for UTC, neverZ, with fractional seconds only when they are non-zero. Parse it as an ISO 8601 offset timestamp, not with a format string. The same applies to every timestamp insidedata(createdAt,settledAt).- It is not a delivery timestamp. When an attempt was sent is the signature's
created; which attempt it is is theWebhook-Attemptheader. Neither appears in the body, which is why the body is byte-identical on every attempt. - Amounts are integers in minor units. A
valuefield alongside them is the major-unit rendering, for display only. Do arithmetic onamountInMinorUnits. - Dates are
YYYY-MM-DD.dateandvalueDatefields carry a calendar date with no time or offset. - All JSON is camelCase and compact. Optional fields are present with a
nullvalue, not omitted, so readreasonas "may be null" rather than "may be missing".
The amount object
{ "amountInMinorUnits": 125000, "value": 1250.00, "currency": "GBP" }| Field | Meaning |
|---|---|
amountInMinorUnits | The exact integer amount in the currency's smallest unit. Authoritative |
value | The same amount in major units, as we rendered it |
currency | ISO 4217 code |
What changed in this release
Payload field names have changedEvery payload now keys its object as
id(it waspaymentIdortradeId), and several fields were renamed. Update your parser before you rely on the new fields. Events recorded before this release still carry the old names when you read them back fromGET /api/v1/events.
New event types
| Event type | What it gives you |
|---|---|
outbound-payment.submitted.v1 | The full outbound payment resource, once per payment, whatever channel it was created through |
outbound-payment.approved.v1 | The payment has had its last required approval |
fx-trade.created.v1 | The full FX trade resource, as soon as the trade exists |
Richer payloads. inbound-payment.received.v1, internal-payment.completed.v1, transaction.created.v1 and the three new types now carry the object as its Client API resource returns it, so most integrations never need a second call. Every outbound payment and FX trade event now carries your internalReference, and outbound-payment.cancelled.v1 and outbound-payment.returned.v1 now carry a reason.
Deprecated. outbound-payment.scheduled.v1 is no longer published and cannot be subscribed to. A future-dated payment now arrives as outbound-payment.submitted.v1 with status Scheduled. Existing scheduled subscriptions have been moved to submitted for you.
Renamed fields
| Event type | Before | Now |
|---|---|---|
All inbound-payment.*, outbound-payment.* and internal-payment.* types | paymentId | id |
All fx-trade.* types | tradeId | id |
inbound-payment.received.v1 | clientAccountId | accountId |
inbound-payment.received.v1 | paymentCode | code |
inbound-payment.returned.v1 | reason | returnReason |
fx-trade.failed.v1 | reason | failedReason (now readable text, see below) |
internal-payment.completed.v1 | debitingAccountId, creditingAccountId | debitingAccount.id, creditingAccount.id |
Changed meaning. fx-trade.booked.v1 is now raised when the broker confirms the trade, not when it is created. Subscribe to fx-trade.created.v1 for the earlier moment.
Versioning
The version is part of the type name. There is no version field.
- Additive changes never change the type name. We may add new optional fields to a
datablock at any time. Your parser must ignore fields it does not recognise. A strict parser that rejects unknown members will break on a routine release. - Breaking changes ship as a new type,
...v2, which nobody receives until they subscribe to it. - When a
.v2ships, the.v1is deprecated: existing subscriptions keep receiving it, new subscriptions to it are refused, and six months later it stops publishing and remaining subscriptions to it are removed. - Migrating is your call: subscribe to
.v2, confirm you handle it, unsubscribe from.v1. While both are subscribed you receive each business event twice, once per type, with the sameid, so de-duplicating onidnaturally processes it once.
We will always tell you before a version is deprecated. Nothing is retired inside the six-month window.
Events you can subscribe to
Subscribe only to the types you handle. Two kinds of payload:
- Resource payloads (
inbound-payment.received,outbound-payment.submitted,internal-payment.completed,fx-trade.created,transaction.created) carry the object exactly as itsGETreturns it at the moment the event is published. Field names, status values and channel values match the resource. - Status payloads (every other type) are small: the object's
id, theclientId, yourinternalReferenceand, where it applies, a reason. Fetch the object if you need more.
Fields that appear on several types mean the same thing everywhere:
| Field | Meaning |
|---|---|
clientId | The member client the object belongs to. An endpoint registered for a group receives events for every member, so route on this |
internalReference | Your own reference, as you sent it when creating the object through the API. null for objects created elsewhere |
channel | How the object was created: Portal, Api or Brokered (by your IFX Payments dealer or operations team on your behalf). null on records older than channel tracking |
createdBy | The name of the user who created it. Only given when channel is Portal or Api; null otherwise |
fee | An amount object, only when the fee is billed per payment. null when fees are invoiced in arrears |
Inbound payment events
An inbound payment is money arriving from outside. Fetch the full object with GET /api/v1/inbound-payments/{id}.
| Event type | Raised when | Terminal |
|---|---|---|
inbound-payment.received.v1 | The payment has been allocated to one of your accounts | No |
inbound-payment.returned.v1 | The funds have gone back to the remitter | Yes |
inbound-payment.received.v1
inbound-payment.received.v1Carries the inbound payment resource.
data field | Type | Meaning |
|---|---|---|
id | UUID | The inbound payment's id |
clientId | UUID | The member client the payment belongs to |
accountId | UUID | The account it was allocated to |
code | string or null | The payment's reference code, for example IP-000123 |
receivedReturnReasons | array | Set when this payment is a bank returning one of your outbound payments: the reasons the bank gave, in the order it sent them. Each item is { "code", "description" }; description is null for a code we do not recognise. An empty array, never null, when there are none |
paymentMethod | string | The rail the funds arrived by, for example Fps, Chaps, Swift or Sepa |
reference | string or null | The reference the remitter gave |
amount | object | Amount and currency |
valueDate | date | The date the funds were credited |
uetr | string or null | The SWIFT unique end-to-end transaction reference, when the bank gave one |
remitter | object | Who sent it: name, accountNumber, bankCode (a sort code or BIC), and address |
remitter.address | object or null | buildingNumber, streetName, city, postalCode, regionState, country, each as the bank gave it and each nullable. null when the bank gave no address at all |
fee | object or null | See the shared fields above |
settledAt | timestamp or null | When we settled the payment |
createdAt | timestamp | When we recorded the payment |
If a payment is re-matched to a different client after allocation, the original client does not receive this event.
{
"id": "9f1c2f0e-4c3b-4b8a-9d2a-5f7c1e6b0a11",
"type": "inbound-payment.received.v1",
"occurredAt": "2026-09-17T09:14:22.431+00:00",
"data": {
"id": "3b8c5a41-9d27-4e6f-8a10-2c4b7e9f0d55",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"accountId": "6d5e4f3a-2b1c-4098-8765-4321fedcba09",
"code": "IP-000123",
"receivedReturnReasons": [],
"paymentMethod": "Fps",
"reference": "INV-2026-0917",
"amount": { "amountInMinorUnits": 125000, "value": 1250.00, "currency": "GBP" },
"valueDate": "2026-09-17",
"uetr": null,
"remitter": {
"name": "Acme Trading Ltd",
"accountNumber": "12345678",
"bankCode": "040004",
"address": {
"buildingNumber": "10",
"streetName": "High Street",
"city": "London",
"postalCode": "EC1A 1BB",
"regionState": null,
"country": "GB"
}
},
"fee": { "amountInMinorUnits": 50, "value": 0.50, "currency": "GBP" },
"settledAt": "2026-09-17T09:14:21+00:00",
"createdAt": "2026-09-17T09:14:20.112+00:00"
}
}inbound-payment.returned.v1
inbound-payment.returned.v1Sent only for a payment that had been allocated to one of your accounts; a payment returned to sender before it was ever allocated is not yours and raises nothing.
data field | Type | Meaning |
|---|---|---|
id | UUID | The inbound payment's id |
clientId | UUID | The member client the payment belonged to |
returnReason | string or null | The return reason code, for example AC04. The same value the inbound payment resource shows as its status reason |
{
"id": "b8e1c4d7-0a29-4f63-91be-3c5d7e2f8a04",
"type": "inbound-payment.returned.v1",
"occurredAt": "2026-09-18T14:05:31+00:00",
"data": {
"id": "3b8c5a41-9d27-4e6f-8a10-2c4b7e9f0d55",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"returnReason": "AC04"
}
}Outbound payment events
The lifecycle of a payment you send. Fetch the full object with GET /api/v1/outbound-payments/{id}.
| Event type | Raised when | Terminal |
|---|---|---|
outbound-payment.submitted.v1 | The payment has been submitted, through any channel. Carries the full resource | No |
outbound-payment.approved.v1 | The payment has had the last approval it needed | No |
outbound-payment.awaiting-funds.v1 | Waiting for the debiting account to be funded. You have to act | No |
outbound-payment.sent.v1 | The payment has left for the beneficiary bank | No |
outbound-payment.rejected.v1 | The payment will not be sent | Yes |
outbound-payment.returned.v1 | The beneficiary bank sent the money back | Yes |
outbound-payment.cancelled.v1 | Cancelled before it was sent | Yes |
outbound-payment.submitted.v1
outbound-payment.submitted.v1Carries the outbound payment resource. Sent once per payment, whether it was created in the Portal, through the API, as part of a mass payment, or by your IFX Payments team. A payment returned to draft and submitted again does not raise a second one.
data field | Type | Meaning |
|---|---|---|
id | UUID | The outbound payment's id |
clientId | UUID | The member client the payment belongs to |
payeeId | UUID | The payee it is sent to |
accountId | UUID | The account it debits |
code | string or null | The payment's reference code, for example OP-000456 |
status | string | The resource status when the event was published: Processing, Scheduled, AwaitingFunds, Sent, Rejected, Cancelled or Returned. A future-dated payment is Scheduled |
statusReason | string or null | Why the payment was rejected, returned or cancelled, when a reason was recorded |
paymentMethod | string | Fps, Chaps, Swift or Sepa |
chargeType | string or null | Who bears the SWIFT charges, OUR or SHA. Only on SWIFT payments |
reference | string or null | The reference shown to the payee |
internalReference | string or null | See the shared fields above |
purpose | string or null | The purpose of the payment |
amount | object | Amount and currency |
valueDate | date | The date the payment is due to be sent |
massPaymentId | UUID or null | The mass payment it came from, if any |
fxTradeId | UUID or null | The FX trade funding it, if any |
createdAt | timestamp | When the payment was created |
createdBy | string or null | See the shared fields above |
channel | string or null | See the shared fields above |
fee | object or null | See the shared fields above |
ultimateDebtor | object or null | The party the payment is made on behalf of, for third- and fourth-party payments: name, identificationNumber, country, buildingName, buildingNumber, streetAddress, city, county, postCode. Only name is always present |
{
"id": "4e2d6f81-7a3b-4c95-8e10-b2c4d6e8f0a3",
"type": "outbound-payment.submitted.v1",
"occurredAt": "2026-09-17T09:58:03.21+00:00",
"data": {
"id": "a71d9e36-5c82-4f0b-9317-6e4d2a8c1b09",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"payeeId": "9b8a7c6d-5e4f-4321-a0b9-c8d7e6f5a4b3",
"accountId": "6d5e4f3a-2b1c-4098-8765-4321fedcba09",
"code": "OP-000456",
"status": "Processing",
"statusReason": null,
"paymentMethod": "Swift",
"chargeType": "SHA",
"reference": "Invoice 4471",
"internalReference": "po-88213",
"purpose": "Supplier payment",
"amount": { "amountInMinorUnits": 980050, "value": 9800.50, "currency": "EUR" },
"valueDate": "2026-09-17",
"massPaymentId": null,
"fxTradeId": null,
"createdAt": "2026-09-17T09:58:01+00:00",
"createdBy": "API User",
"channel": "Api",
"fee": null,
"ultimateDebtor": null
}
}The status events
approved, awaiting-funds, sent, rejected, returned and cancelled share one small shape:
data field | Type | Meaning | On |
|---|---|---|---|
id | UUID | The outbound payment's id | All |
clientId | UUID | The member client the payment belongs to | All |
internalReference | string or null | See the shared fields above | All |
reason | string or null | The same value the resource shows as statusReason. A cancellation you made through the API reads back as the reason you sent: incorrect_amount, incorrect_payee or payment_not_required | returned, cancelled only |
{
"id": "5c4a7b20-8e19-4d3f-b6a2-0f1e2d3c4b5a",
"type": "outbound-payment.sent.v1",
"occurredAt": "2026-09-17T10:02:07+00:00",
"data": {
"id": "a71d9e36-5c82-4f0b-9317-6e4d2a8c1b09",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"internalReference": "po-88213"
}
}{
"id": "8a1b2c3d-4e5f-4a6b-9c7d-0e1f2a3b4c5d",
"type": "outbound-payment.rejected.v1",
"occurredAt": "2026-09-17T10:02:07+00:00",
"data": {
"id": "a71d9e36-5c82-4f0b-9317-6e4d2a8c1b09",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"internalReference": "po-88213"
}
}approved is raised by the approval that completes the set: when a payment needs several signatories, the earlier approvals raise nothing. A payment that needs no approval never raises it.
sent is not a confirmation that the beneficiary has been credited, which is the receiving bank's business, and it is not terminal: a payment can still be returned days later. Subscribe to outbound-payment.returned.v1 if that matters to you.
awaiting-funds and rejected are sent at most once per payment, even though the underlying check can fail repeatedly. A payment that sits unfunded for days produces one awaiting-funds, not one per retry.
Payments that stay inside IFX PaymentsA payment routed between two accounts we hold raises none of these events, because the outbound payments resource does not serve them and the id would not fetch. Use the transaction feed to reconcile those.
Internal payment events
internal-payment.completed.v1 fires when a transfer between your own accounts has completed. Terminal. It carries the internal payment resource, as GET /api/v1/internal-payments/{id} returns it.
data field | Type | Meaning |
|---|---|---|
id | UUID | The internal payment's id |
clientId | UUID | The member client the transfer belongs to |
code | string | The transfer's reference code |
debitingAccount | object | The account debited: id, and iban when it has one |
creditingAccount | object | The account credited: id, and iban when it has one |
amount | object | Amount and currency |
reference | string or null | The reference shown to the account holder |
internalReference | string or null | See the shared fields above |
status | string | The resource status, Completed on this event |
valueDate | date | The date the transfer moves the funds |
channel | string or null | See the shared fields above |
createdAt | timestamp | When the transfer was created |
createdBy | string or null | See the shared fields above |
There is no failure event for internal payments: a transfer is validated in full before it is created and completes synchronously, so the create call tells you everything an event could. If POST /api/v1/internal-payments returns success, the transfer happened.
{
"id": "2a6b8c0d-1e3f-4a5b-8c7d-9e0f1a2b3c4d",
"type": "internal-payment.completed.v1",
"occurredAt": "2026-09-17T11:30:44.62+00:00",
"data": {
"id": "d4c3b2a1-9876-4f5e-8d7c-6b5a49382716",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"code": "IAT-000042",
"debitingAccount": { "id": "6d5e4f3a-2b1c-4098-8765-4321fedcba09", "iban": "GB33IFXP04000412345678" },
"creditingAccount": { "id": "1f2e3d4c-5b6a-4079-8869-abcdef012345", "iban": "GB61IFXP04000487654321" },
"amount": { "amountInMinorUnits": 250000, "value": 2500.00, "currency": "GBP" },
"reference": "Move to ops",
"internalReference": "sweep-0917",
"status": "Completed",
"valueDate": "2026-09-17",
"channel": "Api",
"createdAt": "2026-09-17T11:30:44+00:00",
"createdBy": "API User"
}
}FX trade events
The life of a trade. Fetch the full object with GET /api/v1/fx/trades/{id}.
| Event type | Raised when | Terminal |
|---|---|---|
fx-trade.created.v1 | The trade has been created. Carries the full resource | No |
fx-trade.booked.v1 | The broker has confirmed the trade at the agreed rate | No |
fx-trade.settled.v1 | Both legs have settled and the bought currency is usable | Yes |
fx-trade.failed.v1 | The trade will not settle, including when it was cancelled | Yes |
booked, settled and failed are each sent at most once per trade. Reverse legs raise nothing: the FX trades resource hides them, so the id would not fetch.
bookedand theBookedstatus are different momentsA new trade's resource status is
Booked, and that is whatfx-trade.created.v1shows. Thefx-trade.booked.v1event comes later, when the broker confirms; by then the resource status isAwaitingSettlement.
fx-trade.created.v1
fx-trade.created.v1Carries the FX trade resource.
data field | Type | Meaning |
|---|---|---|
id | UUID | The trade's id |
accountId | UUID | The account the trade books against |
clientId | UUID | The member client the trade belongs to |
valueDate | date | The date both legs settle |
code | string | The trade's reference code, for example FX-000789 |
sell | object | The amount and currency you sell |
buy | object | The amount and currency you buy |
fixedSide | string | The side whose amount you fixed: Buy or Sell |
sellRate | number | Your rate quoted from the sell currency; buyRate is its inverse |
buyRate | number | Your rate quoted from the buy currency |
status | string | The resource status when the event was published: Booked, AwaitingSettlement, Settled, Failed or Cancelled |
createdBy | string or null | See the shared fields above |
createdAt | timestamp | When the trade was created |
channel | string or null | See the shared fields above |
internalReference | string or null | See the shared fields above |
{
"id": "3f4e5d6c-7b8a-4912-8a3b-4c5d6e7f8091",
"type": "fx-trade.created.v1",
"occurredAt": "2026-09-17T08:30:12.055+00:00",
"data": {
"id": "e5f6a7b8-c9d0-4e1f-a2b3-c4d5e6f70819",
"accountId": "6d5e4f3a-2b1c-4098-8765-4321fedcba09",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"valueDate": "2026-09-19",
"code": "FX-000789",
"sell": { "amountInMinorUnits": 1000000, "value": 10000.00, "currency": "GBP" },
"buy": { "amountInMinorUnits": 1168200, "value": 11682.00, "currency": "EUR" },
"fixedSide": "Sell",
"sellRate": 1.1682,
"buyRate": 0.856018,
"status": "Booked",
"createdBy": "Jane Smith",
"createdAt": "2026-09-17T08:30:11+00:00",
"channel": "Portal",
"internalReference": null
}
}The status events
booked, settled and failed share one small shape:
data field | Type | Meaning | On |
|---|---|---|---|
id | UUID | The trade's id | All |
clientId | UUID | The member client the trade belongs to | All |
internalReference | string or null | See the shared fields above | All |
failedReason | string or null | Why the trade will not settle, worded as the trade resource words it | failed only |
failedReason is one of:
failedReason | Resource status |
|---|---|
| The trade was cancelled before execution. | Cancelled |
| The trade could not be sent to the broker. | Failed |
| The broker returned an error while booking the trade. | Failed |
| The broker rejected the trade. | Failed |
| The market rate moved beyond the allowed threshold before execution. | Failed |
| The trade could not be re-quoted for execution. | Failed |
| The trade could not be completed. | Failed |
A cancelled trade arrives as fx-trade.failed.v1, not as a separate type. Treat the text as display text: branch on the resource status if you need to tell a cancellation from a failure.
{
"id": "6f0d8b52-3e47-4a91-bc05-8d7e6f5a4b32",
"type": "fx-trade.settled.v1",
"occurredAt": "2026-09-19T09:00:12+00:00",
"data": {
"id": "e5f6a7b8-c9d0-4e1f-a2b3-c4d5e6f70819",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"internalReference": "hedge-0917-01"
}
}{
"id": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
"type": "fx-trade.failed.v1",
"occurredAt": "2026-09-17T08:31:02+00:00",
"data": {
"id": "e5f6a7b8-c9d0-4e1f-a2b3-c4d5e6f70819",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"internalReference": "hedge-0917-01",
"failedReason": "The broker rejected the trade."
}
}Transaction events
transaction.created.v1 fires whenever a ledger movement is booked on one of your currency accounts. It is the ledger feed rather than a companion to the payment and trade types: one payment or trade produces several movements, so this is by far the highest-volume type. Subscribe to it only if you are building a ledger mirror, and size your receiver for it.
It carries the transaction resource.
data field | Type | Meaning |
|---|---|---|
transactionId | UUID | The transaction's id. Together with currencyAccountId it names one movement |
currencyAccountId | UUID | The currency account the movement was booked on |
accountId | UUID | The account that owns that currency account |
clientId | UUID | The member client the movement belongs to |
date | date | The date the movement takes value |
amount | object | Amount and currency |
direction | string | Credit or Debit |
transactionType | string | What produced the movement, spelled as the transactions resource spells it, so you can filter without fetching |
reference | string | The reference of the payment behind the movement. An empty string, never null, when there is none |
code | string | The code of the payment, trade or reconciliation behind the movement. An empty string, never null, when there is none |
sourceType | string or null | The kind of object behind the movement: OutboundPayment, InboundPayment, FxTrade, FxTreasuryTrade, TreasuryPayment or InterAccountTransfer |
sourceId | UUID or null | The id of that object |
invoiceNumber | string or null | The invoice the movement relates to, if any |
createdAt | timestamp | When we recorded the movement |
Two spellings for internal payments
sourceTypeuses the transactions resource's spelling,InterAccountTransfer. The events resource'srelatedObjectTypeand the internal payment events call the same objectInternalPayment. Same id, same object.
{
"id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"type": "transaction.created.v1",
"occurredAt": "2026-09-17T09:14:23.004+00:00",
"data": {
"transactionId": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
"currencyAccountId": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"accountId": "6d5e4f3a-2b1c-4098-8765-4321fedcba09",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"date": "2026-09-17",
"amount": { "amountInMinorUnits": 125000, "value": 1250.00, "currency": "GBP" },
"direction": "Credit",
"transactionType": "InboundPayment",
"reference": "INV-2026-0917",
"code": "IP-000123",
"sourceType": "InboundPayment",
"sourceId": "3b8c5a41-9d27-4e6f-8a10-2c4b7e9f0d55",
"invoiceNumber": null,
"createdAt": "2026-09-17T09:14:23+00:00"
}
}One movement can touch more than one currency account; each currency account it touched produces its own event. GET /api/v1/transactions?transactionId={transactionId} returns every row for the transaction.
Sub-account request events
A sub-account request is what POST /api/v1/sub-accounts records: your ask for a new sub-account under one of your clients, with one default virtual IBAN. Your IFX Payments team reviews it, and each step raises one of these events. Fetch the full object with GET /api/v1/sub-account-requests/{id}.
| Event type | Raised when | Terminal |
|---|---|---|
sub-account-request.accepted.v1 | The sub-account has been created and its virtual IBAN is under review. subAccountId is now known, but the sub-account is not yet listed by GET /api/v1/sub-accounts | No |
sub-account-request.activated.v1 | The IBAN has been allocated and the sub-account approved. iban is now known | Yes |
sub-account-request.rejected.v1 | The request was refused, by validation or by review. errors carries any field-level detail | Yes |
sub-account-request.failed.v1 | The IBAN could not be allocated. errors carries any field-level detail | Yes |
Each type is sent at most once per request, and duplicate or late updates are ignored. A request moves Pending → Accepted → Active, or ends in Rejected or Failed. rejected can arrive straight from Pending, or after accepted if the review refuses it; in the second case subAccountId stays set, because the sub-account was created before the refusal. failed can likewise arrive with or without an accepted before it.
The event type and the resource status pair up as follows:
| Event type | status |
|---|---|
sub-account-request.accepted.v1 | Accepted |
sub-account-request.activated.v1 | Active |
sub-account-request.rejected.v1 | Rejected |
sub-account-request.failed.v1 | Failed |
All four carry the sub-account request resource, exactly as its GET returns it at the moment the event is published.
data field | Type | Meaning |
|---|---|---|
id | UUID | The sub-account request's id |
clientId | UUID | The parent client the sub-account was requested under. See the shared fields above |
status | string | The resource status when the event was published. See the pairing table above |
entityType | string | What you asked for, as you sent it: Individual, or a legal type such as LimitedCompany, PublicLimitedCompany, SoleTrader, Charity, LimitedPartnership, LimitedLiabilityPartnership, Trust, Partnership, LimitedLiabilityCompany or OtherLegalEntity |
name | string | The legal name you sent, or for an individual the given and family names joined in the order you asked for |
friendlyName | string or null | The default virtual IBAN's friendly name, as you sent it |
purpose | string or null | The default virtual IBAN's purpose, as you sent it |
transactionActivitySubTypes | array or null | The activity codes you sent for the default virtual IBAN, as integers from GET /api/v1/reference/transaction-activity-sub-types. null when you sent none |
subAccountId | UUID or null | The new sub-account's client id. null until the sub-account has been created, so always set from accepted onwards. Use it as clientId on later calls for that sub-account |
iban | string or null | The default virtual IBAN. Set on activated; null before that |
errors | array | Field-level problems reported on a rejected or failed request, each { "property", "message" }; property is null for a problem not tied to one field. An empty array, never null, when there are none |
createdAt | timestamp | When the request was recorded |
updatedAt | timestamp or null | When the request last changed status. null until the first outcome, so always set on these events |
The request carries the IBAN only. For the account number, sort code and BIC of the default virtual IBAN, call GET /api/v1/accounts?clientId={subAccountId} once the request is Active; the account resource carries all four.
There is no internalReference on either request family: POST /api/v1/sub-accounts and POST /api/v1/virtual-accounts do not accept one. Correlate on the request id you were given at creation, or on your idempotencyKey.
{
"id": "7c2e9a40-1b5d-4f8e-93a7-6d0c4b2e8f15",
"type": "sub-account-request.accepted.v1",
"occurredAt": "2026-10-06T10:12:45.318+00:00",
"data": {
"id": "f3a1c6d8-2e4b-4a90-b7c5-1d8e6f0a2b3c",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"status": "Accepted",
"entityType": "LimitedCompany",
"name": "Acme Trading Ltd",
"friendlyName": "Operations",
"purpose": "Collections",
"transactionActivitySubTypes": [50005000, 20002000],
"subAccountId": "a9b8c7d6-e5f4-4321-8a0b-9c8d7e6f5a4b",
"iban": null,
"errors": [],
"createdAt": "2026-10-06T09:58:02+00:00",
"updatedAt": "2026-10-06T10:12:45+00:00"
}
}{
"id": "0e5d8c3b-7a92-4f16-b4d8-2c1a9e7f6b05",
"type": "sub-account-request.rejected.v1",
"occurredAt": "2026-10-07T16:20:11+00:00",
"data": {
"id": "5b0d2e9f-8c47-4a13-b6e5-9d2f7a1c4e38",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"status": "Rejected",
"entityType": "LimitedCompany",
"name": "Northwind Holdings Ltd",
"friendlyName": "Treasury",
"purpose": null,
"transactionActivitySubTypes": null,
"subAccountId": null,
"iban": null,
"errors": [
{ "property": null, "message": "The parent entity is not an intermediary." }
],
"createdAt": "2026-10-07T16:19:48+00:00",
"updatedAt": "2026-10-07T16:20:11+00:00"
}
}Virtual account request events
A virtual account request is what POST /api/v1/virtual-accounts records: your ask for one more virtual IBAN on a client you act for, or on one of its active sub-accounts. Fetch the full object with GET /api/v1/virtual-account-requests/{id}.
| Event type | Raised when | Terminal |
|---|---|---|
virtual-account-request.accepted.v1 | The virtual IBAN record has been created and is under review. virtualAccountId is now known | No |
virtual-account-request.allocated.v1 | The account details have been allocated. iban, accountNumber, sortCode and swiftBic are now known | Yes |
virtual-account-request.rejected.v1 | The request was refused, by validation or by review. errors carries any field-level detail | Yes |
virtual-account-request.failed.v1 | The virtual IBAN could not be completed. errors carries any field-level detail | Yes |
Each type is sent at most once per request, and duplicate or late updates are ignored. A request moves Pending → Accepted → Allocated, or ends in Rejected or Failed. rejected can arrive straight from Pending, or after accepted if the review refuses it; in the second case virtualAccountId stays set. failed can likewise arrive with or without an accepted before it.
The event type and the resource status pair up as follows:
| Event type | status |
|---|---|
virtual-account-request.accepted.v1 | Accepted |
virtual-account-request.allocated.v1 | Allocated |
virtual-account-request.rejected.v1 | Rejected |
virtual-account-request.failed.v1 | Failed |
All four carry the virtual account request resource, exactly as its GET returns it at the moment the event is published.
data field | Type | Meaning |
|---|---|---|
id | UUID | The virtual account request's id |
clientId | UUID | The client the virtual IBAN was requested for, as you declared it: a member client or one of its active sub-accounts. See the shared fields above |
status | string | The resource status when the event was published. See the pairing table above |
friendlyName | string or null | The friendly name you sent |
transactionActivitySubTypes | array | The activity codes you sent, as integers from GET /api/v1/reference/transaction-activity-sub-types. Never empty, because the request requires at least one |
virtualAccountId | UUID or null | Our id for the virtual IBAN record. null until the record has been created, so always set from accepted onwards |
iban | string or null | The allocated IBAN. Set on allocated; null before that |
accountNumber | string or null | The allocated account number. Set on allocated; null before that |
sortCode | string or null | The allocated sort code. Set on allocated; null before that |
swiftBic | string or null | The BIC the allocated IBAN is reachable at. Set on allocated; null before that |
errors | array | Field-level problems reported on a rejected or failed request, each { "property", "message" }; property is null for a problem not tied to one field. An empty array, never null, when there are none |
createdAt | timestamp | When the request was recorded |
updatedAt | timestamp or null | When the request last changed status. null until the first outcome, so always set on these events |
{
"id": "4b9f2c71-8d3e-4a65-9c10-e7f5a3b1d2c8",
"type": "virtual-account-request.allocated.v1",
"occurredAt": "2026-10-29T15:41:09.77+00:00",
"data": {
"id": "2d7e4f1a-9b3c-4e58-a6f0-8c1b5d9e3a72",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"status": "Allocated",
"friendlyName": "EUR collections",
"transactionActivitySubTypes": [50005000, 30005000],
"virtualAccountId": "6e1a9c5b-3f7d-4b82-9a04-d2c8e6f1b5a3",
"iban": "GB29IFXS60161331926819",
"accountNumber": "31926819",
"sortCode": "601613",
"swiftBic": "IFXSGB2L",
"errors": [],
"createdAt": "2026-10-29T14:03:55+00:00",
"updatedAt": "2026-10-29T15:41:09+00:00"
}
}{
"id": "9a3c7e15-2f8b-4d60-b1e4-c5d9f7a2e8b6",
"type": "virtual-account-request.rejected.v1",
"occurredAt": "2026-10-30T09:06:30+00:00",
"data": {
"id": "8f4b6d2a-1c9e-4735-a0d8-3e7c5b9f2a61",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"status": "Rejected",
"friendlyName": "USD collections",
"transactionActivitySubTypes": [50005000],
"virtualAccountId": null,
"iban": null,
"accountNumber": null,
"sortCode": null,
"swiftBic": null,
"errors": [
{ "property": null, "message": "The entity is not active." }
],
"createdAt": "2026-10-30T09:05:52+00:00",
"updatedAt": "2026-10-30T09:06:30+00:00"
}
}
rejectedandfailedgive you the field errors and nothing elseThis applies to both request families. Where
outbound-payment.returned.v1andoutbound-payment.cancelled.v1carry areason, andfx-trade.failed.v1afailedReason, a rejected or failed request has no reason field, in the event or on the resource.errorsis the whole explanation; when it is empty, the request was refused at review and no further detail is given.
Follow the request, not the accountNeither family raises an event on the sub-account or virtual IBAN itself. The request's
idis the object every event carries, andsubAccountIdorvirtualAccountIdis how you get from the request to the thing it created. The sub-account appears inGET /api/v1/sub-accountsonly once the request isActive; before that, the request is the only way to see it.
The verification ping
endpoint.verification.v1 is a system event. You cannot subscribe to it and you cannot turn it off: it is sent when an endpoint is first registered, when its URL changes, and whenever verification is re-run. It is an ordinary signed delivery, so answering it proves your whole receive path works: TLS, routing, signature verification and all.
data field | Type | Meaning |
|---|---|---|
endpointId | UUID | The endpoint being verified |
{
"id": "1d7f9a3e-2c58-4b61-9f0d-7a6e5c4b3210",
"type": "endpoint.verification.v1",
"occurredAt": "2026-09-17T08:59:02+00:00",
"data": { "endpointId": "8f2b6c14-5a7d-4e39-b0c1-9d8e7f6a5b43" }
}Answer any 2xx and the endpoint becomes Active; see Delivery, retries and endpoint health for what happens when it does not. Your handler should verify the signature and return 2xx without needing to understand the type. Treating an unknown type as "acknowledge and ignore" is the right default for every event.
The same events over HTTP
GET /api/v1/events returns these events as a paginated list, and GET /api/v1/events/{id} returns one by the id its webhook body carried. Scope: events:read. The window is 90 days.
{
"id": "9f1c2f0e-4c3b-4b8a-9d2a-5f7c1e6b0a11",
"type": "inbound-payment.received.v1",
"clientId": "c0a81f42-7b3e-4d19-9f88-1e2d3c4b5a60",
"relatedObjectId": "3b8c5a41-9d27-4e6f-8a10-2c4b7e9f0d55",
"relatedObjectType": "InboundPayment",
"occurredAt": "2026-09-17T09:14:22.431+00:00",
"createdAt": "2026-09-17T09:14:23.118+00:00",
"data": { }
}data is the webhook body's data block verbatim, as it was when the event was published, so anything you can do with a delivery you can do with a polled event. relatedObjectType is InboundPayment, OutboundPayment, InternalPayment, FxTrade or Transaction and names the resource relatedObjectId belongs to, so you can turn a row into a GET without a lookup table.
Poll on createdAt, not occurredAt. createdAt is when the event was raised on the channel; occurredAt is the business instant, and processing lag means an event with an older occurredAt can appear after one you have already seen. Default ordering is createdAt descending.
Filters: clientId, type, relatedObjectId, relatedObjectType, occurredFrom, occurredTo, createdFrom, createdTo, plus pageNumber (max 100000), pageSize (1–200, default 50), orderBy and orderDirection. An event is recorded whether or not you have an endpoint registered and whatever happened to its delivery, which is what makes this a real recovery path rather than a second view of the same failure.
Updated 4 days ago