OpenFX generates events whenever a resource is created, updated, or transitions to a new
status. Each event type follows the naming convention resource.action and carries a
consistent payload envelope with a full resource snapshot.
Payload structure
Every webhook delivery and every record in the Events API uses this structure:
{
"id": "whd_01953e1a5f4b7100",
"type": "payment.completed",
"createdAt": "2026-02-23T12:05:00Z",
"data": {
"resourceType": "payment",
"resourceId": "pmt_01953e1a5f4b7005",
"snapshot": {
"id": "pmt_01953e1a5f4b7005",
"status": "completed",
"rail": "ach",
"sourceAccountId": "acc_01953e1a5f4b7002",
"counterpartyId": "cpt_01953e1a5f4b7004",
"sendAmount": {
"currency": "USD",
"exponent": 2,
"value": "150000",
"displayValue": "1500.00"
},
"createdAt": "2026-02-23T12:00:00Z",
"updatedAt": "2026-02-23T12:05:00Z"
}
}
}
| Field | Type | Description |
|---|
id | string | Unique delivery ID (whd_ prefix). Use for deduplication. |
type | string | Event type in resource.action format. |
createdAt | string | ISO 8601 timestamp when the event was generated. |
data.resourceType | string | Type of the affected resource (e.g., payment, customer, account). |
data.resourceId | string | ID of the affected resource with its typed prefix. |
data.snapshot | object | Full resource object as it existed at the time of the event. The shape matches the corresponding GET endpoint response. |
previousAttributes | object | Only present on .updated and .status_changed events. Contains the previous values of fields that changed. |
previousAttributes for change events
Events with .updated or .status_changed suffixes include a previousAttributes object
at the top level that shows what changed:
{
"id": "whd_01953e1a5f4b7102",
"type": "account.status_changed",
"createdAt": "2026-02-23T12:01:00Z",
"data": {
"resourceType": "account",
"resourceId": "acc_01953e1a5f4b7002",
"snapshot": {
"id": "acc_01953e1a5f4b7002",
"status": "active",
"type": "demand_deposit",
"currency": "USD"
}
},
"previousAttributes": {
"status": "pending"
}
}
Use previousAttributes to detect specific transitions without querying the API.
For example, you can detect a payment moving from processing to completed by
checking previousAttributes.status === "processing" and
data.snapshot.status === "completed".
Complete event type reference
Customer events (3)
| Event type | Trigger | Key payload fields |
|---|
customer.created | New customer created (via onboarding or directly) | entityId, status, kybStatus |
customer.status_changed | Customer status transitioned (e.g., draft to active) | status, previousAttributes.status |
customer.kyb_status_changed | KYB review result received | kybStatus, previousAttributes.kybStatus |
The customer.kyb_status_changed event is critical for onboarding flows. Listen for
kybStatus: "approved" in the snapshot to know when a customer is ready to create
accounts and send payments. Handle kybStatus: "rejected" to surface rejection reasons
to your users.
Account events (2)
| Event type | Trigger | Key payload fields |
|---|
account.created | New account created via POST /accounts | type, currency, customerId, status |
account.status_changed | Account status transitioned | status, previousAttributes.status |
Account number events (2)
| Event type | Trigger | Key payload fields |
|---|
account_number.created | Account number provisioned via POST /accounts/{id}/account-numbers | type, accountId |
account_number.status_changed | Account number status transitioned | status, previousAttributes.status |
Blockchain address events (1)
| Event type | Trigger | Key payload fields |
|---|
blockchain_address.created | Blockchain address generated via POST /accounts/{id}/blockchain-addresses | chain, asset, address, accountId |
Counterparty events (3)
| Event type | Trigger | Key payload fields |
|---|
counterparty.created | New counterparty created via POST /counterparties | name, customerId |
counterparty.activated | Counterparty activated after creation | status |
counterparty.archived | Counterparty archived | status |
Payment method events (3)
| Event type | Trigger | Key payload fields |
|---|
payment_method.created | Payment method added via POST /counterparties/{id}/payment-methods | type, currency, counterpartyId |
payment_method.validated | Payment method validation completed successfully | validationResult |
payment_method.rejected | Payment method validation failed | validationResult, status |
Payment events (10)
| Event type | Trigger | Key payload fields |
|---|
payment.created | New payment created via POST /payments or rail-specific endpoint | status, rail, sendAmount, sourceAccountId |
payment.requires_action | Payment needs user action (compliance, RFI, quote refresh) | status, requiresActionReason |
payment.in_review | Payment entered compliance review | status |
payment.processing | Payment submitted to the payment rail | status |
payment.completed | Payment successfully delivered to counterparty | status, receiveAmount, completedAt |
payment.returned | Payment returned by receiving bank | status, returnReason |
payment.reversed | Payment reversed after completion | status |
payment.refunded | Refund issued for payment | status |
payment.failed | Payment failed to process | status, failureReason |
payment.canceled | Payment canceled by caller | status |
Conversion events (4)
| Event type | Trigger | Key payload fields |
|---|
conversion.created | New FX conversion created via POST /fx/conversions | sellCurrency, buyCurrency, status |
conversion.processing | Conversion submitted for execution | status |
conversion.completed | Conversion settled successfully | status, exchangeRate, sellAmount, buyAmount |
conversion.failed | Conversion failed | status, failureReason |
Transfer events (3)
| Event type | Trigger | Key payload fields |
|---|
transfer.created | Internal transfer created via POST /transfers | sourceAccountId, destinationAccountId, amount |
transfer.completed | Transfer completed | status |
transfer.failed | Transfer failed | status, failureReason |
Transaction events (1)
| Event type | Trigger | Key payload fields |
|---|
transaction.created | New ledger entry created for any balance-affecting event | type, direction, amount, accountId, referenceType, referenceId |
The transaction.created event fires for every balance change — payments, conversions,
transfers, fees, and adjustments. It is the most reliable signal for real-time balance
reconciliation. Filter by referenceType to distinguish the source operation.
Onboarding events (3)
| Event type | Trigger | Key payload fields |
|---|
onboarding.created | Orchestrated onboarding started via POST /onboardings | status, customerId |
onboarding.completed | All onboarding steps completed successfully | status |
onboarding.failed | Onboarding failed at one or more steps | status, failureReason |
Collection events (7)
| Event type | Trigger | Key payload fields |
|---|
collection.created | New collection created via POST /collections | status, rail, amount |
collection.requires_action | Collection needs user action | status, requiresActionReason |
collection.submitted | Collection submitted to ACH network | status |
collection.processing | Collection being processed | status |
collection.completed | Collection completed — funds received | status, amount |
collection.returned | Collection returned (e.g., insufficient funds) | status, achReturnCode |
collection.failed | Collection failed to process | status, failureReason |
collection.canceled | Collection canceled | status |
Filtering strategies
Subscribe to what you need
Rather than subscribing to all events and filtering in your application, subscribe only
to the event types you process. This reduces webhook traffic and simplifies your handler:
{
"eventTypes": [
"payment.completed",
"payment.failed",
"payment.returned",
"transaction.created"
]
}
Filter by resource type in your handler
If you subscribe to multiple event types, route them by data.resourceType or type:
def handle_event(event):
resource_type = event["data"]["resourceType"]
if resource_type == "payment":
handle_payment_event(event)
elif resource_type == "customer":
handle_customer_event(event)
elif resource_type == "transaction":
handle_transaction_event(event)
else:
log.info(f"Ignoring event type: {event['type']}")
Use the Events API for filtered queries
The GET /events endpoint supports filtering by event type, which is useful for
backfill and recovery scenarios:
curl "https://sandbox.api.openfx.com/v1/events?type=payment.completed&limit=50" \
-H "Authorization: Bearer sk_sandbox_your_api_key" \
-H "X-Signature: <computed-signature>" \
-H "X-Timestamp: 1740500000"
Next steps