Returning.AIDocs
v1

Guides / Broker Integrations

.md

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.

Webhook delivery and readback

A request can be accepted before the configured workflow and any user update finish.

  1. Capture the stable business event ID and original occurredAt value when registration, KYC, or another event is committed.

  2. POST to the provisioned handshake URL and read data.sessionToken. Never expose the workflow API key to the browser.

  3. Send the event

    Broker backend

    POST the workflow-specific payload with x-session-token. Keep the same event identity on an exact retry.

  4. Check workflow execution

    Returning.AI workflow owner

    Treat the main response as queue acceptance. Check the configured workflow history or Returning.AI-owned diagnostic next.

  5. Check saved state

    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.

  6. Refresh the portal

    Portal owner

    Refresh the widget only after readback confirms the same canonical user and the expected saved value.

Flow

  1. Receive a committed broker event. Keep its stable source identity and event time. A retry timestamp is not a business event timestamp.
  2. 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.
  3. Read data.sessionToken. The current service source uses a 300-second session-token TTL. Prefer the returnedexpiresIn when present and exchange again before expiry.
  4. Call the main webhook. Send the session token inx-session-token and send the payload required by that workflow.
workflow-webhook.js
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.

handshake-response.json
{
  "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.

user-registered.example.json
{
  "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 eventId stable 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 eventUse it forDo not infer
user.registeredRegistration committed in the broker system.That the user has completed KYC or received a reward.
user.kyc_completedA confirmed KYC state transition.That a saved field automatically creates a milestone.
deposit_candidateA deposit-shaped event awaiting the configured business definition.That every positive balance operation is a genuine first deposit.
first_trade_qualifiedA 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_aggregateAn approved canonical-user and window aggregate.That raw source rows and the aggregate should both earn credit.
reward_redemptionA 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 original occurredAt when 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.

SettingWhat it does
Webhook URLWhere 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 OnCompletion or failure (the default), Completion only or Failure only. When the job's final status doesn't match, nothing is sent.
Include CSV ReportAdds csvUrl, a link to the results file. On by default.
Include SummaryAdds summary, the row counts. On by default.
Include Error DetailsAdds 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.

bulk-callback-request.http
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:completed

A job that finished, with one failed row:

bulk-callback-completed.json
{
  "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:

bulk-callback-failed.json
{
  "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"
  }
}
FieldMeaning
eventusers.bulk-update.completed or users.bulk-update.failed.
jobIdThe job's _id, the same ID that List bulk update jobs returns. Use it with Get bulk update job status and details.
communityIdYour community ID.
statuscompleted or failed. completed can still include failed rows; failed means the job as a whole failed, for example because the file was rejected.
nameThe step's Log Name, or Data Workflow Bulk Update when it is empty.
workflowId, workflowNodeIdThe workflow and the step that queued the job.
completedAtWhen the job finished, ISO 8601 UTC. Completed jobs only.
csvUrlA 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.
summarytotal, processed, successful, failed and skipped row counts. Completed jobs only.
errors.messageWhat went wrong, such as 1 user failed, or why the file was rejected. Show it to people; don't branch on the text.
errors.failedBatchesNumbers, 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 2xx status 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.

PathUse whenSuccess meansNext check
Configured workflow webhookA 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 APIA 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 IngestBuffered 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.