Back to blog

Reliable Laravel Webhooks: Signatures, Queues, Retries, and Idempotency

Manuk Minasyan · · 4 min read
Reliable Laravel Webhooks: Signatures, Queues, Retries, and Idempotency

A queue does not guarantee delivery, and a signature does not prevent duplicate processing. Reliable webhooks need a persistent sender, a versioned authentication contract, bounded retries, and a receiver that can accept the same logical event more than once.

If you need the product-level setup first, start with FilaForms webhook concepts. This guide focuses on the implementation on both sides.

Write an outbox in the submission transaction

Store the submission and logical webhook event in one database transaction. The outbox row should include a stable event_id, type/version, destination, payload or reconstructable reference, state, attempt count, and next-attempt time.

Dispatch after commit. Also run a recovery scheduler that finds committed pending rows whose queue dispatch was lost. A queue message alone is not a durable audit of delivery.

Keep the same event_id across retries and manual redrives. Generate a separate attempt_id for each network request. A changing attempt ID cannot deduplicate repeated delivery of the same business event.

Sign exact raw bytes

Serialize the body once, then sign exactly what you send. Do not decode and re-encode JSON during verification because whitespace and key-order changes alter bytes.

A versioned message can be:

v1.{unix_timestamp}.{raw_body}

Compute an HMAC-SHA256 with the endpoint secret and send a structured header containing version, timestamp, and signature. The receiver verifies the supported version, checks timestamp tolerance, recomputes the HMAC over the raw body, and compares in constant time.

GitHub's webhook validation guide demonstrates raw-payload verification. HMAC authenticates integrity; TLS still supplies transport confidentiality and server authentication.

Add replay protection and rotation

A captured signed body remains valid unless the receiver checks freshness or stable event identity. Reject timestamps outside a documented tolerance and insert the event ID into an inbox table with a unique index.

For secret rotation, sign with the new secret and verify against current/previous secrets for a bounded overlap. Never log either secret or return it in error details.

Retry only retryable failures

Use short connection and total timeouts. Retry network errors, timeouts, 408, 429, and most 5xx responses. Honor a valid Retry-After where practical. Treat most other 4xx responses as configuration/payload failures and move them to an operator-visible failed state.

Use exponential backoff with jitter and a maximum attempt/time horizon. Record status, duration, error class, body hash, and a bounded/redacted response excerpt—not raw PII payloads.

Laravel's HTTP client retry API and queue backoff controls are mechanisms; the outbox state remains the delivery record.

Defend user-configured destinations

Webhook URLs create SSRF risk. Require HTTPS in production, reject loopback/private/link-local/cloud-metadata addresses for IPv4 and IPv6, disable redirects or revalidate each destination, and protect against DNS changes between validation and connection.

An egress proxy with network-level restrictions is stronger than string validation alone. Apply response-size limits so a destination cannot return an unbounded body.

Build a fast idempotent receiver

The receiving route should:

  1. read the raw body;
  2. verify timestamp and signature;
  3. validate event type/version;
  4. atomically insert the stable event ID into an inbox with a unique index;
  5. persist or dispatch work;
  6. return 2xx quickly.

If the event ID already exists, return success without repeating the effect. Webhooks can arrive twice or out of order; use object versions/timestamps or fetch authoritative state when the source supports it.

Failure tests that create confidence

Do not promise “exactly once.” The defensible contract is durable at-least-once attempts plus an idempotent receiver and transparent failure state. That is both more honest and more operable.

Related posts