Furrow Forms

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
[ hmac-sha256 ]
signature = sha256=hex(hmac_sha256(secret, timestamp + "." + rawBody))
Webhook delivery headers
HeaderValue
X-Furrow-Signaturesha256=<hex HMAC of the payload>
X-Furrow-TimestampUnix seconds at send time
X-Furrow-EventEvent name, e.g. submission.created
X-Furrow-DeliveryUnique delivery ID

Verify a delivery

verify.ts
[ node ]
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.