Skip to content

Status store

Status store

With postal.webhooks.store enabled (the default), the webhook processor maintains two tables. The store is entirely optional — with both store flags disabled the package registers no migrations and never touches the database.

postal_messages — one row per message

Keyed unique on (server, postal_message_id). Holds the latest known delivery status (+ status_details), envelope fields (to, from, subject, tag, message_id, token), opens / clicks counters, the last_event + last_event_at, and an optional notifiable morph set by the notification channel.

Query via the PostalMessage model:

use Cbox\LaravelPostal\Models\PostalMessage;

PostalMessage::query()->forServer('cbox-billing')->bounced()->get();
PostalMessage::query()->delivered()->where('tag', 'onboarding')->count();

Scopes: forServer(), delivered(), bounced(), failed(), held().

postal_message_events — the deduplicated log

Every processed delivery is appended with its payload and a unique dedupe_key. php artisan postal:tail follows this table.

Idempotency

Postal retries webhook deliveries with backoff until it sees a 2xx, so the same delivery can arrive more than once. Every webhook body carries a uuid per webhook request; the processor inserts {server}:{uuid} as a unique dedupe_key — a duplicate insert means redelivery, and the processor stops: no counter is incremented twice and no event is dispatched twice. Bodies without a uuid fall back to a content hash; inbound messages dedupe on their Postal message id.

Processing is transactional: the dedupe insert, the status-row update and the event dispatch commit together. If anything fails mid-way, the dedupe marker rolls back and the redelivery retries cleanly — a crash can never permanently swallow a delivery. Status rows are created with insertOrIgnore and mutated under a row lock, so concurrent workers on the same message cannot double-create rows or lose open/click counts.

With the store disabled there is no dedupe state, so redeliveries dispatch the event again — listeners must then tolerate duplicates themselves (uuid() on every event exists for exactly that).