Webhooks
Trigger workflows from external events, and receive a signed callback when a run finishes
/api/v1/webhooks/:workflowIdcurl -X POST https://www.prompteryx.ai/api/v1/webhooks/YOUR_WORKFLOW_ID \
-H "X-Webhook-Secret: your_workflow_secret" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "action": "process"}'Turn it on: add a Webhook Trigger node to the workflow in Visual Studio and save. Until then the URL answers 403 WEBHOOK_DISABLED.
Authentication is set on the trigger node: none, a shared secret in the X-Webhook-Secret header, HTTP Basic, a Bearer token, or a custom header. You can also require an HMAC signature from the sender (GitHub, Stripe, Slack presets or custom). The HTTP method is enforced too (POST by default).
Payload: the trigger's output is { body, headers, query, params, method, receivedAt }; later steps reference the JSON you sent through body (non-JSON bodies arrive as text).
Limits: 30 calls a minute per caller IP; each call counts as a workflow run against your plan (429 QUOTA_EXHAUSTED when the monthly allowance is used up).
Pass webhookUrl in POST /api/v1/workflows/:id/execute (or webhookUrl in the SDK's workflows.run). When the run ends Prompteryx POSTs the result there. The URL must be public http(s); private and internal addresses are refused.
What arrives
POST https://your-server.com/callback
Content-Type: application/json
X-Prompteryx-Event: workflow.completed
X-Prompteryx-Timestamp: 1790909344
X-Prompteryx-Signature-256: v1=4fed51e60e8bcdcae67c1f08a1f83b3059773198696cf568806509728097a7c9
{
"event": "workflow.completed", // or "workflow.failed"
"timestamp": "2026-10-02T02:49:04.006Z",
"data": {
"executionId": "api_1790909291145_o3dbvg",
"workflowId": "0QDHW2HTH3zRjHOtybJz",
"workflowName": "Daily price check",
"status": "completed", // or "failed"
"error": null,
"outputs": { "Extract Price": { "extractedText": "$99.99" } },
"duration": 204, // milliseconds
"startedAt": "2026-10-02T02:49:03.594Z",
"completedAt": "2026-10-02T02:49:04.006Z"
}
}outputs is the same node-name map as GET /api/v1/executions/:id. For the full per-node detail, call that endpoint with the executionId.
- Create your signing secret on the API Keys page. It starts with
whsec_and is shown once. Rotating it there replaces it immediately. - Every callback carries
X-Prompteryx-Timestamp(unix seconds) andX-Prompteryx-Signature-256: v1=<hex>, where the hex is HMAC-SHA256 of"<timestamp>.<raw body>"keyed with your secret. - Recompute it over the raw body, compare in constant time, and refuse timestamps more than 5 minutes from your clock.
With the SDK
import { constructWebhookEvent } from '@prompteryx/sdk' // 0.5.0+
app.post('/prompteryx-callback', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const event = await constructWebhookEvent({
secret: process.env.PROMPTERYX_WEBHOOK_SECRET, // whsec_…
body: req.body.toString('utf8'),
headers: req.headers,
})
console.log(event.event, event.data.executionId, event.data.outputs)
res.sendStatus(204)
} catch {
res.sendStatus(401)
}
})Plain Node.js
const crypto = require('crypto')
// rawBody: the request body as a string, BEFORE JSON parsing.
function verifyPrompteryxCallback(rawBody, headers, secret /* whsec_… */) {
const ts = headers['x-prompteryx-timestamp']
const sig = headers['x-prompteryx-signature-256'] || ''
if (!ts || !/^\d+$/.test(ts)) return false
// Refuse anything older (or newer) than 5 minutes: replay protection.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex')
// During a secret rotation the header can hold several v1= values.
return sig.split(',').map((s) => s.trim()).filter((s) => s.startsWith('v1=')).some((s) => {
const got = Buffer.from(s.slice(3))
const want = Buffer.from(expected)
return got.length === want.length && crypto.timingSafeEqual(got, want)
})
}Old header is going away
X-Prompteryx-Signature header (HMAC keyed with the execution id) plus X-Prompteryx-Signature-Deprecated: true for one more release. It is not secret, because the id is in the body, so don't rely on it; switch to X-Prompteryx-Signature-256. Until you create a signing secret, callbacks carry only the old header. Callbacks sent by the desktop app (local runs) also carry only the old header for now.Always verify
executionId to make your handler idempotent. A delivery is attempted once and is not retried.