webhook
Handle Revolut webhook events
- License 许可
- MIT
- 最近更新
- 2026-07-24
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- description
- Handle Revolut webhook events
03 SKILL.md 正文
Webhook Handling
This package ships no controller and no route. support#47 took the entry point
(webhook/cashier/{provider}) and — the point of the move — the ORDER of the steps around it.
Getting that order wrong was #24, and it hid 14 event types behind a 200 for the life of the
package.
The flow, and who owns each part:
- support's
WebhookControllerreceives the POST and calls the driver's one webhook method,RevolutGateway::webhook($content, $headers)(Concerns/HandlesRevolutWebhooks.php), which hands back aRevolutIncomingWebhook. RevolutIncomingWebhook::parse()— verifies the HMAC-SHA256 signature overv1.{timestamp}.{body}(Webhooks/RevolutWebhookVerifier.php), then decodes. A missing signing secret is a hard failure, not a shrug. A body that is not a JSON object throws.- support dispatches
Events\WebhookReceivedwith the raw decoded body — above any decision about what the event means. This is the escape hatch, and it is support's to sequence. RevolutIncomingWebhook::pipeline()→RevolutWebhookSynchronizer::handle()applies the event and returnsbool.- support dispatches
Events\WebhookHandledonly when that bool istrue.
The two sets, which are not the same set
- Subscribed —
config('cashier-revolut.webhook.events'), defaulting to all 22 cases ofEnums\RevolutWebhookEvent. This is whatRevolutGateway::registerWebhook()tells Revolut to send. An event not in this list is never delivered, so it reaches no listener at all. - Applied — the
matchinRevolutWebhookSynchronizer::handle(), 8 of the 22. Everything else takesdefault => $applied = false.
Matching an arm is necessary, not sufficient (#35). A mapped event can still find nothing to
apply itself to, and then it is as unhandled as the 14: a refund/chargeback order, an owner that
resolves to nobody, a SUBSCRIPTION_* with no local record, a 404 on the refetch. syncOrder()
and syncSubscription() return bool and each arm assigns from them — so the candidate set is
the match, and the applied set is what those methods answer. Do not latch true before the
match again; that was the bug.
The rule they implement is "the delivery had an effect": a local row was written, or a payment
outcome was announced. Two edges follow, and both are deliberate — an unchanged subscription
status is true (the row was mirrored before the typed event was skipped), and a declined payment
is true (nothing persisted, but PaymentFailed was dispatched).
Enums\RevolutWebhookEvent is the catalogue — a fact about Revolut. It says nothing about our
coverage, and must not grow an isMapped(). Conflating the two is what made the hatch unreachable.
Adding an event
To deliver one Revolut adds: add the case to the enum. It is then subscribed by default and
reaches WebhookReceived listeners, with no further work.
To apply one: add an arm to the synchronizer's match that assigns from a sync method —
=> $applied = $this->syncX($event, $id) — and make that method return bool, true only when
the delivery had an effect and false on every path that returns early having done nothing. An
arm that calls a void method drops the answer; one that assigns true outright re-creates #35.
Dispatch the support event (Events\PaymentSucceeded, Events\SubscriptionCanceled, …) from
inside the sync method. Whether an event deserves a typed agnostic event is a support question
first (#28) — support leads, drivers follow.
The one rule
pipeline() returns false for an event we do not apply, and never throws. A throw turns an
event we merely have no opinion about into a failed delivery — Revolut retries a 4XX three more
times at 10-minute intervals and then drops it to the failed-events list, so the cost is wasted
retries and a polluted failure log, not an infinite loop. If making it throw feels more honest,
that instinct is #24 — WebhookSyncTest and WebhookDeliveryTest will stop you.
04 同仓库更多 Skills
subscriptions
Work with Revolut Subscriptions API — plans, subscriptions, billing cycles
migration
Create or modify database migrations
implement-contract
Implement a contract (interface) from isapp/laravel-cashier-support
check
Run quality checks — tests, static analysis, code style
api-request
Make an HTTP request to the Revolut Merchant API via RevolutClient