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) |
request_money_full_outcome | eventType === "request_money_full_outcome" |
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:
- Your API call returns 200 with
status: "Accepted" (work continues in background).
- RaaS POSTs
request_money_full_outcome once (Accepted or Failed) when orchestration finishes.
- 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.
operationHookWorker and requestMoneyFullWebhookWorker share operationStatusHook but answer different questions: platform lifecycle vs request-money-full orchestration outcome. Implement one handler that branches on eventType.
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). |
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.
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.
| 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. |
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
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)
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:
- Tenant
trusted is true.
- At least one URL in
operationStatusHook and a configured sharedSecret.
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
{
"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
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:
- At least one URL in
operationStatusHook, and
- 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
{
"eventType": "request_money_full_outcome",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"status": "Accepted",
"date": "2026-04-24T12:34:56.789Z"
}
Example — failure
{
"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
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:
{ "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.