Paddle Billing sends webhooks to a public HTTPS URL. Expose gives your local server one, labels every Paddle event with its type, and lets you replay an event without going through another test checkout. We use it ourselves: Expose's own billing runs on Paddle.
expose share http://localhost:3000
Expose prints a public URL such as https://abc123.eu-1.sharedwithexpose.com. If you use Laravel Herd or Valet, run expose in the project directory instead.
With Expose Pro you can keep the same URL every time, so you only create the notification destination once:
expose share http://localhost:3000 --subdomain=my-shop
https://my-shop.eu-1.sharedwithexpose.com/paddle/webhook.transaction.completed, subscription.created and subscription.updated.pdl_ntfset_.Paddle signs every webhook and sends the signature in the Paddle-Signature header:
Paddle-Signature: ts=1671552777;h1=eb4d0dc8853be92b7f063b9f3ba5233eb920a09459b6e6b2c26705b4364db151
To verify it:
ts and the signature h1.ts:body.h1 using a constant-time comparison.Paddle's SDKs do this for you. Verification needs the raw request body, so read it before a JSON parser changes it. Expose forwards the body and the Paddle-Signature header unchanged.
With Laravel Cashier for Paddle, set PADDLE_WEBHOOK_SECRET to the secret key. Cashier registers the /paddle/webhook route and verifies the signature for you.
Complete a checkout in the sandbox with one of the test cards from the Paddle docs, or send a simulated event from Developer Tools → Simulations to your destination. Then open the Expose dashboard:
http://127.0.0.1:4040
You see every request with its headers, body and your application's response. The built-in Paddle Billing request plugin shows the event_type of each request in the terminal and in the dashboard:
200 POST /paddle/webhook ............ payment_method.saved 14:51:04 53 ms
200 POST /paddle/webhook ............ transaction.completed 14:51:03 61 ms
200 POST /paddle/webhook ............ transaction.updated 14:51:02 65 ms
When your handler has a bug, fix the code and click Replay on the request in the dashboard. Expose sends the same event to your local server again, so you don't need another test checkout. You can also change the request before replaying it.
If your code rejects webhooks whose ts is too old, allow a larger time difference in your local environment while you replay older events.
200 response within five seconds. Respond first and do slow work in a queue.--subdomain with Expose Pro, or update the destination in Paddle.expose catch-all and use its URL as the destination. See Catch-All.