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
- Save and activate a workflow with a webhook trigger.
- Exchange the selected service account's credentials.
- Send one JSON body with a stable
Idempotency-Key. - Capture the
workflow_run_idfrom the202response. - Read that run through JSON:API.
Prerequisites
- In the intended workspace, create a service account with
webhooksandapi_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
/testendpoint: it only previews a payload and creates no run. - Collect the selected account's client ID and client secret. Set
HODOFLOW_BASE_URLas described in Base URLs, workspaces, and versions. - Use Bash, cURL, and
jqon 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
401with{"error":"missing_credentials"}: the bearer token is absent, invalid, or expired. Exchange once and retry once.401with{"error":"Unauthorized"}: the token was not issued for the exact selected service account, the organization differs, or connector auth failed. Correct configuration before retrying.403with{"error":"Insufficient scope"}: grantwebhooksto the selected account; retrying the same token cannot add a scope.404with{"error":"Trigger not found"}: verify the token-only URL and that the workflow is saved and active.413: reduce the body before retrying.429or503: waitRetry-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
- Add sender verification with API keys or HMAC signatures.
- Implement deduplication-aware retries.
- Review the exact webhook endpoint contract.