Webhooks
Every delivery is signed, retried, and logged — on every tier. Webhooks are configured on the project (forms can override with their own URL), and one call totest_webhook verifies the wiring before you go live.
Signature scheme
signature = sha256=hex(hmac_sha256(secret, timestamp + "." + rawBody))| Header | Value |
|---|---|
| X-Furrow-Signature | sha256=<hex HMAC of the payload> |
| X-Furrow-Timestamp | Unix seconds at send time |
| X-Furrow-Event | Event name, e.g. submission.created |
| X-Furrow-Delivery | Unique delivery ID |
Verify a delivery
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret: string, rawBody: string, headers: Headers): boolean {
const timestamp = headers.get('X-Furrow-Timestamp')!;
const received = headers.get('X-Furrow-Signature')!; // "sha256=<hex>"
// Reject stale timestamps to prevent replays (e.g. older than 5 minutes)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected =
'sha256=' +
createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}Delivery guarantees
- [ RETRY ]Failed deliveries retry automatically with backoff — up to 8 attempts over ~24 hours.
- [ LOG ]A full delivery log per project: status, attempts, and response codes for every delivery.
- [ TEST ]One call to
test_webhook(MCP or REST) fires a signed test delivery so you — or your agent — can verify wiring before going live. - [ SCOPE ]Webhooks are project-level and cascade to every form; a form can override with its own URL when one destination differs.