Send HTTP POST requests to external services.

In Workflow, webhooks are configured as an `ACTION` step with `actionType: WEBHOOK` rather than a separate step type.

## Configuration

- **Webhook URL**: HTTP(S) endpoint to call
- **Webhook Secret**: Optional secret for authentication
- **Payload**: Optional JSON object nested under `payload` on the request body. It is never merged onto the top-level fields.

```json
{
  "id": "notify-crm",
  "type": "ACTION",
  "config": {
    "actionType": "WEBHOOK",
    "url": "https://example.com/webhooks/lumail",
    "secret": "optional-secret",
    "payload": { "source": "welcome-workflow" }
  }
}
```

`get_workflow` / `get_workflow_draft` replace `config.secret` with the literal `[REDACTED]`. That is a **read placeholder**, not a keep-token.

- **First configure**, or any **new** webhook step: write the real secret or omit it. Never paste a redacted `get_workflow` payload as the first draft.
- **Later graph updates** may send `"secret": "[REDACTED]"` only if draft already stores a real secret. `restoreRedactedWorkflowSecrets` then copies the stored value.
- If the stored value is missing, empty, or already `[REDACTED]`, sending `[REDACTED]` is **rejected**. The placeholder is never stored.
- After configure/publish, `get_workflow` still shows `[REDACTED]`. That does **not** mean the stored secret is valid.

## Request

`POST` with `content-type: application/json`. When a secret is set, the same value is sent as `x-lumail-signature` and as `body.secret`.

```json
{
  "subscriberId": "sub_123",
  "subscriberEmail": "user@example.com",
  "organizationId": "org_xyz",
  "workflowId": "wf_789",
  "stepId": "step_456",
  "secret": "your-secret",
  "payload": { "source": "welcome-workflow" }
}
```

`secret` is omitted when the step has no secret. `payload` is always an object (`{}` when unset). Do not flatten `payload` onto the top level.

`get_workflow` always redacts `config.secret`. Do not treat that as proof the stored secret is real. See the rules above.

## Response

- **Success**: HTTP status < 400, workflow continues
- **Failure**: HTTP 4xx (except 408/429) fails the run immediately and shows the status plus response body. 5xx, 408, 429, and timeouts retry then fail. The FAILED run event stores HTTP status, a truncated response body, and the URL host
- **Dry run**: Records a skipped-action timeline event and makes no HTTP request

## Related Documentation

- [Dynamic Promo Codes Tutorial](/docs/tutorials/dynamic-promo-codes) - Use webhooks for personalized coupons
- [Update Subscriber API](/docs/api-reference/api-subscribers-patch) - Update subscribers from webhook responses
- [API Tokens](/docs/api-reference/api-tokens) - Authenticate webhook callbacks
- [Subscriber Events](/docs/features/subscriber-events) - View webhook execution events
