Workflow webhooks
Use a workflow webhook for a specific configured lifecycle trigger, such as registration, KYC completion, a deposit candidate, account status, or a reward redemption. The webhook delivers an event to a workflow. It does not by itself prove that a user field, balance, or widget has changed.
Use the configured auth mode
When token exchange is enabled for the trigger, call the handshake from your server and put the returned short-lived session token on the main request. The workflow API key stays server-side. Direct-key mode is a separate trigger configuration, not a rule based on how old a workflow is.
Webhook delivery and readback
A request can be accepted before the configured workflow and any user update finish.
- Broker backend
Capture the stable business event ID and original occurredAt value when registration, KYC, or another event is committed.
- Broker backend
POST to the provisioned handshake URL and read data.sessionToken. Never expose the workflow API key to the browser.
- Broker backend
POST the workflow-specific payload with x-session-token. Keep the same event identity on an exact retry.
- Returning.AI workflow owner
Treat the main response as queue acceptance. Check the configured workflow history or Returning.AI-owned diagnostic next.
- Returning.AI data owner
If the workflow queues a user update, inspect its terminal and per-row result, then perform the operation-specific authoritative readback.
- Portal owner
Refresh the widget only after readback confirms the same canonical user and the expected saved value.
Flow
- Receive a committed broker event. Keep its stable source identity and event time. A retry timestamp is not a business event timestamp.
- Call the provisioned handshake URL. Send the workflow API key in the header required by that trigger. The documented route shape ends in
/handshake; use the complete URL supplied for the environment rather than guessing a host or trigger ID. - Read
data.sessionToken. The current service source uses a 300-second session-token TTL. Prefer the returnedexpiresInwhen present and exchange again before expiry. - Call the main webhook. Send the session token in
x-session-tokenand send the payload required by that workflow.
async function sendRegistration(user) {
const handshakeUrl = process.env.RAI_WORKFLOW_HANDSHAKE_URL;
const webhookUrl = process.env.RAI_WORKFLOW_WEBHOOK_URL;
const workflowApiKey = process.env.RAI_WORKFLOW_API_KEY;
if (!handshakeUrl || !webhookUrl || !workflowApiKey) {
throw new Error('Missing provisioned workflow settings');
}
const handshake = await fetch(handshakeUrl, {
method: 'POST',
headers: {
Authorization: workflowApiKey,
'Content-Type': 'application/json',
},
});
if (!handshake.ok) {
throw new Error(`Handshake failed: ${handshake.status}`);
}
const handshakeBody = await handshake.json();
const sessionToken = handshakeBody.data?.sessionToken;
if (!sessionToken) {
throw new Error('Handshake did not return data.sessionToken');
}
const event = {
eventType: 'user.registered',
eventId: user.registrationEventId,
customerId: user.customerId,
occurredAt: user.registeredAt,
};
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'x-session-token': sessionToken,
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
});
if (!response.ok) {
throw new Error(`Webhook request failed: ${response.status}`);
}
// Record the HTTP status. Check workflow history and saved state separately.
}The environment values in this example are named placeholders. The Returning.AI setup owner must provide the exact handshake URL, main webhook URL, trigger reference, required permission, and selected auth mode for the environment.
Response shape
The handshake response places the session token underdata.sessionToken. A current source-backed TTL is 300 seconds, but the returned expiry and the provisioned trigger configuration are the values to use at runtime.
{
"data": {
"sessionToken": "<SHORT_LIVED_SESSION_TOKEN>",
"expiresIn": 300
}
}Main response boundary
The generic main webhook path returns HTTP 200 after it queues the execution. Do not assume a universal JSON acknowledgement field, and do not treat HTTP 200 as workflow completion or a balance update. Use workflow history or a Returning.AI-owned diagnostic, then verify the saved value.
Payload guidance
Webhook payloads belong to the configured workflow. Lifecycle examples commonly use camelCase names, while Event Ingest trading rows commonly use snake_case. Do not copy one contract into the other without checking the trigger configuration.
{
"eventType": "user.registered",
"eventId": "registration-CUSTOMER_1001-2026-09-07",
"customerId": "CUSTOMER_1001",
"occurredAt": "2026-09-07T09:00:00.000Z",
"tradingAccounts": [
{
"platform": "mt5",
"server": "broker-live-1",
"accountLogin": "ACCOUNT_2001"
}
]
}- Use a stable broker customer identifier that matches the enrollment and widget identity. Trading account keys remain separate from the canonical customer.
- Keep
eventIdstable across an exact retry. It is a business identity in this example, not a generic promise that the webhook controller deduplicates it. - Preserve the source event's
occurredAt. Do not callnew Date()again while retrying an old event. - Send only the fields required by the configured workflow. Never send access keys, trading passwords, session tokens, or other private credentials in the event body.
Keep event types separate
Separate events when their eligibility, timing, or reward rule differs. Names below are examples for workflow configuration, not a universal endpoint schema.
| Example event | Use it for | Do not infer |
|---|---|---|
user.registered | Registration committed in the broker system. | That the user has completed KYC or received a reward. |
user.kyc_completed | A confirmed KYC state transition. | That a saved field automatically creates a milestone. |
deposit_candidate | A deposit-shaped event awaiting the configured business definition. | That every positive balance operation is a genuine first deposit. |
first_trade_qualified | A result after complete accessible history has been checked. | That the first event observed by a new extractor is the trader's first ever trade. |
trade_volume_aggregate | An approved canonical-user and window aggregate. | That raw source rows and the aggregate should both earn credit. |
reward_redemption | A separate redemption action and its own validation. | That a queued event has already changed the balance. |
Retries and business dedupe
The generic webhook controller creates an execution identity from the trigger, current request time, and a random value. A caller-suppliedeventId therefore does not create universal dedupe by itself. The workflow or downstream mutation needs an approved stable business key, or the trigger owner must provide another supported contract.
- Once reconciliation and the configured replay policy permit a retry, keep the same
eventId, customer identity, source account, and originaloccurredAtwhen the payload is unchanged. - If the result is unknown, reconcile the request and workflow state before sending again. A timeout is not proof that the first execution failed.
- If a payload is corrected, use the configured correction or compensation path. A new event ID can cause a second additive side effect.
- If referral follow-up fails after enrollment, repair that relationship separately. Do not retry user creation just because a later referral step failed.
Bulk update results callback
When a workflow runs a Users Bulk Update step, the step only queues a bulk update job, and the job can finish after the workflow has ended. Instead of polling Get bulk update job status, the step can send the job's final result to an HTTPS endpoint you run.
Set it on the step in the dashboard: Settings > Integration > Data Workflows, open the workflow, select the Users Bulk Update step and switch on Callback URL. A community owner or admin can change it. Files uploaded with Bulk update users from CSV or the premium currency upload never send one; poll those jobs instead.
| Setting | What it does |
|---|---|
| Webhook URL | Where the result is sent. It must be a public https:// (or http://) address of up to 4,096 characters, with no username or password in it. Saving the step fails for a malformed address, one with credentials, localhost, or a private or reserved IP address. A host name that resolves to a private address is refused when the callback is sent. |
| Send Callback On | Completion or failure (the default), Completion only or Failure only. When the job's final status doesn't match, nothing is sent. |
| Include CSV Report | Adds csvUrl, a link to the results file. On by default. |
| Include Summary | Adds summary, the row counts. On by default. |
| Include Error Details | Adds errors when the job has any. On by default. |
Each job keeps the settings the step had when it was queued. Changing the step later, or switching the callback off, doesn't change jobs already in the queue.
The request
Once per job, after it ends completed or failed, Returning.AI sends a POST with a JSON body, usually within a minute of the job finishing. Nothing is sent while the job is queued or running. The request carries these headers and no credentials or signature, so use a hard-to-guess path on your endpoint and treat the body as a notice: confirm the job with Get bulk update job status and your API key before you act on it.
POST /your/bulk-results/path HTTP/1.1
Content-Type: application/json
Accept: application/json
User-Agent: returning-ai-bulk-update-webhook/1.0
Idempotency-Key: bulk-update:66f000000000000000000701:completedA job that finished, with one failed row:
{
"event": "users.bulk-update.completed",
"jobId": "66f000000000000000000701",
"communityId": "66f000000000000000000001",
"status": "completed",
"name": "Nightly coins top-up",
"workflowId": "66f000000000000000000002",
"workflowNodeId": "66f000000000000000000003",
"completedAt": "2026-09-30T02:00:05.000Z",
"csvUrl": "<resultsCsvUrl>",
"summary": {
"total": 2,
"processed": 2,
"successful": 1,
"failed": 1,
"skipped": 0
},
"errors": {
"message": "1 user failed"
}
}A job that failed because the file was rejected:
{
"event": "users.bulk-update.failed",
"jobId": "66f000000000000000000702",
"communityId": "66f000000000000000000001",
"status": "failed",
"name": "Nightly coins top-up",
"workflowId": "66f000000000000000000002",
"workflowNodeId": "66f000000000000000000003",
"errors": {
"message": "Invalid headers: Coins"
}
}| Field | Meaning |
|---|---|
event | users.bulk-update.completed or users.bulk-update.failed. |
jobId | The job's _id, the same ID that List bulk update jobs returns. Use it with Get bulk update job status and details. |
communityId | Your community ID. |
status | completed or failed. completed can still include failed rows; failed means the job as a whole failed, for example because the file was rejected. |
name | The step's Log Name, or Data Workflow Bulk Update when it is empty. |
workflowId, workflowNodeId | The workflow and the step that queued the job. |
completedAt | When the job finished, ISO 8601 UTC. Completed jobs only. |
csvUrl | A link to the results file: one line per uploaded row, with the row's cells under lower-case column names and an errorMsg column that holds the row's errors as JSON and is empty when the row succeeded. Completed jobs only. |
summary | total, processed, successful, failed and skipped row counts. Completed jobs only. |
errors.message | What went wrong, such as 1 user failed, or why the file was rejected. Show it to people; don't branch on the text. |
errors.failedBatches | Numbers, as strings, of the batches of rows that could not be processed. Present only when there are any. |
A field that doesn't apply is left out, not sent empty. For each failed row's error code, read Get bulk update job details with the jobId. The results file holds your traders' emails, so treat the csvUrl link as confidential and keep it out of logs and shared channels. Download the file when the callback arrives.
Responding, retries and duplicates
- Return any
2xxstatus within 15 seconds, then do the work. Keep the response body under 64 KB. - A timeout, a network error, a status outside
2xx, a redirect or a larger body counts as a failed delivery. Redirects are not followed. - A failed delivery is retried with the same body and headers, about 1, 2, 4 and 8 minutes apart, for at most 5 attempts. After that the callback stops for good; check the job with Get bulk update job status instead. Delivery problems never change the job or run it again.
- The same callback can arrive more than once. Store each
Idempotency-Key(bulk-update:<jobId>:<status>) and ignore repeats.
Specific API operation versus workflow
Choose by the configured operation, not by a claim that one family always finishes synchronously. An API response confirms that operation's response only; a workflow response confirms queue acceptance. Ask the platform owner for the exact host, path, credential permission, accepted identifier, and readback operation before sending an API request.
| Path | Use when | Success means | Next check |
|---|---|---|---|
| Configured workflow webhook | A lifecycle event has a named trigger and workflow. | The main request was accepted for execution. | Workflow history, queued user update if any, then saved-state readback. |
| Specific supported user API | A platform owner has supplied one exact read or write operation. | That operation returned its documented result. | Follow the operation's documented readback when side effects are asynchronous. |
| SQS or Event Ingest | Buffered trading facts, aggregates, or backfills need replayable delivery. | The transport accepted the candidate message. | Event Ingest status, workflow, saved state, and widget refresh. |
Direct-key mode
Some configured triggers can accept a workflow API key directly on the main webhook when token exchange is disabled. This is a configuration choice, not a blanket legacy rule. Before using it, have the Returning.AI owner confirm that the trigger has token exchange disabled, the exact header and URL, the key permission, and the retry/readback behavior. Never put that key in browser code.
Next step
After a registration event is accepted, finish enrollment and identity readback before opening the widget quickstart. For trading facts, continue to the trading volume and rewards guide. For buffered delivery, use the SQS data feed guide.