# Webhook Events

## Overview

Webhooks allow your application to react in real time to important events across the GrailPay platform without the need to constantly poll the API. Whenever a subscribed event occurs (like a payout being sent or a transaction status changing), GrailPay sends a secure HTTP POST request directly to your server with the event details.

This push-based architecture enables you to automate workflows, synchronize data, and trigger time-sensitive actions the moment something happens.

Each webhook is sent to a configured HTTPS endpoint, and your server is expected to return an HTTP 200 OK response to confirm receipt. You register a single webhook URL and GrailPay delivers every event to it. There is no per-event subscription: your handler receives all events, routes on the `event` field, and ignores any it doesn't need.

:::note
This page documents the current webhook events. If your integration was built against the previous event set (PascalCase event names such as `TransactionStarted`, with no envelope), see [Legacy Webhook Events](/docs/technical/webhooks/legacy-events/). The [Migration Guide](/docs/migration/v3/) maps each legacy event to its replacement.
:::

### Authenticating Webhooks

To verify that a webhook really came from GrailPay and not a malicious actor, every delivery carries a `Signature` header. When you register a webhook through `POST /api/v3/webhooks`, the response includes a `secret` generated for that registration. Keep it: it is the key GrailPay uses to sign every event sent to that URL. Each registration has its own secret, and you can read it again at any time with `GET /api/v3/webhooks/{uuid}`.

The signature is the hex-encoded HMAC-SHA256 of the JSON request body, keyed with that secret. To verify a delivery, compute the same HMAC over the body exactly as you received it—before parsing or re-serialising it—and compare the result with the `Signature` header using a constant-time comparison. Reject any delivery that does not match.

We strongly recommend implementing this verification step. It helps protect your application from spoofed requests and ensures that only genuine GrailPay events are processed.

### Security

GrailPay requires all webhook endpoints to use HTTPS to guarantee the secure delivery of sensitive financial event data. This applies across both sandbox and production environments. Non-secure HTTP URLs will be rejected, and no webhook events will be delivered until a valid HTTPS endpoint is configured.

Requiring HTTPS helps protect against man-in-the-middle attacks and ensures compliance with industry-standard security practices. Before registering a webhook, make sure your server is accessible via a secure, publicly reachable HTTPS URL with a valid SSL certificate.

If your environment isn't properly configured, webhook notifications will silently fail to deliver.

### Response & Retry Logic

To acknowledge receipt of a webhook event, your server must respond with an HTTP 200 OK status code. This tells GrailPay that the event was received and processed successfully.

If a 200 OK response is not returned, we consider the delivery attempt a failure. In such cases, GrailPay will automatically retry sending the webhook at increasing intervals for a limited number of attempts. Repeated failures delay delivery, and a registration whose deliveries fail 30 times in a row is deactivated automatically; see [Manage Webhooks](/docs/technical/webhooks/manage-webhooks/) for how that is reported.

To ensure reliable communication, your webhook handler should be:

- Idempotent: able to handle duplicate event deliveries without side effects. Use `event_uuid` to detect duplicates.
- Efficient: respond quickly, even if additional processing is handled asynchronously.

***

## Event Envelope

Every event shares the same top-level structure. The event-specific content is always under `data`.

```json title="Envelope"
{
  "event": "transaction.created",
  "event_uuid": "019d072d-1203-7695-842b-a15567fbb0d8",
  "event_occurred_at": "2026-04-05 14:28:33",
  "event_version": 3,
  "vendor_uuid": "019e2711-5a92-735d-a7d5-669aec76b54f",
  "data": { }
}
```

| Field | Description |
| --- | --- |
| `event` | The event name, in `group.action` form. Use this to route the event in your handler. |
| `event_uuid` | Unique identifier for this delivery. Use it to de-duplicate retried deliveries. |
| `event_occurred_at` | When the event occurred, as `YYYY-MM-DD HH:MM:SS`. |
| `event_version` | The webhook schema version. Always `3` for the events on these pages. |
| `vendor_uuid` | The vendor the event relates to. |
| `data` | The event-specific payload. Its shape is documented per event. |

:::note
Webhooks are not specific to any entity or event type. One registered URL receives every event for every merchant, business, and person you work with, so there is nothing to register per merchant or per event. Use `event` to route each delivery and the UUIDs inside `data` to identify the entity it concerns.
:::

***

## Available Events

Events are grouped by the entity they describe. Each group has its own page with every event's description and full example payload.

- [Bank Account Events](/docs/technical/webhooks/events/bank-account/): `bank_account.removed`, `bank_account.link_failed`, `bank_account.link_success`
- [Batch Refund Events](/docs/technical/webhooks/events/batch-refund/): `batch_refund.created`
- [Business Events](/docs/technical/webhooks/events/business/): `business.created`, `business.updated`
- [Clawback Events](/docs/technical/webhooks/events/clawback/): `clawback.created`, `clawback.processing`, `clawback.failed`, `clawback.completed`
- [Compliance Status Events](/docs/technical/webhooks/events/compliance-status/): `compliance_status.processing`, `compliance_status.in_review`, `compliance_status.approved`, `compliance_status.rejected`
- [Merchant Events](/docs/technical/webhooks/events/merchant/): `merchant.created`, `merchant.updated`
- [NOC Events](/docs/technical/webhooks/events/noc/): `noc.received`, `noc.processed`
- [Payout Events](/docs/technical/webhooks/events/payout/): `payout.processing`, `payout.failed`, `payout.completed`
- [Person Events](/docs/technical/webhooks/events/person/): `person.created`, `person.updated`, `person.status_changed`
- [Processor Payout Events](/docs/technical/webhooks/events/processor-payout/): `processor_payout.processing`, `processor_payout.failed`, `processor_payout.completed`
- [Refund Events](/docs/technical/webhooks/events/refund/): `refund.created`, `refund.cancelled`, `refund.capture_processing`, `refund.capture_failed`, `refund.capture_completed`, `refund.payout_processing`, `refund.payout_failed`, `refund.payout_completed`
- [Reverse Payout Events](/docs/technical/webhooks/events/reverse-payout/): `reverse_payout.created`, `reverse_payout.cancelled`, `reverse_payout.processing`, `reverse_payout.failed`, `reverse_payout.completed`
- [Transaction Events](/docs/technical/webhooks/events/transaction/): `transaction.created`, `transaction.awaiting_cancellation`, `transaction.paused`, `transaction.resumed`, `transaction.cancelled`, `transaction.processing`, `transaction.failed`, `transaction.completed`, `transaction.declined`
- [FBO Funding Events](/docs/technical/webhooks/events/fbo-funding/): `fbo.funding_created`, `fbo.funding_processing`, `fbo.funding_completed`, `fbo.funding_failed`, `fbo.funding_canceled`
- [FBO Account Events](/docs/technical/webhooks/events/fbo-account/): `fbo.account_snapshot`

***

## Testing

At this time, GrailPay does not support direct simulation of webhook events in the Sandbox environment. However, you can test your webhook integration by registering a webhook URL in Sandbox and performing actions that naturally trigger webhook events. This approach allows you to validate that your endpoint receives and processes real webhook payloads as expected.

For more details on testing in Sandbox, refer to our [Technical Overview](/docs/technical/overview#testing) guide.

***

## Questions?

If you're encountering any issues, please reach out to [support@grailpay.com](mailto:support@grailpay.com).