Reward members from your own system with a webhook workflow
A webhook trigger lets your own system award XP, coins or a numerical user field when something happens on your side: a deposit, a course completion, a support action. The API builds the workflow that receives those calls, and the same API proves what the workflow did.
The one step the API cannot take is deciding what "the same event twice" means in your system. That stays with the sender.
What you end up with
- A draft workflow with a webhook trigger and its generated key.
- A read step that resolves the member from the event.
- A branch and a write step that awards the reward once per event.
- A live workflow, one real run, and the member's field read back.
1. Read the catalog
curl --request GET \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/catalog" \
--header "Authorization: Bearer <API_KEY>"
Check nodeTypes for platform-action and http, and triggerTypes for webhook. Note the URL of each type's schema; every option below comes from it.
Take every field key from the catalog rather than guessing one: GET /catalog returns platformActionFields.get and platformActionFields.post, each entry carrying the key the step stores, the label the platform stores beside it, and whether the field is builtin or custom. A key outside those lists is stored as sent and POST /{workflowId}/validate reports UNKNOWN_USER_FIELD as a warning, but the step reads or writes nothing, so take the key from the catalog.
2. Decide the event contract first
Write it down before building anything: the event name, the field that identifies the member, the field that identifies the event (for deduplication), and the reward amount.
Keep the reward amount in the workflow, not in the sender's payload. A sender that can choose the amount can choose to send a million.
3. Create the workflow with its trigger
A webhook trigger key is generated by the platform. Ask for it with apiKey.enabled: true and no value, and keep the key from this response: no later read shows it again.
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":"Deposit reward","trigger":{"type":"webhook","name":"Deposit event","options":{"tokenExchangeEnabled":false,"apiKey":{"enabled":true}}}}'
Readback: 201, status: draft, trigger.type: webhook, a revision, and trigger.options.apiKey.value present. That value is your sender's credential. Store it somewhere secret and do not paste it into a document.
tokenExchangeEnabled is the other supported mode: the sender first exchanges the workflow key for a short session and sends that instead. Choose one mode and tell the sender which. The sender's side, the key exchange, sending the event and reading back the result, is covered in Workflow webhooks.
Cleanup: if you abandon this workflow, delete it (step 9) so an unused key does not stay valid.
4. Add the read step
Resolve the member from the event payload. The trigger's id is trigger._id from the create response, and the payload field is read from the trigger's root. The read keys total_xp and total_coins below come from the catalog's get list:
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":"platform-action","name":"Read member XP","position":{"after":"trigger"},"options":{"action":"get-user-fields","selectUser":{"type":"email","value":["{{65f1a2b3c4d5e6f708192a41.email}}"]},"fields":[{"label":"Total XP","field":"total_xp"},{"label":"Total Coins","field":"total_coins"}]}}'
Readback: 201 with the step, its _id and the new revision. That _id is what later steps reference.
5. Add a branch and the reward step
A router with one condition branch and one fallback. Conditions use AND: an OR or WHERE group is stored and deployed, and the runtime then treats it as always true, so validation reports ROUTER_OPERATOR_UNSAFE.
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: 68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501" \
--data '{"type":"router","name":"Event has a member?","position":{"after":"65f1a2b3c4d5e6f708192a3c"},"options":{"executePaths":"first_matching","branches":[{"branchName":"Member found","branchType":"CONDITION","conditions":[{"id":"group-1","operator":"AND","nestedGroups":[{"id":"nested-1","operator":"AND","conditions":[{"id":"condition-1","key":["{{65f1a2b3c4d5e6f708192a3c.Total XP}}"],"type":"number","operator":"greater_than_or_equal","value":["0"]}]}]}]},{"branchName":"Otherwise","branchType":"FALLBACK","conditions":[]}]}}'
Then the award, inside branch 0 of that router (65f1a2b3c4d5e6f708192a3e):
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: 9ab88dc5d9fdaada545d24e2d76d58cd758c7b99eb905df867d90cdda294e12f" \
--data '{"type":"platform-action","name":"Add 100 XP","position":{"routerNodeId":"65f1a2b3c4d5e6f708192a3e","branchIndex":0},"options":{"action":"post-user-fields","selectUser":{"type":"email","value":["{{65f1a2b3c4d5e6f708192a41.email}}"]},"fields":[{"label":"XP","field":"xp","logic":"add","value":["100"]}]}}'
logic: add is what makes the reward cumulative. overwrite replaces the value with the amount, which is almost never what a reward means. The write key xp comes from the catalog's post list; a custom field would appear there with its own key and label.
Readback: GET /{workflowId} shows three steps and the write step nested under the first branch.
6. Validate, then dry-run
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/validate" \
--header "Authorization: Bearer <API_KEY>" \
--header "Content-Type: application/json" \
--data '{}'
A dry run proves the reward without paying anyone:
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":{"email":"trader@example.com","event_id":"E-1"}}'
Readback: a trace where the read step is executed, the write step is simulated with wouldDo naming the member, the field xp, the amount 100 and the logic add, and sideEffects: "none".
Stop and fix when the trace shows a literal mapping instead of a resolved member, or when the branch taken is not the one you expected. A simulated step's wouldDo.users list is the member the platform resolved, so an empty list means the identifier did not resolve.
7. Deploy and fire one real event
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/deploy" \
--header "Authorization: Bearer <API_KEY>" \
--header "If-Match: 68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501"
Then send one event from your own system, with a fresh event id. Read the run back:
curl --request GET \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/runs?page=1&limit=5" \
--header "Authorization: Bearer <API_KEY>"
Read GET /runs/{runId} for that run, then read the member's field through the platform API. The run result and the member's value together are what settle it; a 200 from the trigger call alone shows only that the call was accepted.
8. Failure handling
| What you see | What it means | What to do |
|---|---|---|
401 from the webhook call | Wrong key, or the trigger was replaced after the sender was configured | Read the trigger: a stored key is never shown again, and a PUT cannot replace it. Set the trigger to another type and back to mint a new key, then update the sender. |
| Deploy went through with warnings you did not read | Validation is advisory, so no validation finding refuses a deploy; the response's warnings list what a run may mishandle | Fix the named steps, then validate again and compare the two reports. |
Run error with User not found | The identifier the event carried does not resolve to a member | Check the sender's field, then the read step's binding. |
Run success, member value unchanged | The write step was skipped, or the amount was zero | Read the run detail and check the branch that ran. |
| Two awards for one event | The sender sent the event twice and the workflow has no duplicate guard | Fix deduplication at the sender, then reconcile the extra award through your own reversal process. |
A retry of a failed run is a real execution: POST /runs/{runId}/retry runs the whole workflow again and can pay twice. It needs no confirmation, so read the member's value before retrying, and send an Idempotency-Key so a lost response replays instead of queueing a second run.
9. Cleanup
curl --request POST \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/undeploy" \
--header "Authorization: Bearer <API_KEY>" \
--header "If-Match: 68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501"
curl --request DELETE \
--url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b" \
--header "Authorization: Bearer <API_KEY>" \
--header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"
A delete also works on a deployed workflow, in one call, with its trigger, steps and run history, and it cannot be undone. Undeploy first if you want runs to stop before the workflow goes. Deleting removes the trigger and its key, but stop the sender as well: it keeps retrying until it is told not to.
What the API does not cover
- Sender-side deduplication. The workflow cannot see that an event is a repeat unless the sender says so or the event carries a stable id the workflow branches on.
- Choosing the reward amount from the event. Keep it in the workflow.
- A hard guarantee that two runs never overlap for the same member. Ordering is the sender's cadence.
Endpoints on this page
- List step types and trigger types:
GET /v1/workflow-builder/data-workflows/catalog - Create a workflow:
POST /v1/workflow-builder/data-workflows - Add a step:
POST /v1/workflow-builder/data-workflows/{workflowId}/nodes - Validate a stored workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/validate - Test a whole workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/test - Deploy a workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/deploy - List the runs of a workflow:
GET /v1/workflow-builder/data-workflows/{workflowId}/runs - Undeploy a workflow:
POST /v1/workflow-builder/data-workflows/{workflowId}/undeploy - Delete a workflow:
DELETE /v1/workflow-builder/data-workflows/{workflowId} - Get a workflow:
GET /v1/workflow-builder/data-workflows/{workflowId} - Get one run:
GET /v1/workflow-builder/data-workflows/runs/{runId} - Retry a failed run:
POST /v1/workflow-builder/data-workflows/runs/{runId}/retry