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": { }
}
FieldTypeMeaning
idUUIDThe identity of the business event, and your de-duplication key. Stable across retries, replays and event-type versions
typestringThe versioned event type, for example inbound-payment.received.v1
occurredAttimestampWhen the platform recorded the business fact
dataobjectType-specific payload

Notes that save time later:

  • occurredAt renders its offset literally: +00:00 for UTC, never Z, 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 inside data (createdAt, settledAt).
  • It is not a delivery timestamp. When an attempt was sent is the signature's created; which attempt it is is the Webhook-Attempt header. Neither appears in the body, which is why the body is byte-identical on every attempt.
  • Amounts are integers in minor units. A value field alongside them is the major-unit rendering, for display only. Do arithmetic on amountInMinorUnits.
  • Dates are YYYY-MM-DD. date andvalueDate fields carry a calendar date with no time or offset.
  • All JSON is camelCase and compact. Optional fields are present with a null value, not omitted, so read reason as "may be null" rather than "may be missing".

The amount object

{ "amountInMinorUnits": 125000, "value": 1250.00, "currency": "GBP" }
FieldMeaning
amountInMinorUnitsThe exact integer amount in the currency's smallest unit. Authoritative
valueThe same amount in major units, as we rendered it
currencyISO 4217 code

What changed in this release

🚧

Payload field names have changed

Every payload now keys its object as id (it was paymentId or tradeId), 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 from GET /api/v1/events.

New event types

Event typeWhat it gives you
outbound-payment.submitted.v1The full outbound payment resource, once per payment, whatever channel it was created through
outbound-payment.approved.v1The payment has had its last required approval
fx-trade.created.v1The 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 typeBeforeNow
All inbound-payment.*, outbound-payment.* and internal-payment.* typespaymentIdid
All fx-trade.* typestradeIdid
inbound-payment.received.v1clientAccountIdaccountId
inbound-payment.received.v1paymentCodecode
inbound-payment.returned.v1reasonreturnReason
fx-trade.failed.v1reasonfailedReason (now readable text, see below)
internal-payment.completed.v1debitingAccountId, creditingAccountIddebitingAccount.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 data block 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 .v2 ships, the .v1 is 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 same id, so de-duplicating on id naturally 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 its GET returns 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, the clientId, your internalReference and, where it applies, a reason. Fetch the object if you need more.

Fields that appear on several types mean the same thing everywhere:

FieldMeaning
clientIdThe member client the object belongs to. An endpoint registered for a group receives events for every member, so route on this
internalReferenceYour own reference, as you sent it when creating the object through the API. null for objects created elsewhere
channelHow 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
createdByThe name of the user who created it. Only given when channel is Portal or Api; null otherwise
feeAn 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 typeRaised whenTerminal
inbound-payment.received.v1The payment has been allocated to one of your accountsNo
inbound-payment.returned.v1The funds have gone back to the remitterYes

inbound-payment.received.v1

Carries the inbound payment resource.

data fieldTypeMeaning
idUUIDThe inbound payment's id
clientIdUUIDThe member client the payment belongs to
accountIdUUIDThe account it was allocated to
codestring or nullThe payment's reference code, for example IP-000123
receivedReturnReasonsarraySet 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
paymentMethodstringThe rail the funds arrived by, for example Fps, Chaps, Swift or Sepa
referencestring or nullThe reference the remitter gave
amountobjectAmount and currency
valueDatedateThe date the funds were credited
uetrstring or nullThe SWIFT unique end-to-end transaction reference, when the bank gave one
remitterobjectWho sent it: name, accountNumber, bankCode (a sort code or BIC), and address
remitter.addressobject or nullbuildingNumber, streetName, city, postalCode, regionState, country, each as the bank gave it and each nullable. null when the bank gave no address at all
feeobject or nullSee the shared fields above
settledAttimestamp or nullWhen we settled the payment
createdAttimestampWhen 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

Sent 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 fieldTypeMeaning
idUUIDThe inbound payment's id
clientIdUUIDThe member client the payment belonged to
returnReasonstring or nullThe 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 typeRaised whenTerminal
outbound-payment.submitted.v1The payment has been submitted, through any channel. Carries the full resourceNo
outbound-payment.approved.v1The payment has had the last approval it neededNo
outbound-payment.awaiting-funds.v1Waiting for the debiting account to be funded. You have to actNo
outbound-payment.sent.v1The payment has left for the beneficiary bankNo
outbound-payment.rejected.v1The payment will not be sentYes
outbound-payment.returned.v1The beneficiary bank sent the money backYes
outbound-payment.cancelled.v1Cancelled before it was sentYes

outbound-payment.submitted.v1

Carries 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 fieldTypeMeaning
idUUIDThe outbound payment's id
clientIdUUIDThe member client the payment belongs to
payeeIdUUIDThe payee it is sent to
accountIdUUIDThe account it debits
codestring or nullThe payment's reference code, for example OP-000456
statusstringThe resource status when the event was published: Processing, Scheduled, AwaitingFunds, Sent, Rejected, Cancelled or Returned. A future-dated payment is Scheduled
statusReasonstring or nullWhy the payment was rejected, returned or cancelled, when a reason was recorded
paymentMethodstringFps, Chaps, Swift or Sepa
chargeTypestring or nullWho bears the SWIFT charges, OUR or SHA. Only on SWIFT payments
referencestring or nullThe reference shown to the payee
internalReferencestring or nullSee the shared fields above
purposestring or nullThe purpose of the payment
amountobjectAmount and currency
valueDatedateThe date the payment is due to be sent
massPaymentIdUUID or nullThe mass payment it came from, if any
fxTradeIdUUID or nullThe FX trade funding it, if any
createdAttimestampWhen the payment was created
createdBystring or nullSee the shared fields above
channelstring or nullSee the shared fields above
feeobject or nullSee the shared fields above
ultimateDebtorobject or nullThe 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 fieldTypeMeaningOn
idUUIDThe outbound payment's idAll
clientIdUUIDThe member client the payment belongs toAll
internalReferencestring or nullSee the shared fields aboveAll
reasonstring or nullThe 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_requiredreturned, 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 Payments

A 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 fieldTypeMeaning
idUUIDThe internal payment's id
clientIdUUIDThe member client the transfer belongs to
codestringThe transfer's reference code
debitingAccountobjectThe account debited: id, and iban when it has one
creditingAccountobjectThe account credited: id, and iban when it has one
amountobjectAmount and currency
referencestring or nullThe reference shown to the account holder
internalReferencestring or nullSee the shared fields above
statusstringThe resource status, Completed on this event
valueDatedateThe date the transfer moves the funds
channelstring or nullSee the shared fields above
createdAttimestampWhen the transfer was created
createdBystring or nullSee 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 typeRaised whenTerminal
fx-trade.created.v1The trade has been created. Carries the full resourceNo
fx-trade.booked.v1The broker has confirmed the trade at the agreed rateNo
fx-trade.settled.v1Both legs have settled and the bought currency is usableYes
fx-trade.failed.v1The trade will not settle, including when it was cancelledYes

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.

📘

booked and the Booked status are different moments

A new trade's resource status is Booked, and that is what fx-trade.created.v1 shows. The fx-trade.booked.v1 event comes later, when the broker confirms; by then the resource status is AwaitingSettlement.

fx-trade.created.v1

Carries the FX trade resource.

data fieldTypeMeaning
idUUIDThe trade's id
accountIdUUIDThe account the trade books against
clientIdUUIDThe member client the trade belongs to
valueDatedateThe date both legs settle
codestringThe trade's reference code, for example FX-000789
sellobjectThe amount and currency you sell
buyobjectThe amount and currency you buy
fixedSidestringThe side whose amount you fixed: Buy or Sell
sellRatenumberYour rate quoted from the sell currency; buyRate is its inverse
buyRatenumberYour rate quoted from the buy currency
statusstringThe resource status when the event was published: Booked, AwaitingSettlement, Settled, Failed or Cancelled
createdBystring or nullSee the shared fields above
createdAttimestampWhen the trade was created
channelstring or nullSee the shared fields above
internalReferencestring or nullSee 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 fieldTypeMeaningOn
idUUIDThe trade's idAll
clientIdUUIDThe member client the trade belongs toAll
internalReferencestring or nullSee the shared fields aboveAll
failedReasonstring or nullWhy the trade will not settle, worded as the trade resource words itfailed only

failedReason is one of:

failedReasonResource 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 fieldTypeMeaning
transactionIdUUIDThe transaction's id. Together with currencyAccountId it names one movement
currencyAccountIdUUIDThe currency account the movement was booked on
accountIdUUIDThe account that owns that currency account
clientIdUUIDThe member client the movement belongs to
datedateThe date the movement takes value
amountobjectAmount and currency
directionstringCredit or Debit
transactionTypestringWhat produced the movement, spelled as the transactions resource spells it, so you can filter without fetching
referencestringThe reference of the payment behind the movement. An empty string, never null, when there is none
codestringThe code of the payment, trade or reconciliation behind the movement. An empty string, never null, when there is none
sourceTypestring or nullThe kind of object behind the movement: OutboundPayment, InboundPayment, FxTrade, FxTreasuryTrade, TreasuryPayment or InterAccountTransfer
sourceIdUUID or nullThe id of that object
invoiceNumberstring or nullThe invoice the movement relates to, if any
createdAttimestampWhen we recorded the movement
📘

Two spellings for internal payments

sourceType uses the transactions resource's spelling, InterAccountTransfer. The events resource's relatedObjectType and the internal payment events call the same object InternalPayment. 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 typeRaised whenTerminal
sub-account-request.accepted.v1The 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-accountsNo
sub-account-request.activated.v1The IBAN has been allocated and the sub-account approved. iban is now knownYes
sub-account-request.rejected.v1The request was refused, by validation or by review. errors carries any field-level detailYes
sub-account-request.failed.v1The IBAN could not be allocated. errors carries any field-level detailYes

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 typestatus
sub-account-request.accepted.v1Accepted
sub-account-request.activated.v1Active
sub-account-request.rejected.v1Rejected
sub-account-request.failed.v1Failed

All four carry the sub-account request resource, exactly as its GET returns it at the moment the event is published.

data fieldTypeMeaning
idUUIDThe sub-account request's id
clientIdUUIDThe parent client the sub-account was requested under. See the shared fields above
statusstringThe resource status when the event was published. See the pairing table above
entityTypestringWhat 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
namestringThe legal name you sent, or for an individual the given and family names joined in the order you asked for
friendlyNamestring or nullThe default virtual IBAN's friendly name, as you sent it
purposestring or nullThe default virtual IBAN's purpose, as you sent it
transactionActivitySubTypesarray or nullThe 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
subAccountIdUUID or nullThe 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
ibanstring or nullThe default virtual IBAN. Set on activated; null before that
errorsarrayField-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
createdAttimestampWhen the request was recorded
updatedAttimestamp or nullWhen 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 typeRaised whenTerminal
virtual-account-request.accepted.v1The virtual IBAN record has been created and is under review. virtualAccountId is now knownNo
virtual-account-request.allocated.v1The account details have been allocated. iban, accountNumber, sortCode and swiftBic are now knownYes
virtual-account-request.rejected.v1The request was refused, by validation or by review. errors carries any field-level detailYes
virtual-account-request.failed.v1The virtual IBAN could not be completed. errors carries any field-level detailYes

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 typestatus
virtual-account-request.accepted.v1Accepted
virtual-account-request.allocated.v1Allocated
virtual-account-request.rejected.v1Rejected
virtual-account-request.failed.v1Failed

All four carry the virtual account request resource, exactly as its GET returns it at the moment the event is published.

data fieldTypeMeaning
idUUIDThe virtual account request's id
clientIdUUIDThe 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
statusstringThe resource status when the event was published. See the pairing table above
friendlyNamestring or nullThe friendly name you sent
transactionActivitySubTypesarrayThe activity codes you sent, as integers from GET /api/v1/reference/transaction-activity-sub-types. Never empty, because the request requires at least one
virtualAccountIdUUID or nullOur id for the virtual IBAN record. null until the record has been created, so always set from accepted onwards
ibanstring or nullThe allocated IBAN. Set on allocated; null before that
accountNumberstring or nullThe allocated account number. Set on allocated; null before that
sortCodestring or nullThe allocated sort code. Set on allocated; null before that
swiftBicstring or nullThe BIC the allocated IBAN is reachable at. Set on allocated; null before that
errorsarrayField-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
createdAttimestampWhen the request was recorded
updatedAttimestamp or nullWhen 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"
  }
}
📘

rejected and failed give you the field errors and nothing else

This applies to both request families. Where outbound-payment.returned.v1 and outbound-payment.cancelled.v1 carry a reason, and fx-trade.failed.v1 a failedReason, a rejected or failed request has no reason field, in the event or on the resource. errors is the whole explanation; when it is empty, the request was refused at review and no further detail is given.

📘

Follow the request, not the account

Neither family raises an event on the sub-account or virtual IBAN itself. The request's id is the object every event carries, and subAccountId or virtualAccountId is how you get from the request to the thing it created. The sub-account appears in GET /api/v1/sub-accounts only once the request is Active; 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 fieldTypeMeaning
endpointIdUUIDThe 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.


Did this page help you?