# 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

1. A draft workflow with a webhook trigger and its generated key.
2. A read step that resolves the member from the event.
3. A branch and a write step that awards the reward once per event.
4. A live workflow, one real run, and the member's field read back.

## 1. Read the catalog

`List step types and trigger types`

```bash
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`](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md) lists them in `platformActionFields.get` (read) and `platformActionFields.post` (write). See [Field keys come from the catalog](https://docs.returning.ai/workflow-builder.md#field-keys-come-from-the-catalog). A key outside those lists is stored as sent and [`POST /{workflowId}/validate`](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md) 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.

`Create a workflow`

```bash
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.

> Keep the key from this response
>
> `trigger.options.apiKey.value` is your sender's credential, and no later read shows it again. 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](https://docs.returning.ai/broker-integrations/workflow-webhooks.md).

Cleanup: if you abandon this workflow, delete it (see [Cleanup](#cleanup)) 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:

`Add a step`

```bash
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.

> Router 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`.

`Add a step`

```bash
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`):

`Add a step`

```bash
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}`](https://docs.returning.ai/api-reference/workflow-builder/get-a-workflow.md) shows three steps and the write step nested under the first branch.

## 6. Validate, then dry-run

`Validate a stored workflow`

```bash
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 '{}'
```

> Validation is advisory
>
> `validate` returns its findings as `warnings` and refuses nothing, and deploy returns the same list and goes ahead. Read them before you deploy.

A dry run proves the reward without paying anyone:

`Test a whole workflow`

```bash
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

`Deploy a workflow`

```bash
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:

`List the runs of a workflow`

```bash
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}`](https://docs.returning.ai/api-reference/workflow-builder/get-one-run.md) 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.

## 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 can pay twice
>
> A retry of a failed run is a real execution: [`POST /runs/{runId}/retry`](https://docs.returning.ai/api-reference/workflow-builder/retry-a-failed-run.md) 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.

## Cleanup

> A delete cannot be undone
>
> 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.

`Undeploy a workflow`

```bash
curl --request POST \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b/undeploy" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "If-Match: 68f78253046393414dec404e5442c140bb5db0393a7d1c389ff57bba71585501"
```

`Delete a workflow`

```bash
curl --request DELETE \
  --url "https://api.returning.ai/v1/workflow-builder/data-workflows/65f1a2b3c4d5e6f708192a3b" \
  --header "Authorization: Bearer <API_KEY>" \
  --header "If-Match: 162af81f5de4ff2b557a4087de9a9a73a0ed4e621f737787e35a93614cb50d1c"
```

## 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

- GET [List step types and trigger types](https://docs.returning.ai/api-reference/workflow-builder/list-step-types-and-trigger-types.md) `/catalog`
- POST [Create a workflow](https://docs.returning.ai/api-reference/workflow-builder/create-a-workflow.md) `/`
- POST [Add a step](https://docs.returning.ai/api-reference/workflow-builder/add-a-step.md) `/{workflowId}/nodes`
- POST [Validate a stored workflow](https://docs.returning.ai/api-reference/workflow-builder/validate-a-stored-workflow.md) `/{workflowId}/validate`
- POST [Test a whole workflow](https://docs.returning.ai/api-reference/workflow-builder/test-a-whole-workflow.md) `/{workflowId}/test`
- POST [Deploy a workflow](https://docs.returning.ai/api-reference/workflow-builder/deploy-a-workflow.md) `/{workflowId}/deploy`
- GET [List the runs of a workflow](https://docs.returning.ai/api-reference/workflow-builder/list-the-runs-of-a-workflow.md) `/{workflowId}/runs`
- POST [Undeploy a workflow](https://docs.returning.ai/api-reference/workflow-builder/undeploy-a-workflow.md) `/{workflowId}/undeploy`
- DELETE [Delete a workflow](https://docs.returning.ai/api-reference/workflow-builder/delete-a-workflow.md) `/{workflowId}`
- GET [Get a workflow](https://docs.returning.ai/api-reference/workflow-builder/get-a-workflow.md) `/{workflowId}`
- GET [Get one run](https://docs.returning.ai/api-reference/workflow-builder/get-one-run.md) `/runs/{runId}`
- POST [Retry a failed run](https://docs.returning.ai/api-reference/workflow-builder/retry-a-failed-run.md) `/runs/{runId}/retry`

Paths that start with `/v1/` are full paths; the rest are relative to `https://api.returning.ai/v1/workflow-builder/data-workflows`.
