These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

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

ColumnTypeNullableDescription
idbigintnoPrimary key
event_idstringnoPaddle’s unique event ID — dedup key
event_typestringnoPaddle event type (e.g. subscription.activated)
occurred_attimestampyesWhen the event occurred on Paddle’s side
processed_attimestampyesWhen this app finished handling the event (null if still pending)
user_idbigintyesResolved user (null if user not yet matched)
payload_summaryjsonyesSummarized event payload for admin inspection
created_attimestampWhen the row was inserted
updated_attimestamp

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:

  1. Row not found → insert with processed_at = null, process the event, then update processed_at = now().
  2. Row found with processed_at set → already handled; return 200 immediately (idempotent).
  3. Row found with processed_at = null → in-flight (concurrent delivery); return 200 and 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.