Webhooks¶
Overview¶
Incoming provider webhooks are handled by WebhookController and routed through WebhookService to the appropriate WebhookHandlerInterface implementation.
Frisbii payloads are persisted to paymenthub_webhooks before processing to ensure idempotency, and processed asynchronously. A webhook with a duplicate (provider, external_id) pair is silently skipped.
MobilePay webhooks are handled inline in the request and are not persisted, so paymenthub_webhooks only holds Frisbii rows.
Flow¶
sequenceDiagram
participant Provider as Payment Provider
participant WP as WordPress REST API
participant WC as WebhookController
participant WS as WebhookService
participant WH as WebhookHandler (MobilePay / Frisbii)
participant DB as paymenthub_webhooks
Provider->>WP: POST /wp-json/payment-hub/v1/webhook/{provider}
WP->>WC: handle()
WC->>WS: process(provider, payload)
WS->>WH: verify signature
WH->>DB: INSERT webhook (status=PENDING)
Note over WH,DB: Duplicate external_id → skip
WH->>WH: dispatch to Action Scheduler
WH-->>WC: 200 OK
Note over WH: Async processing via Action Scheduler
WH->>DB: UPDATE status=PROCESSED / SKIPPED / FAILED Signature verification¶
Each webhook handler verifies the provider's HMAC signature against the configured webhook secret before persisting or processing the payload. A failed verification returns HTTP 401.
Async processing¶
Webhook processing is deferred to Action Scheduler (paymenthub/webhook/process). This decouples the HTTP response time from business logic and provides automatic retry on failure.
A processed webhook ends in one of three states:
| Status | Meaning |
|---|---|
PROCESSED | The handler acted on the event |
SKIPPED | The handler does not act on this event type |
FAILED | Processing threw — retry with wp payment-hub webhooks retry --id=<id> |
A status the handler has already set is kept, so SKIPPED stays visible instead of being reported as processed.
Webhook handlers¶
| Provider | Handler class | Processing |
|---|---|---|
mobilepay | MobilePayWebhookHandler | Inline (WebhookHandlerInterface) |
frisbii | FrisbiiWebhookHandler | Async (AsyncWebhookHandlerInterface) |
Both receive their dependencies via container inflectors in PaymentServiceProvider::boot().
Supported events¶
An event outside these lists is stored with status SKIPPED, so it is visible which events we receive without acting on.
Frisbii¶
| Event | Action |
|---|---|
customer_payment_method_added | Stores the payment method on the agreement and activates it |
customer_payment_method_failed | Disables the agreement (FAILED) |
customer_payment_method_deleted | Disables the agreement (CANCELED) |
invoice_created | Persists the payment if we do not know it yet |
invoice_authorized | Persists the payment if needed, then marks it AUTHORIZED |
invoice_settled | Marks the payment CAPTURED |
invoice_failed | Marks the payment FAILED and stores the failure reason as payment meta |
invoice_refund | Marks the payment REFUNDED |
invoice_refund_failed | Routed to PaymentStateService::paymentRefundFailed() |
The event only states that something happened — the current state is always read back from the Frisbii API before acting, since an event can arrive late or more than once for the same invoice.
For invoice_failed the charge endpoint (GET /v1/charge/{handle}) is used rather than the invoice endpoint, because only the charge carries error and error_state. These are stored on the payment as failure_code, failure_description and failure_state. A failure_state of hard_declined prevents PaymentRetryHandler from scheduling a retry.
A declined charge is answered by Frisbii with HTTP 200 and state: failed, so the same failure handling runs synchronously in AgreementService::createRecurringPayment() — the webhook is a confirmation, not the only signal.
MobilePay¶
| Event | Action |
|---|---|
recurring.agreement-activated.v1 | Activates the agreement |
recurring.agreement-expired.v1 | Disables the agreement (EXPIRED) |
recurring.agreement-rejected.v1 | Disables the agreement (REJECTED) |
recurring.agreement-stopped.v1 | Disables the agreement (CANCELED) |
recurring.charge-reserved.v1 | Marks the payment AUTHORIZED |
recurring.charge-captured.v1 | Marks the payment CAPTURED |
recurring.charge-canceled.v1 | Marks the payment CANCELED |
recurring.charge-failed.v1 | Marks the payment FAILED and stores the failure reason |
recurring.charge-creation-failed.v1 | Marks the payment FAILED and stores the failure reason |
Register the endpoint and events with wp payment-hub mobilepay webhooks register.
Failed payments¶
A payment that ends as FAILED sets the order to failed and fires payments-hub/payments/schedule-retry, which schedules a new attempt on the next date in the dunning plan (PaymentRetryDateService). Renewal orders only.
See Dunning and payment retries for the plan, what stops a retry, and how the order screen shows the next attempt.