Webhooks
A webhook is the no-code way out: a collection plus a set of events, and Groveback POSTs a signed payload to your URL. (For calls your code initiates, use an integration instead.)
Create one
Section titled “Create one”curl -X POST "$URL/api/v1/admin/webhooks" \ -H "authorization: Bearer $ADMIN_KEY" -H 'content-type: application/json' \ -d '{ "name": "order-created", "collection": "orders", "events": ["insert", "update"], "url": "https://example.com/hooks/groveback", "headers": { "X-Tenant": "acme" }, "enabled": true }'An update subscription also fires on replace.
Gated on webhooks:manage.
Verifying a delivery
Section titled “Verifying a delivery”Each request carries:
X-Groveback-Signature: t=<unix ms>,v1=<hex HMAC-SHA256>The signed payload is "<t>.<rawBody>". Verify with a timing-safe compare and a timestamp
tolerance:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, header, secret, toleranceMs = 5 * 60 * 1000) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); const t = Number(parts.t); if (!Number.isFinite(t) || Math.abs(Date.now() - t) > toleranceMs) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest(); const got = Buffer.from(parts.v1, 'hex'); return expected.length === got.length && timingSafeEqual(expected, got);}Use the raw body, not a re-serialized object — key order would change the hash.
Delivery semantics
Section titled “Delivery semantics”- Up to 3 attempts, with 1 s and 5 s backoff.
- A 10 s timeout per attempt.
- Any 2xx counts as success.
- Exactly one delivery-log entry per event, recording the final outcome and the attempt count.
- The log is capped at 50 entries per webhook.
curl "$URL/api/v1/admin/webhooks/order-created/deliveries?limit=20" \ -H "authorization: Bearer $ADMIN_KEY"A dead receiver never affects your writes
Section titled “A dead receiver never affects your writes”The change listener only enqueues. Delivery failures become log entries, never exceptions in the write path — so a receiver being down cannot make your API start rejecting documents.
Error messages are sanitized to request failed or request timed out. The failure cause of
a URL is never echoed back into the admin API.
Definitions are re-read per delivery, so a webhook deleted or disabled between the event and the drain is silently dropped.
With REDIS_URL configured, pending deliveries survive a restart. Without it, undelivered
events are lost with the process.
Testing
Section titled “Testing”curl -X POST "$URL/api/v1/admin/webhooks/order-created/test" \ -H "authorization: Bearer $ADMIN_KEY"Sends a signed delivery with event "test" immediately and returns the delivery record —
the fastest way to check your signature verification.
Rotating the secret
Section titled “Rotating the secret”curl -X POST "$URL/api/v1/admin/webhooks/order-created/rotate-secret" \ -H "authorization: Bearer $ADMIN_KEY"The new secret is returned once and the old one stops verifying immediately. Plan a brief window where your receiver accepts both.
Config import never produces a live unsigned webhook
Section titled “Config import never produces a live unsigned webhook”An imported webhook arrives disabled with no secret, and enabling it requires a secret on file. A shape export can therefore never smuggle in a live outbound sender — a property worth knowing when you promote config between environments.