Webhook Calls
Document information
| Field | Value |
|---|---|
| Canonical URL | /docs/03_writing-step-code/30_webhook-calls |
| Version (published date) | 2026-05-08 |
| Tags | code-snippets, webhooks, reference |
Summary
This guide covers reliable outbound webhook calls from ProcessFlow JavaScript snippets. It includes safe URL validation, authentication headers, payload signing, retry behavior for transient failures, and sequential batch dispatch patterns. Use these recipes when your process needs to notify or synchronize external systems.
Start with a safe webhook baseline
A stable webhook step validates destination input first, then sends a minimal JSON payload with explicit metadata such as timestamp and source identifier. This keeps outbound calls easier to debug and trace across systems. A predictable response shape also makes downstream process routing simpler.
const webhookUrl = process_input?.result?.webhook_url ?? "";
const payload = process_input?.result?.payload ?? {};
const headers = process_input?.result?.headers ?? {};
if (!webhookUrl) {
return { success: false, error: "webhook_url is required" };
}
if (!/^https?:\/\//i.test(webhookUrl)) {
return { success: false, error: "webhook_url must start with http:// or https://" };
}
const requestPayload = {
...payload,
timestamp: new Date().toISOString(),
source: "processflow",
};
const response = await api.post(webhookUrl, requestPayload, {
"Content-Type": "application/json",
...headers,
});
return {
success: Boolean(response?.success),
data: {
status_code: response?.status_code ?? null,
response: response?.data ?? null,
webhook_url: webhookUrl,
},
};
Build authentication headers consistently
Webhook destinations typically require bearer, basic, or API-key authentication. Construct auth headers in one place so calling code stays simple and security review is easier. Keeping a normalized header builder reduces accidental leakage and inconsistent naming across steps.
type AuthInput = {
authType: "bearer" | "basic" | "api_key";
authValue: string;
apiKeyHeader?: string;
};
function buildAuthHeaders(input: AuthInput): Record<string, string> {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (input.authType === "bearer") headers.Authorization = `Bearer ${input.authValue}`;
if (input.authType === "basic") headers.Authorization = `Basic ${Buffer.from(input.authValue).toString("base64")}`;
if (input.authType === "api_key") headers[input.apiKeyHeader ?? "X-API-Key"] = input.authValue;
return headers;
}Add signing for trusted receivers
For destinations that verify request integrity, include a deterministic signature header built from the outbound payload and shared secret. Include a timestamp header so receivers can enforce replay-window validation. This pattern is especially useful for high-value event streams.
const webhookUrl = process_input?.result?.webhook_url ?? "";
const payload = process_input?.result?.payload ?? {};
const secretKey = process_input?.result?.secret_key ?? "";
if (!webhookUrl || !secretKey) {
return { success: false, error: "webhook_url and secret_key are required" };
}
const signedPayload = {
...payload,
timestamp: Date.now(),
nonce: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
};
const payloadString = JSON.stringify(signedPayload);
const signature = tf.utils.sha256(`${secretKey}:${payloadString}`);
const response = await api.post(webhookUrl, signedPayload, {
"Content-Type": "application/json",
"X-Webhook-Signature": `sha256=${signature}`,
"X-Webhook-Timestamp": String(signedPayload.timestamp),
});
return {
success: Boolean(response?.success),
data: { status_code: response?.status_code ?? null, signature },
};
Retry and batch delivery patterns
Retries should be limited to transient errors such as timeouts, rate limits, and 5xx responses. Exponential backoff reduces pressure on target systems while improving delivery success. Always capture attempt history so failed events can be replayed safely later.
For multi-destination fan-out, sequential batch execution keeps ordering deterministic and error handling simple. Use continue_on_error when best-effort delivery is acceptable, and stop-on-error for workflows where partial delivery is risky. Both modes should return consistent per-target results.
const webhooks = process_input?.result?.webhooks ?? [];
const payload = process_input?.result?.payload ?? {};
const continueOnError = process_input?.result?.continue_on_error ?? true;
const retryableCodes = new Set([408, 429, 500, 502, 503, 504]);
if (!Array.isArray(webhooks) || webhooks.length === 0) {
return { success: false, error: "webhooks array is required" };
}
const results = [];
for (const [index, item] of webhooks.entries()) {
const url = typeof item === "string" ? item : item?.url ?? "";
const headers = typeof item === "string" ? {} : item?.headers ?? {};
const name = typeof item === "string" ? `Webhook ${index + 1}` : item?.name ?? `Webhook ${index + 1}`;
if (!url) {
results.push({ name, success: false, error: "missing URL" });
if (!continueOnError) break;
continue;
}
let delivered = false;
let lastError = "unknown error";
const attempts = [];
for (let attempt = 1; attempt <= 3; attempt += 1) {
const response = await api.post(url, payload, headers);
const statusCode = Number(response?.status_code ?? 0);
attempts.push({ attempt, status_code: statusCode, success: Boolean(response?.success) });
if (response?.success && statusCode < 400) {
delivered = true;
break;
}
lastError = `HTTP ${statusCode || "unknown"}`;
if (!retryableCodes.has(statusCode)) break;
await new Promise((resolve) => setTimeout(resolve, 500 * Math.pow(2, attempt - 1)));
}
results.push({ name, url, success: delivered, error: delivered ? null : lastError, attempts });
if (!delivered && !continueOnError) break;
}
return {
success: results.every((entry) => entry.success),
data: {
total: webhooks.length,
succeeded: results.filter((entry) => entry.success).length,
failed: results.filter((entry) => !entry.success).length,
results,
},
};
Final checklist
Use this checklist before deploying webhook steps:
- Destination URLs are validated and restricted to expected schemes.
- Auth headers are built consistently from explicit input fields.
- Signature logic includes timestamp metadata where required.
- Retries are limited to transient failure categories.
- Batch mode behavior (
continue_on_error) matches business requirements. - Logs include diagnostics but exclude secrets and raw credentials.