Webhooks
Have squiggli tell your server when documents are sent, viewed, signed, completed, declined, voided or expire.
Register a URL
curl https://squiggli.billy-965.workers.dev/v1/webhooks \
-H "Authorization: Bearer $SQUIGGLI_API_KEY" -H "Content-Type: application/json" \
-d '{ "url": "https://example.com/webhooks/squiggli", "events": ["envelope.completed", "envelope.declined"] }'The response includes a secret (whsec_...), shown once: store it. Leave out events to get all of them. A webhook made with an API key only gets envelopes made with that key; add "all_envelopes": true for every envelope in the business. Webhooks made in the web app get everything. URLs must be public https.
Events
| Event | When | data.signer |
|---|---|---|
envelope.sent | The envelope goes out. | No |
envelope.viewed | A signer opens it for the first time. | Yes |
envelope.signed | A signer signs (not necessarily the last). | Yes |
envelope.completed | Everyone has signed. Download links are included. | No |
envelope.declined | A signer declines. | Yes |
envelope.voided | The sender cancels it. | No |
envelope.expired | The deadline passes. | No |
The payload
{
"id": "evt_mw6c1k2z...",
"type": "envelope.completed",
"created_at": "2026-11-09T09:15:02.000Z",
"data": {
"envelope": {
"id": "env_mw6bdnhw48572v054y",
"title": "Harbour Lane lease, unit 4",
"status": "signed",
"metadata": { "job_id": "J-1042" },
"progress": { "signers": 1, "signed": 1 },
"signers": [{ "id": "sgn_...", "name": "Ava Chen", "status": "signed", "signed_at": "2026-11-09T09:15:01.000Z" }],
"files": {
"signed_pdf_download": "https://squiggli.billy-965.workers.dev/d/eyJ...",
"certificate_download": "https://squiggli.billy-965.workers.dev/d/eyJ..."
}
}
}
}The _download links work without a key for 7 days.
Check the signature
Every request has Squiggli-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your secret. Check it over the raw body, before parsing, and refuse anything older than 5 minutes.
Node (Express)
import crypto from 'node:crypto'
app.post('/webhooks/squiggli', express.raw({ type: 'application/json' }), (req, res) => {
const [t, v1] = (req.header('Squiggli-Signature') ?? '').split(',').map((p) => p.split('=')[1])
const expected = crypto.createHmac('sha256', process.env.SQUIGGLI_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
if (!fresh || !v1 || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.sendStatus(400)
const event = JSON.parse(req.body.toString())
// dedupe on event.id, then handle event.type
res.sendStatus(200)
})Workers, Deno, Bun and Next.js
async function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${parts.t}.${rawBody}`))
const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('')
return hex === parts.v1
}Python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts["v1"])PHP
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_SQUIGGLI_SIGNATURE']), $p);
$ok = abs(time() - (int)$p['t']) < 300 && hash_equals(hash_hmac('sha256', $p['t'] . '.' . $raw, $secret), $p['v1']);Retries
Answer with any 2xx within 10 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours (about 21 hours in all), then given up. Retries keep the same Squiggli-Event-Id header and id, so dedupe on it. Events can arrive out of order: if order matters, fetch the envelope.