Verify webhook signatures
Every delivery includes:
Elicitra-Signature: t=<unix-seconds>,v1=<hex-hmac>The signature is HMAC-SHA256 over:
<timestamp>.<raw-request-body>Use the raw bytes before JSON parsing, reject timestamps more than five minutes from your clock, and compare in constant time. For every CloudEvent delivery, deduplicate the id in durable storage. Endpoint verification uses the same signature but sends the non-CloudEvent JSON body { "challenge": "..." }, so return 2xx and record the challenge for entry in Elicitra instead of applying CloudEvent deduplication. Framework JSON middleware must not run before signature verification.
During secret rotation the header contains two v1 values for up to 24 hours. Accept a delivery when any current signature matches; retire the previous secret after the overlap.
TypeScript / Node.js
Section titled “TypeScript / Node.js”Download the executable TypeScript example. It tests a valid first delivery, an idempotent duplicate, tampered bytes, an expired timestamp, dual-signature rotation, and signed malformed JSON.
const result = await acceptElicitraWebhook({ rawBody: requestBodyBuffer, signatureHeader: request.headers["elicitra-signature"] ?? "", secrets: previousSecret ? [currentSecret, previousSecret] : [currentSecret], validateEventData: validateAgainstPinnedV1JsonSchema, exposeChallenge: saveChallengeForEndpointActivation, persistEventOnce: async ({ id, rawBody }) => { const inserted = await db.query( `INSERT INTO elicitra_webhook_inbox (event_id, raw_body) VALUES ($1, $2) ON CONFLICT (event_id) DO NOTHING RETURNING event_id`, [id, rawBody], ); return inserted.rowCount === 1; },});
response.status(result.status).end();Python
Section titled “Python”Download the executable Python example. It runs the same acceptance and deduplication tests with only the standard library.
result = accept_elicitra_webhook( raw_body=request.get_data(cache=False, as_text=False), signature_header=request.headers.get("Elicitra-Signature", ""), secrets=[secret for secret in (current_secret, previous_secret) if secret], validate_event_data=validate_against_pinned_v2_json_schema, persist_event_once=insert_inbox_row_on_conflict_do_nothing, expose_challenge=save_challenge_for_endpoint_activation,)
return "", result["status"]Compile the exact JSON Schema 2020-12 document selected by the allowlisted event type and dataschema; reject the complete envelope or data before persistence if validation fails. The database table must have a unique constraint on event_id. The insert, accepted raw body (or durable work item), and any required inbox metadata must commit before you return 2xx. Do not use a race-prone SELECT followed by INSERT.
A duplicate returns 2xx without enqueuing the business action again. A later worker can perform slow CRM work without causing unnecessary Elicitra retries. Validate the parsed CloudEvent and its referenced v6 JSON Schema before applying business side effects.