The endpoint
Every gateway shares one route:getWebhookHandler(), and calls handle(). Return the URL from Helper::generateWebhookUrl(), and show it on your settings page so administrators can paste it into the provider’s dashboard.
The webhook route runs with error handling only. There’s no session, no authentication and no CSRF protection, because the caller is a machine. Your signature check is the authentication.
A handler
src/WebhookHandler.php
Verify every request
1
Read the raw body
Compute the signature over
(string) $request->getBody(), not over a re-encoded array. Re-encoding changes key order and escaping, and the signature won’t match.2
Compare in constant time
Use
hash_equals(), never ==.3
Fail closed
If the secret isn’t configured, reject the request. Skipping verification “until it’s set up” means anyone can cancel subscriptions or mark orders paid.
4
Check what the payload claims
Confirm the event refers to an object you know, and that the amount and currency match the order before you treat it as payment.
Responses
Acting on events
Idempotency
Providers retry, deliver out of order, and sometimes deliver twice.Look up state before you change it, and make repeated deliveries a no-op.
Rely on the exceptions:
AlreadyPaidException and AlreadyFulfilledException tell you the work is already done.Never grant credits directly from a webhook. Go through the order commands, which enforce the state machine.
Log the provider’s event ID so duplicates are visible while debugging.
Testing
- Use the provider’s dashboard to replay events at your installation.
- For local development, expose your machine with a tunnel, and set the site URL so the generated webhook URL is reachable.
- Send a request with a wrong signature and confirm you get a
400. - Send an unknown event type and confirm you get a
200. - Watch
var/log/app-*.logwhile testing.