Webhooks

Updated

Paddle notifies the store through a single REST endpoint. Authentication is the signature, never the request’s origin.

Endpoint

POST /wp-json/wpinterface-paddle/v1/webhook (Webhooks::handle, public permission_callback). With the wpinterface_paddle_webhook_base_url filter set, the URL becomes {base}?rest_route=/wpinterface-paddle/v1/webhook.

ResponseMeaning
200 OK / Already processedHandled or deduplicated
400Missing or malformed signature or payload
401 / 403Signature did not verify (Billing / Classic)
500An exception occurred; Paddle retries

Signature verification

Billing: the header is Paddle-Signature: ts=<unix>;h1=<hex>[;h1=…]. The plugin computes HMAC-SHA256("{ts}:{raw body}", secret) and compares it with hash_equals against every h1 (to support secret rotation). The tolerance is 300 seconds. The live and sandbox secrets are both tried; the one that verifies decides the environment.

Classic: remove p_signature, cast the values to strings, ksort, PHP-serialize, then openssl_verify(…, OPENSSL_ALGO_SHA1) against the live and sandbox public keys (normalized to PEM). Non-string p_signature values are rejected.

Environment must match

The verifying environment must match the order’s mode (test ⇔ sandbox), otherwise the event is ignored.

Billing events

Billing_Driver::handle_event deduplicates events by event_id (transient wpinterface_paddle_evt_{md5}, 3 days, set after successful processing). After handling, the action wpinterface_paddle_billing_webhook fires.

EventHandling
transaction.paid, transaction.completedParent order: custom_data must carry this site’s tag and the transaction must equal the order’s stored _wpinterface_paddle_transaction_id. Then: check the currency, refuse if totals.total is below the EDD total, store totals, the customer and line items, complete the order and activate pending subscriptions. Renewal (origin subscription_recurring): Orders::record_renewal() with Paddle’s total, tax and billing_period.ends_at. Other origins only add a note. A paid transaction for this site’s order that is not its current transaction gets a “superseded” note and a gateway error.
transaction.payment_failed, transaction.past_dueOnly for origin subscription_recurring and still unpaid: subscription failing + edd_recurring_payment_failed.
subscription.created/activated/trialing/updated/resumedLink the profile ID if the transaction webhook has not arrived yet (never activates). Recover failing → active. The expiration only moves earlier (paid renewals move it forward). Tracks pause state.
subscription.past_duefailing
subscription.pausedPause note and meta _wpinterface_paddle_paused
subscription.canceled, or scheduled_change.action = cancelCancelled in EDD without calling Paddle back. If the subscription was failing, access ends now.
adjustment.created/updatedUnder the order lock: refund approved → record in EDD (unless the store requested it); pending_approval → note; rejected → note, plus for store refunds a loud note, a gateway error and the refund transaction marked failed. Chargeback approved → edd_record_order_dispute. chargeback_warning / chargeback_reverse → note.

Classic alerts

Classic_Driver::handle_alert handles the alerts below. After handling, the action wpinterface_paddle_classic_webhook fires.

AlertHandling
Fulfilment webhook (p_order_id, no alert_name), payment_succeededOrder from the passthrough (JSON with matching site and ref; a bare numeric passthrough from legacy pay links is accepted only on fulfilment webhooks for pending, abandoned or failed orders without the API marker). Check the currency and the amount, flag second payments, store receipt, earnings and totals, complete the order.
subscription_created, subscription_updatedLink the profile only for placeholder IDs and a matching subscription_plan_id (a mismatch gets a note and a gateway error). Store update_url / cancel_url. deleted → cancel. Never moves the expiration on subscription_created; on subscription_updated only earlier, and only when next_bill_date changed.
subscription_payment_succeededinitial_payment=1 (or a pending parent): amount check, complete the parent, activate. Otherwise a renewal. Stores _wpinterface_paddle_last_paid.
subscription_payment_failedfailing, unless older than the last successful payment
subscription_cancelledCancel without calling Paddle back
payment_refunded, subscription_payment_refundedUnder a per-order lock: dedupe by alert_id; match store requests (ledger); VAT refunds are noted only; otherwise record the refund in EDD.
payment_dispute_createdDeduplicated; edd_record_order_dispute

Classic bill dates are UTC days. Classic_Driver::expiration_from_bill_date() converts them to the end of that UTC day in site time, and rejects malformed dates.

Was this page helpful?