From 3335ef3c24b6ca6203df6db814a0bab70e8ba0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Alvial?= Date: Wed, 6 May 2026 16:15:02 -0300 Subject: [PATCH 1/2] feat(webhook): publish OPERATION_COMPLETED and OPERATION_FAILED Add webhook events for terminal operation transitions, sharing the OperationEvent payload shape with OPERATION_REQUESTED. Use currentState.status to distinguish outcomes; reason is populated on FAILED. Intermediate statuses (PROCESSING, ON_HOLD, ACTION_REQUIRED) remain poll-only via GET /api/operations/{operationId}. Journey pages and webhooks overview updated to push subscribing to terminal events first and only fall back to polling for intermediate statuses. --- .../events/operation/operation-completed.mdx | 5 + .../events/operation/operation-failed.mdx | 5 + apis/fx-webhook/openapi.yml | 113 +++++++++++++++++- docs.json | 4 +- journeys/deposit.mdx | 2 +- journeys/swap.mdx | 2 +- journeys/transfer.mdx | 2 +- journeys/withdrawal.mdx | 2 +- webhooks/overview.mdx | 16 ++- 9 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 api-reference/fx-webhook/events/operation/operation-completed.mdx create mode 100644 api-reference/fx-webhook/events/operation/operation-failed.mdx diff --git a/api-reference/fx-webhook/events/operation/operation-completed.mdx b/api-reference/fx-webhook/events/operation/operation-completed.mdx new file mode 100644 index 0000000..3c3998e --- /dev/null +++ b/api-reference/fx-webhook/events/operation/operation-completed.mdx @@ -0,0 +1,5 @@ +--- +title: "OPERATION_COMPLETED" +description: "Fires when a payment operation reaches its terminal success state." +openapi: "apis/fx-webhook/openapi.yml webhook OPERATION_COMPLETED" +--- diff --git a/api-reference/fx-webhook/events/operation/operation-failed.mdx b/api-reference/fx-webhook/events/operation/operation-failed.mdx new file mode 100644 index 0000000..a4cd99f --- /dev/null +++ b/api-reference/fx-webhook/events/operation/operation-failed.mdx @@ -0,0 +1,5 @@ +--- +title: "OPERATION_FAILED" +description: "Fires when a payment operation reaches its terminal failure state." +openapi: "apis/fx-webhook/openapi.yml webhook OPERATION_FAILED" +--- diff --git a/apis/fx-webhook/openapi.yml b/apis/fx-webhook/openapi.yml index 97fad92..2b12b5f 100644 --- a/apis/fx-webhook/openapi.yml +++ b/apis/fx-webhook/openapi.yml @@ -145,6 +145,8 @@ components: - BENEFICIARY_PAYMENT_INSTRUCTION_APPROVED - BENEFICIARY_PAYMENT_INSTRUCTION_REJECTED - OPERATION_REQUESTED + - OPERATION_COMPLETED + - OPERATION_FAILED example: OPERATION_REQUESTED ExecutionLogStatus: type: string @@ -1079,11 +1081,13 @@ components: OperationEvent: type: object description: > - Payload delivered when an operation is first created - (`OPERATION_REQUESTED`). Subsequent state transitions - (`PROCESSING`, `COMPLETED`, `FAILED`, etc.) are not published as - webhook events — poll `GET /api/operations/{operationId}` to - track lifecycle progress. + Payload delivered for operation lifecycle events + (`OPERATION_REQUESTED`, `OPERATION_COMPLETED`, `OPERATION_FAILED`). + Inspect the `X-Event-Type` header — or `currentState.status` on + the payload — to tell which transition fired. Intermediate + statuses (`PROCESSING`, `ON_HOLD`, `ACTION_REQUIRED`) are not + published as webhooks; fetch + `GET /api/operations/{operationId}` if you need them. properties: id: type: string @@ -1091,6 +1095,8 @@ components: readOnly: true customerId: type: string + format: uuid + example: "9c7b6a2e-1d3f-4a5b-8c9d-0e1f2a3b4c5d" account: $ref: "#/components/schemas/AccountReferenceEvent" sourceAmount: @@ -1123,6 +1129,9 @@ components: value: "Amazon" - key: "compliance-tier" value: "high" + currentState: + $ref: "#/components/schemas/OperationState" + description: Status the operation is in at the time the event fires. atTime: type: string format: date-time @@ -1136,7 +1145,57 @@ components: - intent - fees - transactions + - currentState - atTime + OperationStatus: + type: string + description: Lifecycle status of an operation. + enum: + - REQUESTED + - PROCESSING + - ON_HOLD + - ACTION_REQUIRED + - COMPLETED + - FAILED + example: COMPLETED + Reason: + type: object + description: Machine- and human-readable explanation. Populated on an `OperationState` when the status transition requires justification (e.g., `FAILED`). + properties: + code: + type: string + description: Machine-readable code. + example: "INSUFFICIENT_BALANCE" + message: + type: string + description: Human-readable description. + example: "Insufficient balance for the requested operation" + details: + type: object + additionalProperties: true + description: Free-form context relevant to the reason. + required: + - code + - message + - details + OperationState: + type: object + description: A single entry in an operation's state history. Carries a status, optional reason, and the time the state was entered. + properties: + status: + $ref: "#/components/schemas/OperationStatus" + reason: + allOf: + - $ref: "#/components/schemas/Reason" + nullable: true + description: Justification for entering this status. Present for `FAILED`; `null` for `REQUESTED` and `COMPLETED`. + createdAt: + type: string + format: date-time + description: Time the operation entered this status. + required: + - status + - createdAt paths: /api/subscriptions: @@ -1544,7 +1603,49 @@ webhooks: OPERATION_REQUESTED: post: summary: Operation requested - description: Fires when a payment operation (deposit, withdrawal, swap) is created. + description: > + Fires when a payment operation (deposit, withdrawal, transfer, + swap) is created. Compliance review and rail processing happen + after this event — wait for `OPERATION_COMPLETED` or + `OPERATION_FAILED` to know the terminal outcome. + tags: + - Operation + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/OperationEvent" + responses: + "200": + $ref: "#/components/responses/WebhookAck" + OPERATION_COMPLETED: + post: + summary: Operation completed + description: > + Fires when a payment operation reaches its terminal success + state. The payload carries `currentState.status: COMPLETED`, + with `transactions` populated by the rail-confirmed legs that + settled the operation. + tags: + - Operation + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/OperationEvent" + responses: + "200": + $ref: "#/components/responses/WebhookAck" + OPERATION_FAILED: + post: + summary: Operation failed + description: > + Fires when a payment operation reaches its terminal failure + state. The payload carries `currentState.status: FAILED` and + `currentState.reason` describing why. No further events fire + for this operation. tags: - Operation requestBody: diff --git a/docs.json b/docs.json index 3298212..6159423 100644 --- a/docs.json +++ b/docs.json @@ -219,7 +219,9 @@ "group": "Operation", "icon": "arrows-rotate", "pages": [ - "api-reference/fx-webhook/events/operation/operation-requested" + "api-reference/fx-webhook/events/operation/operation-requested", + "api-reference/fx-webhook/events/operation/operation-completed", + "api-reference/fx-webhook/events/operation/operation-failed" ] }, { diff --git a/journeys/deposit.mdx b/journeys/deposit.mdx index 50f8fd5..02a3acc 100644 --- a/journeys/deposit.mdx +++ b/journeys/deposit.mdx @@ -61,7 +61,7 @@ Deposits credit an account when funds arrive via the chosen funding rail. The cu - Poll `GET /api/operations/{operationId}` until the inbound payment is reconciled and the operation transitions to `COMPLETED`. Lifecycle transitions are not published as webhook events; the `OPERATION_REQUESTED` webhook only fires on creation. + Subscribe to `OPERATION_COMPLETED` to receive the deposit confirmation once the inbound payment is reconciled, or `OPERATION_FAILED` if reconciliation fails. Both deliver the same payload as `OPERATION_REQUESTED`, with `currentState.status` set to the new status (and `currentState.reason` populated on failure). The intermediate `PROCESSING` status is not published as a webhook; poll `GET /api/operations/{operationId}` if you need to surface it in your UI. ```bash curl --request GET \ diff --git a/journeys/swap.mdx b/journeys/swap.mdx index 7a5716c..210f684 100644 --- a/journeys/swap.mdx +++ b/journeys/swap.mdx @@ -54,7 +54,7 @@ Swaps convert funds between assets within the same account — for example, BRL - Poll `GET /api/operations/{operationId}` to follow the operation through `PROCESSING` to `COMPLETED` (or `FAILED`). Lifecycle transitions are not published as webhook events; the `OPERATION_REQUESTED` webhook only fires on creation. + Subscribe to `OPERATION_COMPLETED` and `OPERATION_FAILED` to receive the terminal outcome — both deliver the same payload as `OPERATION_REQUESTED`, with `currentState.status` set to the new status (and `currentState.reason` populated on failure). The intermediate `PROCESSING` status is not published as a webhook; poll `GET /api/operations/{operationId}` if you need to surface it in your UI. ```bash curl --request GET \ diff --git a/journeys/transfer.mdx b/journeys/transfer.mdx index c7bca3f..0739a1a 100644 --- a/journeys/transfer.mdx +++ b/journeys/transfer.mdx @@ -38,7 +38,7 @@ Transfers move funds between two accounts owned by the same customer. They are s - Poll `GET /api/operations/{operationId}` to follow the operation through `PROCESSING` to `COMPLETED` (or `FAILED`). Lifecycle transitions are not published as webhook events; the `OPERATION_REQUESTED` webhook only fires on creation. + Subscribe to `OPERATION_COMPLETED` and `OPERATION_FAILED` to receive the terminal outcome — both deliver the same payload as `OPERATION_REQUESTED`, with `currentState.status` set to the new status (and `currentState.reason` populated on failure). The intermediate `PROCESSING` status is not published as a webhook; poll `GET /api/operations/{operationId}` if you need to surface it in your UI. ```bash curl --request GET \ diff --git a/journeys/withdrawal.mdx b/journeys/withdrawal.mdx index 67cea85..5645eb5 100644 --- a/journeys/withdrawal.mdx +++ b/journeys/withdrawal.mdx @@ -95,7 +95,7 @@ Withdrawals move funds out of an account to an external destination — a bank a - Poll `GET /api/operations/{operationId}` to follow the operation through `PROCESSING` to `COMPLETED` (or `FAILED`). Compliance review can pause the operation in `ON_HOLD` or `ACTION_REQUIRED` until additional checks clear. Lifecycle transitions are not published as webhook events; the `OPERATION_REQUESTED` webhook only fires on creation. + Subscribe to `OPERATION_COMPLETED` and `OPERATION_FAILED` to receive the terminal outcome — both deliver the same payload as `OPERATION_REQUESTED`, with `currentState.status` set to the new status (and `currentState.reason` populated on failure). Intermediate statuses (`PROCESSING`, `ON_HOLD`, `ACTION_REQUIRED`) are not published as webhooks; poll `GET /api/operations/{operationId}` if you need to surface them in your UI. ```bash curl --request GET \ diff --git a/webhooks/overview.mdx b/webhooks/overview.mdx index 4d7f626..4e6f239 100644 --- a/webhooks/overview.mdx +++ b/webhooks/overview.mdx @@ -66,15 +66,19 @@ Browse the full event catalog by resource: | Resource | Events | Description | | --- | --- | --- | | [Account](/api-reference/fx-webhook/events/account/asset-activated) | 1 | Asset onboarding completion | -| [Operation](/api-reference/fx-webhook/events/operation/operation-requested) | 1 | Operation creation | +| [Operation](/api-reference/fx-webhook/events/operation/operation-requested) | 3 | Operation creation, completion, and failure | | [Beneficiary](/api-reference/fx-webhook/events/beneficiary/beneficiary-payment-instruction-created) | 3 | Payment instruction creation, approval, and rejection | You can also fetch the catalog programmatically: [`GET /references/ResourceName/all`](/api-reference/fx-webhook/subscriptions/list-resource-references). - Webhook events fire on **creation and review outcomes only**. Operation - lifecycle transitions (`PROCESSING`, `COMPLETED`, `FAILED`, `ON_HOLD`, - `ACTION_REQUIRED`) and account state changes are **not** published as - webhooks — poll `GET /api/operations/{operationId}` or - `GET /api/accounts/{accountId}` to track them. + Operations publish webhooks on creation (`OPERATION_REQUESTED`) and + on terminal outcomes (`OPERATION_COMPLETED`, `OPERATION_FAILED`). + Intermediate statuses (`PROCESSING`, `ON_HOLD`, `ACTION_REQUIRED`) + are not published — poll `GET /api/operations/{operationId}` if + you need them. + + Accounts publish only `ACCOUNT_ASSET_ACTIVATED`. Other account state + changes are not published — poll `GET /api/accounts/{accountId}` if + you need them. From c07301f6cbb902b7138da11015da84577559fe99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Alvial?= Date: Wed, 6 May 2026 16:15:15 -0300 Subject: [PATCH 2/2] fix(webhook): correct event name to ASSET_ACTIVATED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec exposed the account event as ACCOUNT_ASSET_ACTIVATED, but the live producer publishes it as ASSET_ACTIVATED — fx-account Constants.kt defines const ASSET_ACTIVATED = "ASSET_ACTIVATED" and emits it via that exact string. The ACCOUNT_ prefix was never on the wire. Renames the EventType enum entry, the webhooks path key, the page title, and the webhooks overview reference. The MDX filename and URL slug stay as-is (already in URL-friendly short form). --- api-reference/fx-webhook/events/account/asset-activated.mdx | 4 ++-- apis/fx-webhook/openapi.yml | 4 ++-- webhooks/overview.mdx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api-reference/fx-webhook/events/account/asset-activated.mdx b/api-reference/fx-webhook/events/account/asset-activated.mdx index 6687546..ee54369 100644 --- a/api-reference/fx-webhook/events/account/asset-activated.mdx +++ b/api-reference/fx-webhook/events/account/asset-activated.mdx @@ -1,5 +1,5 @@ --- -title: "ACCOUNT_ASSET_ACTIVATED" +title: "ASSET_ACTIVATED" description: "Fires when an account asset finishes onboarding and funding instructions are available." -openapi: "apis/fx-webhook/openapi.yml webhook ACCOUNT_ASSET_ACTIVATED" +openapi: "apis/fx-webhook/openapi.yml webhook ASSET_ACTIVATED" --- diff --git a/apis/fx-webhook/openapi.yml b/apis/fx-webhook/openapi.yml index 2b12b5f..04a7df9 100644 --- a/apis/fx-webhook/openapi.yml +++ b/apis/fx-webhook/openapi.yml @@ -140,7 +140,7 @@ components: type: string description: The specific event type within a resource, sent as the `X-Event-Type` header on each delivery. enum: - - ACCOUNT_ASSET_ACTIVATED + - ASSET_ACTIVATED - BENEFICIARY_PAYMENT_INSTRUCTION_CREATED - BENEFICIARY_PAYMENT_INSTRUCTION_APPROVED - BENEFICIARY_PAYMENT_INSTRUCTION_REJECTED @@ -1525,7 +1525,7 @@ paths: $ref: "#/components/schemas/ResourceReference" webhooks: - ACCOUNT_ASSET_ACTIVATED: + ASSET_ACTIVATED: post: summary: Account asset activated description: Fires when an account asset finishes onboarding and funding instructions are available. diff --git a/webhooks/overview.mdx b/webhooks/overview.mdx index 4e6f239..c53eaa2 100644 --- a/webhooks/overview.mdx +++ b/webhooks/overview.mdx @@ -78,7 +78,7 @@ You can also fetch the catalog programmatically: [`GET /references/ResourceName/ are not published — poll `GET /api/operations/{operationId}` if you need them. - Accounts publish only `ACCOUNT_ASSET_ACTIVATED`. Other account state + Accounts publish only `ASSET_ACTIVATED`. Other account state changes are not published — poll `GET /api/accounts/{accountId}` if you need them.