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-woo/v1/webhook (Webhooks::handle, public permission_callback). With the wpinterface_paddle_woo_webhook_base_url filter set, the URL becomes {base}/?rest_route=/wpinterface-paddle-woo/v1/webhook. A request with a Paddle-Signature header is handled as Billing; otherwise it must be a form post with a string p_signature (Classic).

ResponseMeaning
200 OK / Already processedHandled, or a Billing event already processed
400Missing signature, or a Billing payload without event_type
401 / 403Signature did not verify (Billing / Classic)
500An exception was thrown while handling; Paddle retries

Signature verification

Billing (verify_billing( $raw_body, $header, $secrets, $now = null )): parses ts=<unix>;h1=<hex>[;h1=…], rejects a timestamp more than 300 seconds from now, computes HMAC-SHA256( "{ts}:{raw body}", secret ) and compares with hash_equals() against every h1 (secret rotation). The live secret is tried, then the sandbox secret; the one that verifies decides the environment.

Classic (verify_classic( $fields, $public_key )): base64-decodes p_signature, removes it, casts scalar values to strings, ksorts, PHP-serializes, and runs openssl_verify( …, OPENSSL_ALGO_SHA1 ). Keys are accepted with or without BEGIN/END lines. The sandbox key is tried, then the live key.

Environment must match

The environment that verified the signature must match the order’s (or subscription’s) environment, otherwise the event is ignored.

Billing events

Billing_Driver::handle_event deduplicates events by event_id (transient wpinterface_paddle_woo_evt_{md5}, 3 days, set only after successful processing). An add-on can take over an event with wpinterface_paddle_woo_handle_billing_event; afterwards wpinterface_paddle_woo_billing_webhook fires.

EventHandling
transaction.paid, transaction.completedInitial payment: custom_data must carry this site’s tag, the order must be a Paddle order in the same environment, and the transaction must be the order’s current one or an earlier one with identical terms. Then refuse (with a one-time note) if the currency differs or the total is below the order total; store totals, tax, earnings, line items and the Paddle customer; link the subscription; complete the order. Renewal (origin subscription_recurring): Subscriptions::record_renewal(). Origins subscription_charge / subscription_update only get a note on the subscription.
transaction.payment_failed, transaction.past_dueOnly origin subscription_recurring, not paid, not already recorded: Subscriptions::mark_failing()
subscription.created, .activated, .trialing, .updated, .past_due, .paused, .resumed, .canceledLinks an unknown subscription when custom_data points at this site’s order; stores the update-payment-method URL; maps the status. Pending, cancelled, expired and pending-cancel subscriptions ignore everything except cancellation.
adjustment.created, adjustment.updatedHandled under the order lock (see Refund internals)

Billing_Driver::EVENTS is the list above; Set up webhook subscribes the destination to it (filterable with wpinterface_paddle_woo_billing_webhook_events).

Classic alerts

Classic_Driver::handle_alert handles the alerts below. An add-on can take over an alert with wpinterface_paddle_woo_handle_classic_alert; afterwards wpinterface_paddle_woo_classic_webhook fires.

AlertHandling
Fulfilment webhook (no alert_name), payment_succeededOrder from the passthrough (site tag, environment, current ref). Check currency and amount, flag a second payment on an already-paid order, store total, tax, earnings, receipt URL and Paddle user ID, then complete the order.
subscription_created, subscription_updatedLinks an unknown subscription from the passthrough, but only if the plan ID matches the order’s plan. Stores update and cancel URLs. Maps deleted, past_due, paused, active / trialing; the next payment follows next_bill_date only when it changed.
subscription_payment_succeededStores _wpinterface_paddle_last_paid. Initial payment: currency and amount checks, complete the parent. Otherwise a renewal via record_renewal().
subscription_payment_failedFailing, unless the event is not later than the last successful payment
subscription_cancelledcancel_from_paddle() with cancellation_effective_date
payment_refunded, subscription_payment_refundedUnder the order lock (see Refund internals)
payment_dispute_createdDeduplicated; order → on-hold

Classic bill dates are UTC days; Classic_Driver::bill_time() converts them to the start of that UTC day.

Several stores on one Paddle account

Each order is tagged with Plugin::site_tag(): the first ten hex characters of md5( 'woo|' . home_url() ) (without scheme and trailing slash). Billing carries it in custom_data.wc_site, Classic in the passthrough’s wc_site. Driver::is_own_reference() ignores anything else. Refunds, disputes and subscription events are matched by Paddle IDs, so they never match another store’s records.

Changing the site URL

Changing the site URL changes the tag: first payments for orders created before the change are then ignored by the webhook (Billing orders can still be confirmed with Check payment with Paddle). Renewals and refunds are unaffected.

Was this page helpful?