Browse documentation
Tutorial Webhook ingestion

Send your first webhook delivery

Configure an active webhook workflow, send one idempotent JSON delivery, and verify the accepted run through JSON:API.

Goal: Deliver one event to an active webhook workflow and verify its run.

View as Markdown

Create one controlled delivery and follow it from 202 Accepted to the JSON:API run record. The selected service account needs webhooks for delivery and api_read for the verification request.

What you will do

  1. Save and activate a workflow with a webhook trigger.
  2. Exchange the selected service account's credentials.
  3. Send one JSON body with a stable Idempotency-Key.
  4. Capture the workflow_run_id from the 202 response.
  5. Read that run through JSON:API.

Prerequisites

  • In the intended workspace, create a service account with webhooks and api_read, and select an active run-as member.
  • Create a workflow, choose a Webhook trigger, and select that exact service account. Add at least one valid step, save, and activate the workflow.
  • Copy the token-only URL displayed on the active workflow. Do not use the builder's /test endpoint: it only previews a payload and creates no run.
  • Collect the selected account's client ID and client secret. Set HODOFLOW_BASE_URL as described in Base URLs, workspaces, and versions.
  • Use Bash, cURL, and jq on a trusted machine.

The trigger token is secret material. Do not put it or the bearer token in source control, logs, screenshots, browser storage, or support messages.

Request

Read the trigger token and service-account credentials interactively:

read -r -s -p "Webhook trigger token: " TRIGGER_TOKEN
printf '\n'
read -r -p "Client ID: " CLIENT_ID
read -r -s -p "Client secret: " CLIENT_SECRET
printf '\n'
export TRIGGER_TOKEN CLIENT_ID CLIENT_SECRET

Exchange the client credentials. There is no refresh token:

TOKEN_RESPONSE="$(
jq -n '{
grant_type: "client_credentials",
client_id: env.CLIENT_ID,
client_secret: env.CLIENT_SECRET
}' |
curl --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
--data-binary @- \
"$HODOFLOW_BASE_URL/api/v1/oauth/token"
)"
TOKEN="$(printf '%s\n' "$TOKEN_RESPONSE" | jq -er '.access_token')"

Create a payload file and a sender-stable delivery ID. Reuse this ID only for retries of this exact delivery:

DELIVERY_FILE="$(mktemp)"
DELIVERY_ID="first-delivery-$(date -u +%Y%m%dT%H%M%SZ)"
printf '%s\n' '{"event":"order.created","data":{"id":"order-123","total":125.5}}' > "$DELIVERY_FILE"
DELIVERY_RESPONSE="$(
curl --silent --show-error \
--request POST \
--header "Authorization: Bearer $TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $DELIVERY_ID" \
--data-binary "@$DELIVERY_FILE" \
"$HODOFLOW_BASE_URL/api/v1/webhooks/$TRIGGER_TOKEN"
)"
printf '%s\n' "$DELIVERY_RESPONSE" | jq .
RUN_ID="$(printf '%s\n' "$DELIVERY_RESPONSE" | jq -er '.workflow_run_id')"

The request is intentionally one delivery, so there is no collection limit to set. 202 means the run was durably accepted and enqueued, not completed.

Read the returned run ID:

curl --silent --show-error --include \
--header "Accept: application/vnd.api+json" \
--header "Authorization: Bearer $TOKEN" \
"$HODOFLOW_BASE_URL/api/json/v1/workflow_runs/$RUN_ID"

Expected response

The delivery response is:

{
"status": "accepted",
"workflow_run_id": "550e8400-e29b-41d4-a716-446655440000"
}

The lookup returns 200, x-api-version: v1, an x-request-id, and one JSON:API workflow-run resource. Its state may still be pending or running:

{
"data": {
"type": "workflow_run",
"id": "550e8400-e29b-41d4-a716-446655440000",
"attributes": {
"state": "pending"
}
}
}

Poll deliberately rather than in a tight loop. Stop when the state becomes completed, failed, or suspended; see Follow a workflow run for the full sequence.

Troubleshooting

  • 401 with {"error":"missing_credentials"}: the bearer token is absent, invalid, or expired. Exchange once and retry once.
  • 401 with {"error":"Unauthorized"}: the token was not issued for the exact selected service account, the organization differs, or connector auth failed. Correct configuration before retrying.
  • 403 with {"error":"Insufficient scope"}: grant webhooks to the selected account; retrying the same token cannot add a scope.
  • 404 with {"error":"Trigger not found"}: verify the token-only URL and that the workflow is saved and active.
  • 413: reduce the body before retrying.
  • 429 or 503: wait Retry-After, then retry with the same delivery ID.
  • A retry returns {"status":"duplicate"}: the first attempt was accepted; no second run was created, so continue with the run ID captured previously.

Clean up local secrets and the temporary body:

rm "$DELIVERY_FILE"
unset CLIENT_ID CLIENT_SECRET TOKEN TOKEN_RESPONSE TRIGGER_TOKEN DELIVERY_ID DELIVERY_FILE DELIVERY_RESPONSE RUN_ID

Next steps

Related documentation