Reversible rewards
A reversible reward can be cancelled later: a deleted or voided trade gets its XP and coins back, exactly once, without a ledger rewrite. The mechanism is a bulk update node in reversible mode, where each row is one business event and a cancellation row names the event it reverses.
The API builds and tests that workflow. The cancellation decision stays with your system. How trade facts are collected and aggregated before they reach the workflow is covered in Trading volume and rewards.
How it works
Every row in a reversible bulk update is one business event:
| Row | eventKey | reversesEventKey | Amount columns | _op columns |
|---|---|---|---|---|
| Original | New and unique | blank | a non-negative amount | add |
| Cancellation | New and unique | the original's eventKey | blank | blank |
The platform stores the effect it actually applied for each original, including any cap, and a cancellation deducts exactly that. Two layers keep it once-only:
- the bulk worker skips a repeated original and a second cancellation;
- event ingest skips a repeated batch id and a repeated record id before the workflow even runs.
Scope is the community, the workflow and the eventKey. An original and its cancellation must pass through the same workflow.
1. Create the workflow with an ingest trigger
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--data '{"name":"Trade rewards (reversible)","trigger":{"type":"event-ingest","name":"Trade events","options":{"batchIdPath":"metadata.batch_id","recordIdPaths":["eventKey"]}}}'
The record id path is eventKey, so a replayed event never reaches the workflow.
2. Add the bulk step in reversible mode
The revertible node needs a CSV the workflow builds, one row per event, never one aggregated row per member: an aggregated row cannot be reversed trade by trade.
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/nodes" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c" \
--data '{"type":"users-bulk-update","name":"Apply or reverse reward","position":{"after":"trigger"},"options":{"payloadFormat":"csv","processingMode":"reversible","identifier":"email","logName":["Trade rewards",".batchId"],"csv":["{{65f1a2b3c4d5e6f708192a41.csv}}"],"callback":{"enabled":false}}}'
processingMode: reversible is the switch. identifier is the community's active identifier field, for example email, or a custom identifier when that field is the lookup key.
Reversible rows accept only these headers, case-insensitive: the identifier, eventKey, reversesEventKey, and optionally eventType, account_id, trade_id, tradeStatus, occurredAt, plus Total XP, Total XP_op, Total Coins, Total Coins_op. Any other header fails the whole job. Custom user fields cannot be written by a reversible node, so a second standard-mode step is needed for those.
3. Build the rows
The CSV is built before the node, and the API builds that builder like any other step: a Code step with its rows bound in options.inputs and its source in options.sourceCode.code.
- The Code step emits one CSV row per event, with a stable
eventKeyderived from the source event,reversesEventKeyfor a cancellation, and the amount columns. - When
account_idortrade_idis present, the original and its cancellation must carry the same values, derived from the same source fields. - A router skips the bulk step when the builder produced no rows.
Create it with POST /{workflowId}/nodes and read the workflow back to confirm what was stored:
curl --request GET \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b" \
--header "Authorization: Bearer <API_KEY>"
A workflow that contains a Code step deploys normally. The one thing to plan for is review: a dry run does not execute source, so the builder shows its inputs and its source but no output, and the bulk step after it cannot run in that dry run. Live-test the Code step on its own with an eventKey you chose, read the output, and only then dry-run the rest. See Code steps.
4. Test before it pays
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/test" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--data '{"mode":"dry-run","triggerPayload":{"metadata":{"batch_id":"batch-0001"},"data":[{"email":"trader@example.com","eventKey":"T-1:award","eventType":"trade","amount":"100"}]}}'
Readback: the bulk step is simulated, and wouldDo carries rowCount, a small sample, the payload format, the processing mode and the identifier. Nothing is uploaded and no job is created.
A Code step in the workflow makes that dry run a partial walk: the builder is simulated with no output, and every step after it is failed with UNRESOLVED_SIMULATED_REFERENCE. Live-test the builder alone first, then read this trace as the evidence for the bulk step and the router.
5. Prove one award and one reversal
- Send a batch with one original event.
- Read the run, then the bulk job's status and per-row detail. The row should read
appliedwith the expected change. - Read the member's field value.
- Send a second batch with a cancellation row whose
reversesEventKeyis the original'seventKey. - Read the job detail again: the row should read
appliedwith a negative change equal to what was applied. - Send the same cancellation again: the row should read
skipped, and the member's value must not move.
An original resent after it was cancelled reads skipped, not re-awarded. A cancellation whose original was never applied fails as unresolved and changes nothing.
6. Failure handling
| What you see | What it means | What to do |
|---|---|---|
| Job failed, every row unreachable | A header the reversible mode does not accept | Use only the documented headers. |
Row unresolved | The cancellation arrived before its original completed, or in a different workflow | Send it in a later batch, through the same workflow. |
Row skipped on a cancellation | A second cancellation of the same original | Nothing to do; the ledger is correct. |
| Row failed with a different-amounts message | The same original eventKey arrived again with different inputs | Use one eventKey per exact reward. |
Event ingest skipped | A duplicate batch or record id | Send a new cancellation eventKey rather than reusing the failed one. |
Job-level counts can hide row-level skips: read the per-row detail, not only the job status.
7. Cleanup
Undeploy and delete as usual. A cancelled original and its cancellation stay in the member's history, which is the record of what happened.
What the API does not cover
- Writing the CSV logic for you. A Code step runs what you wrote; the row shape and the reversal keys are your design.
- The decision to cancel. Your system decides and sends the cancellation row.
- Rewriting history. Correction of already-paid rewards is a separate, deliberate operation with its own approval.
Endpoints on this page
- Create a workflow:
POST /v1/workflow-builder/data-workflows - Add a step:
POST /v1/workflow-builder/data-workflows/{workflowId}/nodes - Get a workflow:
GET /v1/workflow-builder/data-workflows/{workflowId} - Test a whole workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/test