Webhooks Reference
middag-account receives inbound webhooks from external services and dispatches domain events internally. This document covers webhook endpoints, payload verification, and event types.
Inbound Webhook Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /webhooks/stripe | Stripe Signature | Stripe payment events |
| POST | /webhooks/hubspot | HubSpot Signature | HubSpot CRM events |
Base URL: /wp-json/middag-account/v1
No JWT authentication is used. Each webhook is validated by service-specific signatures.
Company Routing
Both endpoints use the X-Middag-Company header (middag_br or middag_global) to route events to the correct legal entity. A single unified endpoint per service handles both entities.
Stripe Webhook Events
Signature Verification
Stripe webhooks are validated using Stripe\Webhook::constructEvent() with the Stripe-Signature header and the webhook endpoint secret. Requests with an invalid or missing signature receive HTTP 401.
$event = \Stripe\Webhook::constructEvent(
$payload,
$request->getHeader('Stripe-Signature'),
$webhookSecret
);Processed Events
| Stripe Event | Action |
|---|---|
checkout.session.completed | Mark order as paid, trigger entitlement flow |
invoice.paid | Record invoice payment, update subscription |
invoice.payment_failed | Flag payment failure, notify organization |
customer.subscription.updated | Sync subscription status changes |
customer.subscription.deleted | Cancel subscription, suspend entitlements |
charge.refunded | Process refund, update order status |
Payload Schema (checkout.session.completed)
{
"id": "evt_1abc...",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_live_...",
"customer": "cus_...",
"payment_status": "paid",
"metadata": {
"organization_id": "42",
"quote_id": "101"
}
}
}
}HubSpot Webhook Events
Signature Verification
HubSpot webhooks are validated using the X-HubSpot-Signature header with the HubSpot client secret. The signature is an HMAC-SHA256 hash of the client secret + request body.
$expectedHash = hash('sha256', $clientSecret . $requestBody);
if (!hash_equals($expectedHash, $signatureHeader)) {
return new WP_REST_Response(['error' => 'Invalid signature'], 401);
}Processed Events
| HubSpot Event | Action |
|---|---|
| Contact created | Sync contact to WordPress user |
| Contact updated | Update user metadata |
| Deal stage changed | Update linked quote status |
| Form submission | Process lead capture, create organization |
Error Handling
| Scenario | HTTP Response | Behavior |
|---|---|---|
| Missing signature header | 401 | Reject immediately |
| Invalid signature | 401 | Reject, log attempt |
| Unknown event type | 200 | Acknowledge but ignore |
| Processing failure | 500 | Log error, retry by provider |
Webhook endpoints always return 200 for known events that are processed successfully. Returning non-2xx causes the provider to retry delivery.
Outgoing Webhook Events (Domain Events)
Domain events are dispatched internally via do_action(). External consumers listen via WordPress hooks. See Extension Points for the full list of domain events.
Key events that trigger cross-system side effects:
| Domain Event | Side Effects |
|---|---|
middag/quote/paid | Create entitlement, update HubSpot deal |
middag/entitlement/provisioned | Auto-create license, environment, or service |
middag/entitlement/suspended | Notify organization owner, pause downstream |
middag/entitlement/cancelled | Revoke downstream access |
Security Requirements
- Never process a webhook without validating the signature
- Webhook secrets must be stored outside the webroot (env vars or wp-config)
- All webhook processing is logged for audit purposes
- Replay protection: Stripe provides timestamp-based replay protection; verify the tolerance window
Related
- REST API Overview -- Authentication methods
- Extension Points -- Internal domain events