Skip to content

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.)

Terminal window
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.

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.

  • 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.
Terminal window
curl "$URL/api/v1/admin/webhooks/order-created/deliveries?limit=20" \
-H "authorization: Bearer $ADMIN_KEY"

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.

Terminal window
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.

Terminal window
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.