Webhooks
Receive HTTPS POST callbacks when a job's status changes instead of polling GET /v1/jobs/{jobId}. Configure a default webhook URL on the project (or override per job at create time). Webhooks work without a signing secret; for production integrations, rotate a secret and verify signatures on your server so only Perchat can trigger your handler.
See also Jobs for job creation and the public Job object shape included in webhook payloads.
Events
Perchat sends a webhook for each meaningful job status transition:
queued— job entered the queue (pending→queuedon first dispatch only; internal retry re-queues do not emit).processing— a worker claimed the job.cancelling— cancel requested while the job was processing.completed,failed,cancelled— terminal outcomes.failedis sent only after retries are exhausted, not on intermediate worker failures that will retry.test— synthetic payload fromPOST /v1/project-webhooks/test(not a job lifecycle event).
pending and internal worker retry transitions are not delivered as webhooks.
Callback request
Perchat POSTs JSON to your configured HTTPS URL. The body matches:
event— event name (see above).timestamp— ISO-8601 time when the event was emitted.job— public job snapshot (same fields asGET /v1/jobs/{jobId}), includinguserMetadata.customerJobRefwhen set.
Request headers:
X-Perchat-Event— duplicatesevent.X-Perchat-Delivery-Id— delivery id (whd_…) for idempotency and support.X-Perchat-Timestamp— Unix seconds used for signing.X-Perchat-Signature— present only when a project signing secret is configured (see Signing secret). Format:t={unix},v1={hex}.
Respond with any 2xx status to acknowledge delivery. Non-2xx responses trigger retries (see below).
Signing secret
A signing secret is optional. Webhooks are delivered as soon as you configure an HTTPS webhookUrl — you do not need a secret to receive callbacks or to use POST /v1/project-webhooks/test.
X-Perchat-Event, X-Perchat-Delivery-Id, and X-Perchat-Timestamp. There is no X-Perchat-Signature header. That is fine for local smoke tests (e.g. inspecting requests in a public inbox), but anyone who discovers your URL could send fake job events to your endpoint.Why rotate a signing secret in production? Your webhook URL is a public HTTPS endpoint. Without verification, an attacker could POST forged completed events and trick your systems into treating jobs as done, fetching the wrong artifacts, or updating the wrong records. The secret lets your server prove each callback was signed by Perchat before you act on job data.
Setup
- Call
POST /v1/project-webhooks/rotate-secret(or use the dashboard Rotate signing secret button). - Copy
data.webhookSecretimmediately — it is shown once. Later reads return onlyhasWebhookSecretandwebhookSecretPreview(last four characters). - Store the secret on your server (environment variable, secrets manager). Do not paste it into third-party inbox tools — they cannot verify signatures for you; only your application should hold the secret.
- Implement verification on your webhook route before processing the JSON body (see Verify signatures).
Rotating a secret invalidates the previous secret for new deliveries. Update your server config before or immediately after rotation to avoid rejecting live traffic.
Verify signatures
When a signing secret is configured, every delivery includes X-Perchat-Signature. Your handler must verify it using the same secret you stored at rotation time. If verification fails, respond with a non-2xx status (typically 401) and do not trust the payload — Perchat will retry until you fix verification or the delivery exhausts attempts.
Algorithm
- Read
X-Perchat-Timestamp(Unix seconds) and the raw request body as received on the wire (before JSON parsing). - Build the signed string:
timestamp + "." + rawBody(UTF-8). - Compute
HMAC-SHA256(webhookSecret, signedString)and encode as lowercase hex. - Parse
X-Perchat-Signature(formatt={unix},v1={hex}) and compare your digest to thev1=value using a constant-time comparison.
express.raw({ type: 'application/json' }), Flask request.get_data() before request.json).Recommended checks
- Reject requests missing
X-Perchat-Signaturewhen your server expects a secret to be configured. - Optionally reject timestamps too far from the current time (e.g. > 5 minutes) to limit replay of captured requests.
- Use
X-Perchat-Delivery-Idfor idempotency — store processed delivery ids so retries do not double-apply side effects. - Respond with any
2xxonly after verification succeeds.
Node.js
import crypto from 'node:crypto'
const secret = process.env.PERCHAT_WEBHOOK_SECRET // from rotate-secret (shown once)
export function verifyPerchatWebhook(req: {
headers: Record<string, string | string[] | undefined>
rawBody: Buffer | string
}): boolean {
const timestamp = String(req.headers['x-perchat-timestamp'] ?? '')
const sigHeader = String(req.headers['x-perchat-signature'] ?? '')
if (!timestamp || !sigHeader) return false
let v1 = ''
for (const part of sigHeader.split(',')) {
const trimmed = part.trim()
if (trimmed.startsWith('v1=')) v1 = trimmed.slice(3)
}
if (!v1) return false
const rawBody = typeof req.rawBody === 'string' ? req.rawBody : req.rawBody.toString('utf8')
const signedPayload = `${timestamp}.${rawBody}`
const expected = crypto.createHmac('sha256', secret).update(signedPayload, 'utf8').digest('hex')
try {
return (
expected.length === v1.length &&
crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'))
)
} catch {
return false
}
}
// Express: use express.raw({ type: 'application/json' }) so req.body is the raw bytes.
// Reject with 401 when verifyPerchatWebhook returns false; respond 2xx only after verification.Python
import hashlib
import hmac
import os
SECRET = os.environ["PERCHAT_WEBHOOK_SECRET"].encode("utf-8")
def verify_perchat_webhook(headers, raw_body: bytes) -> bool:
timestamp = headers.get("X-Perchat-Timestamp", "")
sig_header = headers.get("X-Perchat-Signature", "")
v1 = None
for part in sig_header.split(","):
part = part.strip()
if part.startswith("v1="):
v1 = part[3:]
if not timestamp or not v1:
return False
signed_payload = f"{timestamp}.{raw_body.decode('utf-8')}"
expected = hmac.new(SECRET, signed_payload.encode("utf-8"), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
# Use request.get_data() before parsing JSON. Return 401 when verification fails.Delivery and retries
Delivery is asynchronous. Failed attempts (network error, timeout, or non-2xx HTTP status) are retried up to maxAttempts (5) with exponential backoff: approximately 1 min, 5 min, 15 min, 1 hour, then 4 hours between attempts.
Inspect delivery history via GET /v1/project-webhooks/deliveries or GET /v1/jobs/{jobId}/webhook-deliveries. Each record includes attempt count, HTTP status, error summary, and next retry time.
Which URL is used
- If the job was created with
webhookUrl, that URL is used for all events on that job. - Otherwise Perchat uses the project default from
PATCH /v1/project-webhooks/settings. - If neither is set, no webhooks are sent for that job.
All webhook URLs must use HTTPS.
Project webhook settings
- webhookUrlstring | null, optional
- Default HTTPS callback URL for jobs in this project.
- hasWebhookSecretboolean
- True when a signing secret is stored on the project.
- webhookSecretPreviewstring | null, optional
- Last four characters of the secret for confirmation (never the full secret).
The Webhook delivery object
- deliveryIdstring
- Stable delivery id (whd_…).
- eventstring
- Event type delivered (or test).
- targetUrlstring
- URL that received (or will receive) the POST.
- statusstring
pending,delivering,succeeded,failed, orretrying.- attemptCountinteger
- Delivery attempts so far.
- maxAttemptsinteger
- Maximum attempts before marking failed.
- lastHttpStatusinteger | null, optional
- HTTP status from the most recent attempt.
- lastErrorSummarystring | null, optional
- Short error from the most recent failed attempt.
Webhook callback payload
- eventstring
- Status event name.
- timestamptimestamp
- When the event was emitted.
- jobobject
- Public
Jobobject — same shape asGET /v1/jobs/{jobId}.
Retrieve webhook settings
/v1/project-webhooks/settingssk_prj_…) that includes webhook:read and/or webhook:update, or with a dashboard user session (Authorization: Bearer <jwt>) plus X-Workspace-Id and X-Project-Id and matching workspace role permissions. Workspace API keys cannot access project webhook endpoints.Requires permission
webhook:read
Update webhook settings
/v1/project-webhooks/settingswebhookUrl: null or an empty string to remove it. Authenticate with a project API key (sk_prj_…) that includes webhook:read and/or webhook:update, or with a dashboard user session (Authorization: Bearer <jwt>) plus X-Workspace-Id and X-Project-Id and matching workspace role permissions. Workspace API keys cannot access project webhook endpoints.Requires permission
webhook:update
Parameters
- webhookUrlstring | null, optional
- HTTPS URL for job status callbacks. Must use https://.
Rotate signing secret
/v1/project-webhooks/rotate-secretdata.webhookSecret — store it on your server immediately; Perchat never shows it again. After rotation, new deliveries include X-Perchat-Signature; your handler must verify it (see Verify signatures) before trusting the payload. Rotating invalidates the previous secret for new deliveries. Authenticate with a project API key (sk_prj_…) that includes webhook:read and/or webhook:update, or with a dashboard user session (Authorization: Bearer <jwt>) plus X-Workspace-Id and X-Project-Id and matching workspace role permissions. Workspace API keys cannot access project webhook endpoints.Requires permission
webhook:update
Send test webhook
/v1/project-webhooks/testtest event to the configured project webhookUrl. Requires a default URL; returns deliveryId for tracking in the delivery log. Authenticate with a project API key (sk_prj_…) that includes webhook:read and/or webhook:update, or with a dashboard user session (Authorization: Bearer <jwt>) plus X-Workspace-Id and X-Project-Id and matching workspace role permissions. Workspace API keys cannot access project webhook endpoints.Requires permission
webhook:update
List project webhook deliveries
/v1/project-webhooks/deliveriessk_prj_…) that includes webhook:read and/or webhook:update, or with a dashboard user session (Authorization: Bearer <jwt>) plus X-Workspace-Id and X-Project-Id and matching workspace role permissions. Workspace API keys cannot access project webhook endpoints.Requires permission
webhook:read
Query parameters
- pageinteger, optional
- Page number (default 1).
- limitinteger, optional
- Page size (default 20, max 100).
List job webhook deliveries
/v1/jobs/{jobId}/webhook-deliveriessk_prj_…) with job:read, or a user session with X-Workspace-Id and X-Project-Id.Requires permission
job:read
Query parameters
- pageinteger, optional
- Page number (default 1).
- limitinteger, optional
- Page size (default 20, max 100).