> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leapfinancial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Partner Webhooks

> Signature, delivery, and payload reference for operationStatusHook (operation status updates and request-money-full outcomes)

# Partner webhooks (`operationStatusHook`)

RaaS can **POST** JSON events to the URLs configured on the tenant (`operationStatusHook`), using a shared **HMAC** so you can verify authenticity.

Both event kinds below use the **same URL list** and signing headers. They are **not** alternatives—you may receive **both** for the same `correlationId` at different times. Branch on payload shape:

| Event                            | Event origin                                                                      | What it tells the partner                                                                                                    | Worker                                                            |
| -------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| **Operation status update**      | Numi platform → RaaS `POST /hooks` (`OperationCreated` / `OperationUpdated`)      | The operation **changed state on the platform** (`Sent`, `Funding`, `Completed`, …) plus sender/recipient context            | `operationHookWorker` (`operationsHook` queue)                    |
| **`request_money_full_outcome`** | RaaS internal async job after `POST /request-money-full` returned HTTP `Accepted` | Whether RaaS **finished orchestrating** the request (CIP, contact, payment method, `requestMoneyV2`): `Accepted` or `Failed` | `requestMoneyFullWebhookWorker` (`requestMoneyFullWebhook` queue) |

| Event                            | How to detect in your handler                                                  |
| -------------------------------- | ------------------------------------------------------------------------------ |
| **Operation status update**      | No `eventType` field (see [Operation status update](#operation-status-update)) |
| **`request_money_full_outcome`** | `eventType === "request_money_full_outcome"`                                   |

## Inbound platform events vs outbound partner webhooks

RaaS handles **two different directions** of operation-related events:

| Direction    | Endpoint / trigger                     | Audience                         | Documentation                  |
| ------------ | -------------------------------------- | -------------------------------- | ------------------------------ |
| **Inbound**  | Numi → RaaS `POST /hooks`              | RaaS only (not your integration) | Internal: `docs/event_system/` |
| **Outbound** | RaaS → your `operationStatusHook` URLs | Partner integration              | This page                      |

After RaaS ingests `OperationCreated` or `OperationUpdated`, it persists the new status and may POST an **operation status update** to your webhook (`operationHookWorker`). That is separate from **`request_money_full_outcome`**, which is only about the async **request-money-full** API pipeline—not about every platform status change.

Typical timeline for `POST /request-money-full` on the same `correlationId`:

1. Your API call returns **200** with `status: "Accepted"` (work continues in background).
2. RaaS POSTs **`request_money_full_outcome`** once (`Accepted` or `Failed`) when orchestration finishes.
3. If orchestration succeeded, Numi creates/updates the operation and RaaS receives platform events → one or more **operation status updates** (no `eventType`) as the transfer progresses.

```mermaid theme={null}
sequenceDiagram
  participant Partner
  participant RaaS
  participant Plat as Numi platform

  Note over Partner,RaaS: A) request-money-full async API
  Partner->>RaaS: POST /request-money-full
  RaaS-->>Partner: 200 Accepted
  RaaS->>RaaS: requestMoneyFullWorker
  RaaS->>Partner: POST request_money_full_outcome (requestMoneyFullWebhookWorker)

  Note over Partner,Plat: B) platform operation lifecycle
  Plat->>RaaS: OperationCreated / OperationUpdated
  RaaS->>RaaS: operationStatusUpdateWorker then operationHookWorker
  RaaS->>Partner: POST OperationStatusHookPayload (no eventType)
```

<Note>
  **`operationHookWorker`** and **`requestMoneyFullWebhookWorker`** share `operationStatusHook` but answer different questions: platform lifecycle vs request-money-full orchestration outcome. Implement one handler that branches on `eventType`.
</Note>

## Tenant configuration

Configure on the tenant (admin API):

| Setting                                                    | Purpose                                                                                                                                     |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `operationStatusHook`                                      | One or more HTTPS URLs. RaaS POSTs to **all** of them in parallel.                                                                          |
| `sharedSecret`                                             | HMAC key for `x-raas-signature`. Required for signed delivery.                                                                              |
| `trusted`                                                  | Must be `true` for operation status webhooks. Non-trusted tenants are skipped (`TENANT_NOT_TRUSTED`).                                       |
| `rules.request.notifyWebHook` / `rules.send.notifyWebHook` | Per operation type (`request`, `send`). If `false`, RaaS does **not** POST for that type (may reassign tenant when `reasignTenant` is set). |

<Note>
  Operation status webhooks and `request_money_full_outcome` share `operationStatusHook`, but **misconfiguration is handled differently**: missing URL/secret for request-money-full **skips** silently (log only); missing URL/secret for operation status updates **fail the job** and Bull retries.
</Note>

## Request

* **Method:** `POST`
* **Content-Type:** `application/json`
* **Body:** Event JSON (shape depends on event kind; see below).
* **Timeout (RaaS → your URL):** 30 seconds per request.

## Headers sent by RaaS

| Header              | Description                                                                                                                                                                   |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-raas-signature`  | **Required when a body is sent.** Base64-encoded HMAC-SHA256 of the **raw** JSON body using the tenant `sharedSecret`. See [Signature verification](#signature-verification). |
| `x-raas-op-country` | Tenant country id (for example `MX`). May be empty if not set on the tenant model.                                                                                            |

## Signature verification

Verify `x-raas-signature` using the **exact raw request body bytes** (UTF-8 string as received) and the tenant `sharedSecret`.

### Algorithm

```text theme={null}
signature = Base64( HMAC-SHA256( rawRequestBody, sharedSecret ) )
```

* **rawRequestBody:** the raw HTTP body string **before** `JSON.parse` (must match what RaaS signed—same key order as `JSON.stringify` from the producer).
* **sharedSecret:** tenant shared secret.
* **HMAC-SHA256:** RFC 2104.
* **Base64:** standard encoding of the HMAC digest.

### Example (Node.js)

```js theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signatureHeader, sharedSecret) {
  const expected = crypto
    .createHmac('sha256', sharedSecret)
    .update(rawBody, 'utf8')
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader, 'base64'),
    Buffer.from(expected, 'base64')
  );
}
```

Use the raw body from your framework (e.g. Express `req.body` as a string only if you use a raw body middleware; otherwise read the raw stream before parsing).

## Delivery, retries, and multiple URLs

| Queue                     | Worker                          | Attempts | Initial delay | Backoff   | On delivery failure                                     |
| ------------------------- | ------------------------------- | -------- | ------------- | --------- | ------------------------------------------------------- |
| `operationsHook`          | `operationHookWorker`           | 3        | 500 ms        | fixed 3 s | Throws `ERROR_FORWARDING_EVENTS` if **every** URL fails |
| `requestMoneyFullWebhook` | `requestMoneyFullWebhookWorker` | 5        | 1 s           | fixed 5 s | Throws `WEBHOOK_DELIVERY_FAILED` if **any** URL fails   |

* **All** URLs in `operationStatusHook` are called in parallel (`Promise.allSettled`).
* Treat handlers as **idempotent** on `correlationId` (retries and duplicate platform events can produce repeated POSTs).
* Operation status updates are **skipped** (no POST) when the persisted status did not change (`NOT_FORWARD_BY_SAME_SATUS`).

OpenAPI types: `OperationStatusHookPayload`, `RequestMoneyFullOutcomePayload` (hidden schema routes on the Partner API spec).

***

## Operation status update

Sent when the Numi platform notifies RaaS of `OperationCreated` or `OperationUpdated` (`POST /hooks` → `OperationHookFlow` → child status update, then parent webhook).

Implemented by `operationHookWorker` in `src/queues/workers/OperationWorkers.ts`.

### Prerequisites

All of the following must be true before RaaS POSTs:

1. Tenant **`trusted`** is `true`.
2. At least one URL in **`operationStatusHook`** and a configured **`sharedSecret`**.
3. **`rules[operationType].notifyWebHook`** is `true` for the operation type (`request` or `send`).

If `notifyWebHook` is `false`, RaaS logs and returns without POST (no error to your URL).

### Payload shape (`OperationStatusHookPayload`)

| Field                   | Type   | Always present | Description                                                                                                   |
| ----------------------- | ------ | -------------- | ------------------------------------------------------------------------------------------------------------- |
| `correlationId`         | string | Yes            | RaaS operation id (partner correlation id when provided at creation).                                         |
| `status`                | string | Yes            | Platform status: `Sent`, `Funding`, `Funded`, `Completed`, `Failed`, `OnHold`, `InTransit`, `Cancelled`, etc. |
| `statusDetails`         | string | No             | Platform detail (e.g. `RequestSent`, `WaitingRecipientPaymentInfo`).                                          |
| `date`                  | string | Yes            | ISO 8601 when the payload was built.                                                                          |
| `firstName`             | string | Yes            | Sender first name (empty string if unavailable).                                                              |
| `lastName`              | string | Yes            | Sender last name.                                                                                             |
| `fundingMethod`         | string | Yes            | Source payment method display name.                                                                           |
| `recipientFirstName`    | string | Yes            | Recipient first name.                                                                                         |
| `recipientLastName`     | string | Yes            | Recipient last name.                                                                                          |
| `payoutMethod`          | string | Yes            | Destination payment method display name.                                                                      |
| `senderPhone`           | string | Yes            | Sender phone.                                                                                                 |
| `recipientPhone`        | string | Yes            | Recipient phone.                                                                                              |
| `senderEmail`           | string | Yes            | Sender email.                                                                                                 |
| `recipientEmail`        | string | Yes            | Recipient email.                                                                                              |
| `payoutReplacementLink` | string | No             | Link when RaaS generated a payout replacement URL.                                                            |

There is **no** `eventType` field. If you receive both event kinds on one endpoint, treat payloads **without** `eventType` as operation status updates.

### Example

```json theme={null}
{
  "correlationId": "d4290fcc-ad62-445d-9071-0f68df2d",
  "status": "Completed",
  "statusDetails": "RequestSent",
  "date": "2026-06-02T15:30:00.000Z",
  "firstName": "Juan",
  "lastName": "Perez",
  "fundingMethod": "account-1234",
  "recipientFirstName": "Maria",
  "recipientLastName": "Garcia",
  "payoutMethod": "MobileWallet",
  "senderPhone": "+5213310120067",
  "recipientPhone": "+13058884000",
  "senderEmail": "sender@example.com",
  "recipientEmail": "recipient@example.com",
  "payoutReplacementLink": "https://…"
}
```

### Flow

```mermaid theme={null}
sequenceDiagram
  participant Plat as Numi platform
  participant RaaS as RaaS POST /hooks
  participant Q as operationsHook queue
  participant You as Your webhook URL

  Plat->>RaaS: OperationCreated / OperationUpdated
  RaaS->>Q: Update status (child), then notify (parent)
  Q->>You: POST OperationStatusHookPayload + x-raas-signature
```

***

## Event: `request_money_full_outcome`

Sent when the **background** request-money-full flow completes: CIP, beneficiary contact, payment method registration, and platform `requestMoneyV2` (see `requestMoneyFullWorker` in `OperationWorkers.ts`).

### Prerequisites

The outcome webhook is **only sent** if the tenant has:

1. **At least one URL** in `operationStatusHook`, and
2. A configured **`sharedSecret`**.

If either is missing, RaaS **skips** sending the event (it logs a warning; no HTTP call is made). Configure both before relying on async notifications.

### Payload shape

| Field           | Type   | Always present                | Description                                                                                                                                                                            |
| --------------- | ------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventType`     | string | Yes                           | Always `"request_money_full_outcome"`.                                                                                                                                                 |
| `correlationId` | string | Yes                           | Same id the partner sent as `request.correlationId` on `POST /request-money-full`.                                                                                                     |
| `status`        | string | Yes                           | `"Accepted"` when the pipeline completed successfully, or `"Failed"` when it stopped with a business/platform error. **Casing matches the JSON exactly** (not `completed` / `failed`). |
| `date`          | string | Yes                           | ISO 8601 timestamp when the payload was built.                                                                                                                                         |
| `errorCode`     | string | Only if `status === "Failed"` | Stable machine code from the failing `NumiError` (e.g. `ERROR_CIP_NOT_COMPLETED`). Aligns with `RequestMoneyFullErrorCode` in the API types.                                           |
| `reason`        | string | Only if `status === "Failed"` | Human-readable message.                                                                                                                                                                |
| `phase`         | string | Only if `status === "Failed"` | High-level stage: `cip`, `contact`, `payment_method`, `request_money`, or `unknown`.                                                                                                   |

### Example — success

```json theme={null}
{
  "eventType": "request_money_full_outcome",
  "correlationId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "Accepted",
  "date": "2026-04-24T12:34:56.789Z"
}
```

### Example — failure

```json theme={null}
{
  "eventType": "request_money_full_outcome",
  "correlationId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "Failed",
  "date": "2026-04-24T12:35:10.000Z",
  "errorCode": "ERROR_CIP_NOT_COMPLETED",
  "reason": "…",
  "phase": "cip"
}
```

### Flow

```mermaid theme={null}
sequenceDiagram
  participant Partner
  participant RaaS as RaaS API + workers
  participant Q as requestMoneyFullWebhook queue
  participant You as Your webhook URL

  Partner->>RaaS: POST /request-money-full
  RaaS-->>Partner: 200 Accepted
  RaaS->>RaaS: requestMoneyFullWorker (CIP, contact, PM, requestMoneyV2)
  RaaS->>Q: Enqueue outcome job
  Q->>You: POST RequestMoneyFullOutcomePayload + x-raas-signature
```

***

## Synchronous errors vs webhook (request-money-full)

`POST /request-money-full` can return **4xx/5xx** immediately (validation, tenant, duplicate `correlationId`, etc.). Those responses use a JSON body with `reason` and `code` suitable for the HTTP layer.

Failures that happen **after** the API returned **200** with `status: "Accepted"` (e.g. CIP not completed, card errors) are reported only via **`request_money_full_outcome`** with `status: "Failed"`, not by changing the original HTTP response.

## Endpoint error codes (HTTP)

Error responses from `POST /user/operations/request-money-full` use this shape:

```json theme={null}
{ "reason": "string", "code": "string" }
```

Use `code` for branching in your integration; values align with `RequestMoneyFullErrorCode` in the OpenAPI / TypeScript models.

## Source

* Operation status webhook: `src/queues/workers/OperationWorkers.ts` (`operationHookWorker`, `operationStatusUpdateWorker`), `src/queues/flows/OperationHookFlow.ts`, `src/controllers/partner/eventHookController.ts`.
* Request-money-full outcome: `sendRequestMoneyFullOutcomeWebhook`, `requestMoneyFullWebhookWorker`, `requestMoneyFullWorker` in the same workers module.
* Queue options: `src/config/queues.ts` (`QUEUE_NAME_OPERATION_HOOK`, `QUEUE_NAME_REQUEST_MONEY_FULL_WEBHOOK`).
* Types: `src/model/operation.ts` (`OperationStatusHookPayload`, `RequestMoneyFullOutcomePayload`, …).
* Repository reference: `docs/partner_webhooks.md`.
