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.

RequestWhen to use itResponse 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=immediateThe 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.

authenticationTypebehaviour
NONEThe endpoint is public. Any caller can trigger a session without credentials.
PATCallers must include a valid bearer token in the Authorization header. Streamline validates the token against the organization's authentication provider.
HMACCallers 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}
DetailValue
AlgorithmHMAC-SHA256
HeaderStreamline-Signature: sha256={hex}
InputExact raw request body bytes (UTF-8)
SecretThe 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-TypeBehavior
application/jsonStandard JSON payload. Supports scalar fields, nested objects, arrays, and file attachments via the base64 file wrapper.
multipart/form-dataSupports mixed text and file uploads in a single request. File parts are extracted as files; text parts are mapped into body.
application/octet-streamTreats 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:

nametyperequiredconstraintsdescription
__typestringyesMust be "file"Identifies this object as a file attachment.
contentstringyesBase64-encodedThe file content encoded as a base64 string.
namestringnoFilename including extension (e.g. "invoice.pdf"). Defaults to a .bin filename if omitted.
contentTypestringnoValid MIME typeMIME 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 filename in the Content-Disposition header — 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 includes Content-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

nametyperequiredconstraintsdescription
authenticationTypestringyesNONE, PAT, or HMACHow Streamline authenticates incoming requests.
integrationIdstringconditionalIntegration holding the authentication secret. Required when authenticationType is HMAC; optional otherwise.
methodstringyesGET or POSTHTTP method to accept.
listeningModeEnabledbooleannoSet 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.
requestPayloadstringnoJSON string snapshot of the captured test request (body, query parameters, and headers). Streamline writes this when a test request succeeds. Default: empty string.
mappableFieldsarraynoFields extracted from the captured payload, available for mapping in later steps. See Mappable fields. Default: empty array.
groupNodesarraynoEntity 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.

nametyperequiredconstraintsdescription
keystringyesThe field path in the captured request (e.g. body.orderId, query.status, headers.x-request-id).
labelstringyesHuman-readable label for the field.
typestringyesstring, number, boolean, date, datetime, time, object, array, or fileData type of the field. Use file for file attachment fields.
exampleValuestring, number, boolean, or nullnoSample value stored with the field and shown where Streamline displays sample data.
fieldCategorystringnoREGULAR, FLATTENED, WILDCARD, or FILESet 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:

  1. Enable listening mode — update the step configuration in the project and set listeningModeEnabled to true.
  2. Send a test request — append ?test=true to the step's webhookSessionStartUrl (from the project response's __meta object) 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.

  1. Retrieve the extracted fields — once extraction finishes, Streamline writes the results to mappableFields on the step configuration and resets listeningModeEnabled to false. Fetch the project via GET /v1/projects/{projectId} to read the populated mappableFields and requestPayload from 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_version is omitted, Streamline executes the published version.
  • sl_project_version must 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.

nametyperequireddescription
idstringyesUnique identifier for the group node.
labelstringyesHuman-readable name for the collection.
pathstringyesDot-notation path to the array in the payload (e.g. body.orders).
wildcardPathstringyesPath with * replacing array indices (e.g. body.orders.*).
fieldsarraynoScalar fields within each item of the collection. Each entry has path (string) and type (string). Default: empty array.
itemCountnumbernoNumber of items found in the sample payload for this collection.
isRepeatingbooleannoWhether this collection repeats (always true for detected arrays). Default: false.
parentPathstring or nullnoPath of the parent group node for nested arrays. null for top-level collections.
displayRootbooleannoWhether 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": []
}