> For the complete documentation index, see llms.txt.
Skip to main content

Check out Port for yourself ➜ 

Webhook

The webhook action node sends HTTP requests to external endpoints. This is the most versatile action type, allowing you to integrate with any system that exposes an HTTP API.

Configuration​

FieldTypeDescription
type"WEBHOOK"Required. Must be "WEBHOOK"
urlstringRequired. The endpoint URL to send the request to
agentbooleanWhether to route the request through the Port Execution Agent. Default: false
methodstringHTTP method: GET, POST, PUT, PATCH, DELETE. Default: POST
headersobjectCustom HTTP headers to include
bodyobjectRequest body (for POST, PUT, PATCH)
synchronizedbooleanWhether to wait for response. Default: true
onTimeout'fail' | 'continue'Behavior on request timeout. Default: 'fail'. See Timeout behavior.
onFailure'continue' | 'terminate'Whether to continue the workflow if this node fails. Default: 'terminate'
verbosebooleanWhen true, captures the full request and response bodies in the run logs. Default: false. See Verbose logging.

Basic example​

Send a POST request to an external API, passing service deployment details from the trigger inputs:

{
"identifier": "send-webhook",
"title": "Send Webhook",
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/deploy",
"method": "POST",
"body": {
"service": "{{ .outputs.trigger.service }}",
"version": "{{ .outputs.trigger.version }}",
"environment": "{{ .outputs.trigger.environment }}"
}
}
}

Custom headers​

Include custom headers in your request, such as content type, custom values, or authentication tokens from secrets:

{
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/webhook",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"X-Custom-Header": "custom-value",
"Authorization": "Bearer {{ .secrets[\"api-token\"] }}"
},
"body": {
"data": "{{ .outputs.trigger.data }}"
}
}
}

Using secrets​

Reference organization secrets in your webhook configuration:

{
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/webhook?token={{ .secrets[\"webhook-token\"] }}",
"method": "POST",
"headers": {
"Authorization": "Bearer {{ .secrets[\"api-key\"] }}"
},
"body": {
"message": "Hello from Port"
}
}
}

Synchronous vs asynchronous​

Synchronous (default)​

When synchronized is true (default), the workflow waits up to 10 seconds for the HTTP response before timing out. That HTTP timeout is the primary limit for the call; the node's timeoutMinutes deadline (default 1 hour) is only a backstop if the run is still open afterward. If the request completes within the HTTP limit, the response is available to subsequent nodes:

{
"identifier": "create_resource",
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/resources",
"method": "POST",
"synchronized": true,
"body": {
"name": "{{ .outputs.trigger.name }}"
}
}
}

Reference the response in later nodes:

{
"body": {
"resourceId": "{{ .outputs.create_resource.response.data.id }}"
}
}

Asynchronous​

When synchronized is false, Port sends the HTTP request and does not treat the HTTP response as the node result. After the request is accepted (or times out with onTimeout: continue), the node run stays in progress until your external service reports completion via Port's API, or until the node deadline passes (default 24 hours). That deadline is the bound on waiting for the callback; it is also a backstop if the initial request path leaves the run open unexpectedly.

The initial HTTP call still has a hard timeout of 5 seconds. That only limits how long Port waits for the HTTP request itself, it is not how long the node may wait for your callback.

{
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/long-running-task",
"synchronized": false,
"body": {
"task": "{{ .outputs.trigger.task }}"
}
}
}
Asynchronous webhook behavior

With asynchronous webhooks, the external service is responsible for updating the node run via Port's API. The node deadline (timeoutMinutes, default 24 hours) bounds that wait: if nothing reports before it passes, the node run is marked FAILED with a Timed out label and onFailure applies.

Timeout behavior​

Webhook nodes have two independent timeouts:

  1. HTTP request timeout (fixed, not configurable) - how long Port waits for the outbound HTTP call:
    • Synchronized: 10 seconds (primary limit for the call; usually concludes the node via success, onTimeout: fail, or onTimeout: continue)
    • Asynchronous (initial request): 5 seconds (limits only the fire request, not the wait for a callback)
  2. Node deadline (timeoutMinutes, 60–43200) - backstop for how long the node run may stay open. Defaults to 1 hour for synchronized (non-agent) webhooks and 24 hours for asynchronous and agent webhooks. See Node timeout.
    • Synchronized: rarely reached, because the HTTP timeout normally concludes the run within seconds. It applies only if the run is still open afterward.
    • Asynchronous: bounds the wait for the external callback after the initial request. If nothing reports before the deadline, the node run is marked FAILED with a Timed out label and onFailure applies (onTimeout is not involved).

You cannot increase the HTTP request timeout. For work that takes longer than a few seconds, use an asynchronous webhook and have the external service report completion via Port's API.

The onTimeout field controls only the HTTP request timeout. It does not extend either timeout and is not used when the node deadline passes.

Not the node timeout

The HTTP timeout and the node deadline are independent. A synchronized run that already failed or continued on the 10-second HTTP timeout is not failed again by the 1-hour node deadline. An asynchronous run whose initial request succeeded (or timed out with onTimeout: continue) keeps waiting until the callback or the node deadline — not until another HTTP timeout.

onTimeout applies when the HTTP request hits the 10s (sync) or 5s (async) limit:

ValueDescription
fail(Default) Mark the node as failed when the request times out
continueDon't fail on timeout. With synchronized webhooks: mark the node as successful with { timeout: true } in the output so the workflow continues. With asynchronous webhooks: keep the node in progress so the external service can call back to update the status.

Examples​

Graceful timeout handling (synchronized)

Use onTimeout: "continue" with a synchronized webhook when the external service might take longer than expected but you want the workflow to continue:

{
"identifier": "trigger-long-running-job",
"title": "Trigger Long Running Job",
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/long-job",
"method": "POST",
"onTimeout": "continue",
"body": {
"jobType": "{{ .outputs.trigger.jobType }}"
}
}
}

When a timeout occurs, the node completes with result SUCCESS and output { "timeout": true }, so subsequent nodes can run.

Asynchronous with continue on timeout

Use onTimeout: "continue" with an asynchronous webhook when the external service will call back to update the status:

{
"identifier": "async-processing",
"title": "Start Async Processing",
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/async-task",
"method": "POST",
"synchronized": false,
"onTimeout": "continue",
"body": {
"task": "{{ .outputs.trigger.task }}",
"callbackUrl": "https://api.port.io/v1/workflows/nodes/runs/{{ .workflowNodeRun.identifier }}"
}
}
}

When a timeout occurs, the node stays in progress until the external service calls PATCH on the callback URL to update it.

Examples​

Send Slack notification​

Post a message to Slack using the Port Slack app's bot token, resolving the target channel dynamically from workflow data so the same node can notify different channels per run:

Dynamic channel routing

Node example (click to expand)
{
"identifier": "slack-notify",
"title": "Send Slack Message",
"config": {
"type": "WEBHOOK",
"url": "https://slack.com/api/chat.postMessage",
"method": "POST",
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Authorization": "Bearer {{ .secrets[\"__SLACK_APP_BOT_TOKEN_T123\"] }}"
},
"body": {
"channel": "{{ .outputs.trigger.slackChannelId }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*{{ .outputs.trigger.service }}* is being deployed to *{{ .outputs.trigger.environment }}*"
}
}
]
}
}
}
Set up the Slack app bot token

Install the Port Slack app to get a __SLACK_APP_BOT_TOKEN_<team_id> secret for your workspace, then reference it in the Authorization header as shown above. Resolving channel from trigger or output data (instead of a fixed webhook URL) is what lets one node route to different channels.

Single fixed channel only

If every message from this node always goes to the same channel, or you'd rather not install the Port Slack app, an incoming webhook URL is a simpler alternative, at the cost of not being able to change the channel per run:

Node example (click to expand)
{
"identifier": "slack-notify-webhook",
"title": "Send Slack Message",
"config": {
"type": "WEBHOOK",
"url": "https://hooks.slack.com/services/xxx/yyy/zzz",
"method": "POST",
"body": {
"text": "Deployment started",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*{{ .outputs.trigger.service }}* is being deployed to *{{ .outputs.trigger.environment }}*"
}
}
]
}
}
}

Trigger GitHub workflow​

Dispatch a GitHub Actions workflow using the GitHub API with token authentication:

Node example (click to expand)
{
"identifier": "trigger-github",
"title": "Trigger GitHub Workflow",
"config": {
"type": "WEBHOOK",
"url": "https://api.github.com/repos/{{ .outputs.trigger.repo }}/actions/workflows/deploy.yml/dispatches",
"method": "POST",
"headers": {
"Accept": "application/vnd.github.v3+json",
"Authorization": "Bearer {{ .secrets[\"github-token\"] }}"
},
"body": {
"ref": "main",
"inputs": {
"environment": "{{ .outputs.trigger.environment }}",
"version": "{{ .outputs.trigger.version }}"
}
}
}
}
Prefer the GitHub integration action

If you have a GitHub integration installed, consider using the GitHub integration action instead. It provides a simpler configuration, automatic authentication via your installed integration, and built-in status reporting.

Call Port API​

You can use webhook nodes to call Port's API, allowing you to execute any route you wish with automatic authentication.

When calling Port's API (https://api.port.io), you don't need to include an access token in the request headers. Port automatically authenticates the request using your organization's credentials.

Node example (click to expand)
{
"identifier": "create_port_entity",
"title": "Create Port Entity",
"config": {
"type": "WEBHOOK",
"url": "https://api.port.io/v1/blueprints/deployment/entities",
"method": "POST",
"body": {
"identifier": "{{ .outputs.trigger.service }}_{{ .outputs.trigger.environment }}",
"title": "{{ .outputs.trigger.service }} ({{ .outputs.trigger.environment }})",
"properties": {
"version": "{{ .outputs.trigger.version }}",
"deployedAt": "{{ now | todateiso8601 }}"
},
"relations": {
"service": "{{ .outputs.trigger.service }}"
}
}
}
}

This is useful when you want to:

  • Fetch entities from the catalog (see Data flow)
  • Create or update entities
  • Trigger other self-service actions
  • Perform any operation available in Port's API
Simpler alternative for entities

For creating or updating entities, consider using the upsert entity action node instead, which provides a simpler configuration.

Verbose logging​

By default, webhook nodes omit request and response bodies from run logs to keep output concise. Setting verbose: true on a node captures the full request and response bodies, making it easier to debug payloads without additional tooling.

{
"identifier": "send-webhook",
"title": "Send Webhook",
"verbose": true,
"config": {
"type": "WEBHOOK",
"url": "https://api.example.com/deploy",
"method": "POST",
"body": {
"service": "{{ .outputs.trigger.service }}"
}
}
}
Security best practice

Enable verbose only when debugging. Leave it off in production to avoid exposing sensitive request/response data in run logs.