Hooks are the platform's outbound feed: when a store's data changes, the backend POSTs the changed document to the endpoints the store subscribed — signed, self-describing, at-least-once. Render servers use it to keep caches warm; integrations use it for orders, invoices and form submissions.
Subscribing#
Hooks live on the store's setup/hooks document, edited in CRM & Studio under Settings › Advanced › Hooks. The standard form is one multi-event entry: an endpoint, a signing key and an events list.
events |
Meaning |
|---|---|
['orders', 'invoices'] |
exactly those kinds |
server |
the render-server group — data, files, templates, elements, setup, slug, services, bulk (never the PII kinds) |
| absent / empty | every kind, current and future (CRM & Studio refuses to create a bare wildcard) |
An entry has a type: callback (the default — POSTs the document) or notification (renders a store e-mail template and mails it to the entry's emails through the store's SMTP setup — or the platform mailer when the store has none; no endpoint, nothing signed). Both gate on enabled and on on: ['create' | 'update' | 'delete'] (absent = callbacks fire on all three, notifications on create only). Optional filter and mask shape what is sent.
Event kinds#
| Kind | Payload | Seed label |
|---|---|---|
orders |
the order document (masked) | orders/{id} |
invoices |
the invoice document (masked) | invoices/{id} |
data |
a localized data document (masked) | {locale}/{collection}/{id} |
forms |
a form record (masked) | forms/{form}/{id} |
files |
a file document | files/{storage_path} |
templates |
a signal {schemaId, version} |
templates/{schemaId}/v{n} |
elements |
a signal {id} |
elements/{id} |
setup |
a signal {id} — one of the public setup docs web / data / design / elements, or $store when the store's locales / name change |
setup/{id} |
slug |
a signal {key, collection?, locale?} — a slug registry entry (root, or a collection's route registry) |
slug/{key} · slug/{collection}/{key} |
bulk |
a signal {source: 'restore' | 'sync', job, parts, counts, at, backup?, env?} — many documents changed at once (a backup restore or an environment sync); no per-document push follows, so refetch everything you cache |
bulk/{job} |
templates, elements, setup, slug and bulk exist only inside the server group: signals that tell a render server what to forget, never a document body. Deletes push an {id} tombstone with x-drop-deleted: true. Private setup documents (mail, hooks, analytics, shipping, …) and the carts collection are never pushed — not even as a signal. A data document's push carries its schema stamps (_dt / _f); see Caching & freshness.
Delivery headers#
Every callback carries x-drop-* headers so a receiver can dispatch without parsing the body:
| Header | Meaning |
|---|---|
x-drop-event |
the kind (orders, data, …) |
x-drop-updated |
the document's Firestore update time (epoch ms) — the ordering guard: drop a push that is not newer than what you hold |
x-drop-locale, x-drop-collection, x-drop-id |
data context (id also on orders / invoices / forms / elements / setup / slug; collection + locale on slug) |
x-drop-form · x-drop-schema + x-drop-version · x-drop-storage-path |
forms · templates · files |
x-drop-deleted: true |
a tombstone |
access-seed |
the signature (below) |
Deliveries are awaited with a 10 s timeout per hook and not retried — answer quickly and reconcile from the API if you missed one.
Verifying a delivery#
When the hook has a private key, the access-seed header is <ts>.<signature> where signature = HMAC-SHA256(ts + '.' + label, key) and label is the per-push label from the table above, reconstructed from the x-drop-* headers. Reject old timestamps and mismatching signatures.
import {verifyHookSeed} from 'drop-core-sdk';
app.post('/hook', (req, res) => {
const ok = verifyHookSeed(req.headers, process.env.HOOK_KEY!, {
label: `${req.headers['x-drop-locale']}/${req.headers['x-drop-collection']}/${req.headers['x-drop-id']}`,
});
if (!ok) return res.status(401).end();
// …apply req.body
res.json({ok: true});
});The SDK's Secure page documents verifyHookSeed; drop-core-server ships a complete receiver (WebSite.receiveEventHook) that verifies, applies the ordering guard and warms its caches.
A header named with an underscore (
access_seed) is still sent for old receivers, but nginx drops underscore headers by default — verifyaccess-seed.