Data model — PaddleWebhookEvent
Idempotency record for every Paddle webhook delivery. Written before any processing so subsequent retries are detected and skipped. The actual subscription state changes are applied to the User model; this table is purely a dedup log.
Table: paddle_webhook_events
| Column | Type | Nullable | Description |
|---|---|---|---|
| id | bigint | no | Primary key |
| event_id | string | no | Paddle’s unique event ID — dedup key |
| event_type | string | no | Paddle event type (e.g. subscription.activated) |
| occurred_at | timestamp | yes | When the event occurred on Paddle’s side |
| processed_at | timestamp | yes | When this app finished handling the event (null if still pending) |
| user_id | bigint | yes | Resolved user (null if user not yet matched) |
| payload_summary | json | yes | Summarized event payload for admin inspection |
| created_at | timestamp | — | When the row was inserted |
| updated_at | timestamp | — | — |
Signature verification
All incoming Paddle webhooks are verified with HMAC-SHA256 before any database work begins. The occurred_at timestamp is compared against the server clock; requests older than 5 minutes are rejected as replay attacks. Signature verification happens in the controller before the event ID lookup.
Deduplication flow
After signature passes, the webhook handler looks up event_id in this table:
- Row not found → insert with
processed_at = null, process the event, then updateprocessed_at = now(). - Row found with
processed_atset → already handled; return200immediately (idempotent). - Row found with
processed_at = null→ in-flight (concurrent delivery); return200and let the original handler finish.
This guarantees each Paddle event is applied exactly once even under retry storms.
Paddle event types handled
🔴 [NEEDS CLARIFICATION: Confirm the full list of event types handled by PaddleWebhookController or its associated listener/service class. Known types based on billing integration: subscription.activated, subscription.updated, subscription.paused, subscription.resumed, subscription.canceled, transaction.completed, transaction.updated, transaction.payment_failed.]
Relations
This model has no Eloquent relations. user_id is a soft reference — it is populated when the event can be matched to a user, but it is not a foreign key constraint.
Related pages
- API — Billing —
POST /webhooks/paddleendpoint - Backend: Billing / Paddle — webhook controller and processing logic
- Data model: User — where subscription state is actually updated