Webhooks
Subscribe to item lifecycle events. Each delivery is signed with HMAC-SHA256 using a secret you only see once at create time.
Events
item.createditem.updateditem.completed/item.uncompleteditem.archived/item.deleted
Register
Pro only. Free returns 402 pro_requiredon create. Listing existing webhooks works on any plan but free users won’t have any to list.
The url must be public HTTPS. Localhost, RFC1918 (10/8, 172.16/12, 192.168/16), CGNAT (100.64/10), link-local, loopback, and any .local / .internal hostname are rejected to prevent SSRF.
/api/v1/webhooksBody: { url, events[], description? }. Returns { webhook, secret } — store the secret immediately.
curl -X POST https://quik.md/api/v1/webhooks \
-H "Authorization: Bearer qk_..." \
-H "Content-Type: application/json" \
-d '{
"url":"https://hooks.example.com/quik",
"events":["item.created","item.completed"]
}'List & delete
/api/v1/webhooks/api/v1/webhooks/:idVerify the signature
Each delivery includes X-Quik-Event and X-Quik-Signature: sha256=<hex>. Compare against the SHA-256 hash of the secret HMAC’d over the raw body.
import crypto from "node:crypto"
function verify(secret, rawBody, header) {
const [, sig] = (header ?? "").split("=")
// We sign with the SHA-256 hash of the secret as the HMAC key.
const key = crypto.createHash("sha256").update(secret).digest("hex")
const expected = crypto
.createHmac("sha256", key)
.update(rawBody)
.digest("hex")
return crypto.timingSafeEqual(
Buffer.from(sig, "hex"),
Buffer.from(expected, "hex"),
)
}Note: we sign with HMAC-SHA256 keyed on the SHA-256 hash of your secret. We never store the raw secret server-side.
Delivery contract
Best-effort, three attempts (immediate, +1s, +5s). If your endpoint returns 2xx within the timeout we stop. Otherwise the last status is recorded on the webhook row. We don’t guarantee ordering — use updated_at on the item to break ties.