Incoming Webhook Step
Trigger a new workflow execution when an external system sends an HTTP request to a generated endpoint.
The Incoming Webhook step is a trigger — it must be the first step in a workflow and it starts a new execution each time an external system sends an HTTP request to the generated endpoint. Use it to integrate with third-party systems that push data rather than respond to API calls.
For mapping placeholder syntax, see Mapping.
When you create a project whose workflow starts with an Incoming Webhook step, the project response includes a webhookSessionStartUrl in the __meta object. That is the URL your external system calls to start a session. The URL uses the step's stable publicId, so it does not change when you update the workflow.
Choose the webhook response timing
By default, POST webhooks wait for Streamline's normal session response before returning to the caller. If the caller only needs confirmation that Streamline accepted the request and started the workflow session, add response_mode=immediate to the webhookSessionStartUrl.
| Request | When to use it | Response behavior |
|---|---|---|
POST {webhookSessionStartUrl} | The caller can wait for the standard workflow response. | Streamline accepts the webhook and returns the normal session response. |
POST {webhookSessionStartUrl}?response_mode=immediate | The caller needs a quick acknowledgement after the workflow starts. | Streamline returns after the request is accepted and handed off to the workflow session. |
GET {webhookSessionStartUrl} | The webhook uses query parameters and browser-style redirects. | GET is the only HTTP method that does not support response_mode=immediate; use POST when you need immediate response mode. |
Immediate response mode confirms that Streamline accepted the request and started the workflow session. It does not mean every downstream step has finished.
The example below starts a webhook session and returns as soon as Streamline accepts the request:
curl -X POST "{webhookSessionStartUrl}?response_mode=immediate" \
-H "Content-Type: application/json" \
-d '{"orderId":"ord-789","status":"placed"}'The immediate response includes the accepted session identifier. When a resume URL is available, the response also includes resumeUrl.
{
"success": true,
"message": "Webhook received successfully",
"sessionId": "11111111-1111-1111-1111-111111111111",
"resumeUrl": "https://us.streamline.intellistack.ai/sessions/11111111-1111-1111-1111-111111111111"
}Authentication
The step's authenticationType controls how Streamline validates incoming requests.
| authenticationType | behaviour |
|---|---|
NONE | The endpoint is public. Any caller can trigger a session without credentials. |
PAT | Callers must include a valid bearer token in the Authorization header. Streamline validates the token against the organization's authentication provider. |
HMAC | Callers must sign the raw request body with a shared secret and include the signature in the Streamline-Signature header. Requires an integrationId referencing a configured integration that holds the HMAC secret. |
When authenticationType is HMAC, you must also provide integrationId (the integration that stores the shared secret). For NONE and PAT, integrationId is optional.
HMAC signature
For HMAC-authenticated webhooks, every request must include a Streamline-Signature header containing the HMAC-SHA256 digest of the raw request body:
Streamline-Signature: sha256={hex}| Detail | Value |
|---|---|
| Algorithm | HMAC-SHA256 |
| Header | Streamline-Signature: sha256={hex} |
| Input | Exact raw request body bytes (UTF-8) |
| Secret | The shared secret from the integration referenced by integrationId |
Streamline recomputes the digest from the raw bytes it receives and compares it to the value in the header. If the digests do not match, the request is rejected with 401. If the header is missing or malformed (e.g. missing the sha256= prefix), the request is rejected with 400.
HMAC-authenticated webhooks are commonly used with embedded workflows, where your server signs the payload and starts the session before passing the resumeUrl to a client-side iframe. For a complete walkthrough — including code examples in Node.js, Python, Java, and cURL — see .
Request formats and file support
Incoming Webhook accepts three request content types. The format you use determines how Streamline parses the body and handles any files included in the request.
| Content-Type | Behavior |
|---|---|
application/json | Standard JSON payload. Supports scalar fields, nested objects, arrays, and file attachments via the base64 file wrapper. |
multipart/form-data | Supports mixed text and file uploads in a single request. File parts are extracted as files; text parts are mapped into body. |
application/octet-stream | Treats the full request body as a single binary file, made available as body.file. |
Nested objects in JSON payloads
Nested objects in a JSON body are fully supported. During field extraction, Streamline flattens each nested property to a dot-notation path so each one becomes its own mappable field in later steps.
The example below shows a payload with a top-level address object alongside flat scalar fields:
{
"firstName": "Jane",
"lastName": "Doe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zip": "12345"
}
}Streamline extracts body.firstName, body.lastName, body.address.street, body.address.city, and body.address.zip as separate mappable fields — each available individually in later steps.
File attachments in JSON payloads
To include a file in an application/json request, wrap the base64-encoded content in an object with __type set to "file". Streamline recognizes this structure and treats it as a file rather than a plain JSON value.
Here is a payload that includes a scalar field and a file attachment:
{
"invoiceNumber": "INV-001",
"attachment": {
"__type": "file",
"name": "invoice.pdf",
"contentType": "application/pdf",
"content": "JVBERi0xLjQK..."
}
}The file wrapper supports these fields:
| name | type | required | constraints | description |
|---|---|---|---|---|
__type | string | yes | Must be "file" | Identifies this object as a file attachment. |
content | string | yes | Base64-encoded | The file content encoded as a base64 string. |
name | string | no | Filename including extension (e.g. "invoice.pdf"). Defaults to a .bin filename if omitted. | |
contentType | string | no | Valid MIME type | MIME type of the file (e.g. "application/pdf"). Auto-detected from the file content if omitted; defaults to "application/octet-stream" when detection is not possible. |
Once extracted, the file field is available as type: "file" in mappableFields and can be mapped to any downstream step that accepts a file input.
File attachments in multipart requests
For multipart/form-data requests, Streamline handles each part based on whether it carries a file:
- File parts — parts that include a
filenamein theContent-Dispositionheader — are extracted as files. The filename and content type are read from the part headers. - Text parts — parts without a
filename— are treated as plain strings unless the part includesContent-Type: application/json, in which case Streamline parses the value as structured JSON.
The example below sends a JSON-encoded field and a file in a single multipart request:
curl -X POST "https://us.streamline.intellistack.ai/v1/webhooks/incoming/{publicId}" \
-F 'order={"id":"ord-001","amount":99.99};type=application/json' \
-F "invoice=@/tmp/invoice.pdf"Do not reuse the same field name for both a text part and a file part in the same multipart request.
Like base64 file attachments, multipart file parts are surfaced as type: "file" in mappableFields and can be mapped to downstream steps that accept file inputs.
Configuration
| name | type | required | constraints | description |
|---|---|---|---|---|
authenticationType | string | yes | NONE, PAT, or HMAC | How Streamline authenticates incoming requests. |
integrationId | string | conditional | Integration holding the authentication secret. Required when authenticationType is HMAC; optional otherwise. | |
method | string | yes | GET or POST | HTTP method to accept. |
listeningModeEnabled | boolean | no | Set to true to put the step in listening mode so it captures a sample request and extracts fields. Streamline resets this to false after extraction completes. Default: false. | |
requestPayload | string | no | JSON string snapshot of the captured test request (body, query parameters, and headers). Streamline writes this when a test request succeeds. Default: empty string. | |
mappableFields | array | no | Fields extracted from the captured payload, available for mapping in later steps. See Mappable fields. Default: empty array. | |
groupNodes | array | no | Entity group descriptors for repeating collections within the payload. See Group nodes. |
Mappable fields
Each entry in mappableFields describes a field from the incoming request that later steps can map. Field keys are prefixed with body., query., or headers. depending on where they appear in the request.
| name | type | required | constraints | description |
|---|---|---|---|---|
key | string | yes | The field path in the captured request (e.g. body.orderId, query.status, headers.x-request-id). | |
label | string | yes | Human-readable label for the field. | |
type | string | yes | string, number, boolean, date, datetime, time, object, array, or file | Data type of the field. Use file for file attachment fields. |
exampleValue | string, number, boolean, or null | no | Sample value stored with the field and shown where Streamline displays sample data. | |
fieldCategory | string | no | REGULAR, FLATTENED, WILDCARD, or FILE | Set by Streamline during extraction. REGULAR — a top-level scalar value (e.g. body.status). FLATTENED — a value from a nested object, expressed as a dot-notation path (e.g. body.address.city). WILDCARD — a value inside a repeating array, referenced with a wildcard path (e.g. body.items.*.name). FILE — a file attachment field. |
Populating mappable fields
mappableFields is a regular configuration property — you can populate it automatically from a test request, or write the array directly when you create or update the project.
Option A: Extract from a test request
This is the fastest path when you have a representative payload. The flow is:
- Enable listening mode — update the step configuration in the project and set
listeningModeEnabledtotrue. - Send a test request — append
?test=trueto the step'swebhookSessionStartUrl(from the project response's__metaobject) and send a POST or GET:
POST {webhookSessionStartUrl}?test=true
The request body, query parameters, and headers you send become the sample payload. Authentication follows the step's authenticationType — for NONE, no credentials are needed; for PAT, include a bearer token; for HMAC, include the Streamline-Signature: sha256={hex} header (see HMAC signature).
Streamline validates that the step is in listening mode, stores the payload as requestPayload on the step configuration (sensitive headers such as Authorization and Cookie are stripped before storage), and starts field extraction in the background. The endpoint returns immediately with:
{
"success": true,
"message": "Test webhook received successfully. Field extraction is being processed in background."
}If the step is not in listening mode, the request is rejected with a 400 error. If an extraction is already in progress, the request is rejected until the current extraction completes.
- Retrieve the extracted fields — once extraction finishes, Streamline writes the results to
mappableFieldson the step configuration and resetslisteningModeEnabledtofalse. Fetch the project viaGET /v1/projects/{projectId}to read the populatedmappableFieldsandrequestPayloadfrom the step's config.
The extracted fields can then be referenced in later steps using the step's description as the mapping source (e.g. {{Incoming webhook.body.orderId}}).
If the results are not what you expected, enable listening mode again, send a different payload, and Streamline re-extracts.
Option B: Set mappable fields directly
If you already know the payload structure, you can skip listening mode entirely and include mappableFields in the step configuration when you create or update the project:
PATCH /v1/projects/{projectId}
{
"workflow": {
"steps": [
{
"type": "webhook",
"description": "Incoming webhook",
"config": {
"authenticationType": "NONE",
"method": "POST",
"mappableFields": [
{ "key": "body.orderId", "type": "string", "label": "Order ID" },
{ "key": "body.amount", "type": "number", "label": "Amount" },
{ "key": "body.status", "type": "string", "label": "Status" }
]
},
"edges": []
}
]
}
}Each key must match the path that will appear in the incoming request at runtime (prefixed with body., query., or headers.).
Customizing after extraction
Whether fields were extracted automatically or written directly, you can modify them at any time by updating the project. Add fields, remove fields, rename labels, or change types — then include the updated mappableFields array in the step's config. Later steps that reference these fields by key use whatever is currently stored on the step.
Draft workflow version execution
Incoming Webhook supports executing published and draft workflow versions for both normal session execution and test mode.
Builder flow (recommended)
In Builder, use the draft testing toggle/flow. Streamline applies the draft version automatically, so users do not need to manually add query parameters in normal Builder usage.
Advanced API flow
For direct API-driven testing, append sl_project_version=<positive integer> to the webhook URL.
Examples:
- Published:
POST {webhookSessionStartUrl} - Draft:
POST {webhookSessionStartUrl}?sl_project_version=12 - Draft with immediate response:
POST {webhookSessionStartUrl}?sl_project_version=12&response_mode=immediate - Draft (test mode):
POST {webhookSessionStartUrl}?test=true&sl_project_version=12
Notes:
- If
sl_project_versionis omitted, Streamline executes the published version. sl_project_versionmust be a positive integer.- Non-numeric or non-positive values are rejected with
400.
Group nodes
When the captured payload contains arrays, Streamline creates groupNodes entries that describe the repeating structure. Each group node identifies an array path so that later steps can iterate over collection items.
| name | type | required | description |
|---|---|---|---|
id | string | yes | Unique identifier for the group node. |
label | string | yes | Human-readable name for the collection. |
path | string | yes | Dot-notation path to the array in the payload (e.g. body.orders). |
wildcardPath | string | yes | Path with * replacing array indices (e.g. body.orders.*). |
fields | array | no | Scalar fields within each item of the collection. Each entry has path (string) and type (string). Default: empty array. |
itemCount | number | no | Number of items found in the sample payload for this collection. |
isRepeating | boolean | no | Whether this collection repeats (always true for detected arrays). Default: false. |
parentPath | string or null | no | Path of the parent group node for nested arrays. null for top-level collections. |
displayRoot | boolean | no | Whether this node is the top-level entry point when the payload contains nested arrays. |
Examples
Let's start with an unconfigured webhook trigger — no fields extracted yet.
{
"authenticationType": "NONE",
"method": "POST",
"listeningModeEnabled": false,
"requestPayload": "",
"mappableFields": []
}Here's what the step configuration looks like after sending a sample payload and extracting fields. Notice that listeningModeEnabled is back to false and the fields use body. prefixed keys.
{
"authenticationType": "NONE",
"method": "POST",
"listeningModeEnabled": false,
"requestPayload": "{\"body\":{\"orderId\":\"ord-789\",\"customerEmail\":\"[email protected]\",\"amount\":149.99,\"status\":\"placed\"},\"query\":{},\"headers\":{}}",
"mappableFields": [
{ "key": "body.orderId", "type": "string", "label": "Order ID", "exampleValue": "ord-789" },
{ "key": "body.customerEmail", "type": "string", "label": "Customer Email", "exampleValue": "[email protected]" },
{ "key": "body.amount", "type": "number", "label": "Amount", "exampleValue": 149.99 },
{ "key": "body.status", "type": "string", "label": "Status", "exampleValue": "placed" }
]
}Here is a PAT-authenticated webhook that accepts GET requests.
{
"authenticationType": "PAT",
"method": "GET",
"listeningModeEnabled": false,
"requestPayload": "",
"mappableFields": []
}