Skip to content

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.