> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hyparrow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Configure a webhook URL, verify signatures, test deliveries, and read delivery history.

## Overview

**Webhooks** let Hyparrow push events to your server in real time instead of you polling the API.
When something happens — a subscription renewal is paid, a customer transaction completes —
Hyparrow sends an HTTP `POST` with a JSON payload to the URL you configure.

Each webhook request carries these headers:

| Header                 | Description                                                   |
| ---------------------- | ------------------------------------------------------------- |
| `Content-Type`         | Always `application/json`                                     |
| `User-Agent`           | `Hyparrow-Webhook/1.0`                                        |
| `X-Webhook-ID`         | Unique ID for this delivery attempt                           |
| `X-Hyparrow-Signature` | HMAC signature of the raw body (present once a secret is set) |

Deliveries are retried automatically on failure with a backoff of **1m, 5m, 30m, 2h, 24h** — up to
**5 attempts**. Your endpoint should return a `2xx` status to acknowledge receipt.

<Note>
  **Base URL:** `https://api.hyparrow.cloud/api/v1`

  Webhook settings endpoints are authenticated with your API key pair:

  ```
  X-API-Key: pk_live_xxxxxxxxxxxx
  X-API-Secret: sk_live_xxxxxxxxxxxx
  ```

  Point your webhook URL at a sandbox listener and configure it from
  `https://sandbox.hyparrow.cloud/api/v1` with `pk_test_` keys to safely exercise deliveries.
</Note>

## Configure your webhook URL

`POST /webhooks/settings` sets the URL Hyparrow delivers events to. You may also pass an optional
`allowedIps` list to restrict which source IPs your endpoint accepts.

<Steps>
  <Step title="Set your URL">
    POST your HTTPS endpoint to `/webhooks/settings`.
  </Step>

  <Step title="Generate a signing secret">
    Call `/webhooks/settings/secret` so deliveries are signed.
  </Step>

  <Step title="Verify the signature on every request">
    Recompute the HMAC over the raw body and compare it to `X-Hyparrow-Signature`.
  </Step>

  <Step title="Send a test event">
    Use `/webhooks/settings/test` to confirm end-to-end delivery.
  </Step>
</Steps>

```bash theme={null}
curl -X POST https://api.hyparrow.cloud/api/v1/webhooks/settings \
  -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
  -H "X-API-Secret: sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": "https://api.acme-store.com/hooks/hyparrow",
    "allowedIps": ["203.0.113.10"]
  }'
```

```json theme={null}
{
  "success": true,
  "message": "Webhook settings updated successfully",
  "data": {
    "webhookUrl": "https://api.acme-store.com/hooks/hyparrow",
    "allowedIps": ["203.0.113.10"]
  }
}
```

<Warning>
  `webhookUrl` must be a valid URL. An invalid value returns `400`.
</Warning>

## View current settings

`GET /webhooks/settings` returns your configured URL, allowed IPs, whether a secret is set (with a
masked preview), and your customer-email preference.

```bash theme={null}
curl https://api.hyparrow.cloud/api/v1/webhooks/settings \
  -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
  -H "X-API-Secret: sk_live_xxxxxxxxxxxx"
```

```json theme={null}
{
  "success": true,
  "data": {
    "webhookUrl": "https://api.acme-store.com/hooks/hyparrow",
    "allowedIps": ["203.0.113.10"],
    "hasSecret": true,
    "secretMasked": "a1b2c3d4...8e7f6a5b",
    "handleCustomerEmails": false
  }
}
```

## Generate a signing secret

`POST /webhooks/settings/secret` generates a new signing secret and stores it. The full secret is
returned **only once** — store it securely.

```bash theme={null}
curl -X POST https://api.hyparrow.cloud/api/v1/webhooks/settings/secret \
  -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
  -H "X-API-Secret: sk_live_xxxxxxxxxxxx"
```

```json theme={null}
{
  "success": true,
  "message": "Webhook secret generated successfully. Store this securely - it will not be shown again.",
  "data": {
    "secret": "a1b2c3d4e5f6...0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e7f6a5b"
  }
}
```

<Warning>
  Generating a new secret replaces any previous one. Update your verification code immediately so
  you don't reject incoming deliveries.
</Warning>

## Verifying the signature

When a secret is set, every delivery includes an `X-Hyparrow-Signature` header. It is an
**HMAC-SHA512** of the **raw request body**, keyed with your webhook secret and hex-encoded.

To verify, recompute the HMAC over the exact bytes you received and compare it to the header using
a constant-time comparison. Reject the request if they differ.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verify(rawBody, signatureHeader, secret) {
    const expected = crypto
      .createHmac("sha512", secret)
      .update(rawBody) // the raw bytes, before JSON.parse
      .digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader),
      Buffer.from(expected)
    );
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib

  def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha512).hexdigest()
      return hmac.compare_digest(signature_header, expected)
  ```
</CodeGroup>

<Warning>
  Compute the HMAC over the **raw, unparsed** body. Re-serializing the parsed JSON can change byte
  ordering or whitespace and produce a different signature.
</Warning>

## Send a test webhook

`POST /webhooks/settings/test` delivers a sample event to your configured URL so you can validate
your endpoint and signature handling.

```bash theme={null}
curl -X POST https://api.hyparrow.cloud/api/v1/webhooks/settings/test \
  -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
  -H "X-API-Secret: sk_live_xxxxxxxxxxxx"
```

```json theme={null}
{
  "success": true,
  "message": "Test webhook sent successfully to https://api.acme-store.com/hooks/hyparrow"
}
```

The payload your endpoint receives looks like:

```json theme={null}
{
  "event": "test.webhook",
  "timestamp": 1751277000,
  "data": {
    "message": "This is a test webhook from Hyparrow",
    "clientId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
  }
}
```

<Warning>
  If no webhook URL is configured, this returns `400 No webhook URL configured`.
</Warning>

## Example event payload

All events share the same envelope: a top-level `event` name, a Unix `timestamp`, and a `data`
object. Here is a `subscription.payment.completed` event, delivered when a subscription renewal is
paid:

```json theme={null}
{
  "event": "subscription.payment.completed",
  "timestamp": 1751277600,
  "data": {
    "subscriptionId": "5d6e7f80-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
    "customerId": "c1a2b3c4-d5e6-7a8b-9c0d-1e2f3a4b5c6d",
    "productId": "8f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
    "amount": 4500.00,
    "currency": "NGN",
    "paidAt": "2026-06-30T11:00:00Z",
    "periodStart": "2026-06-30T11:00:00Z",
    "periodEnd": "2026-07-30T11:00:00Z",
    "reference": "VA-2026-0042",
    "paymentSource": "va"
  }
}
```

Other events you may receive include `checkout.payment.completed` (a checkout-link payment landed)
and `customer.transaction.completed` (a customer transaction settled). All follow the same
`event` / `timestamp` / `data` envelope, so write your handler to switch on the `event` field.

## Customer email preference

`PUT /webhooks/settings/customer-emails` controls whether Hyparrow sends transactional emails
(invoice receipts, subscription notices) **directly to your end customers**. Set
`handleCustomerEmails` to `false` if you prefer to own all customer communication yourself.

```bash theme={null}
curl -X PUT https://api.hyparrow.cloud/api/v1/webhooks/settings/customer-emails \
  -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
  -H "X-API-Secret: sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "handleCustomerEmails": false }'
```

```json theme={null}
{
  "success": true,
  "message": "Customer email preference updated",
  "data": { "handleCustomerEmails": false }
}
```

<Note>
  The `handleCustomerEmails` field is required and must be a boolean. Omitting it returns `400`; an
  explicit `false` is accepted and disables customer-facing emails.
</Note>

## Delivery history

`GET /webhooks/events` returns recent delivery attempts so you can audit what was sent and whether
it succeeded. Useful both for debugging and as a polling fallback.

| Query param | Description                                                    |
| ----------- | -------------------------------------------------------------- |
| `limit`     | Max events to return (default `50`, capped at `100`)           |
| `since`     | RFC3339 timestamp; return events created at or after this time |

```bash theme={null}
curl "https://api.hyparrow.cloud/api/v1/webhooks/events?limit=20&since=2026-06-30T00:00:00Z" \
  -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
  -H "X-API-Secret: sk_live_xxxxxxxxxxxx"
```

```json theme={null}
{
  "success": true,
  "data": [
    {
      "id": "f0e1d2c3-b4a5-6789-0abc-def123456789",
      "clientId": "1a2b3c4d-...",
      "event": "subscription.payment.completed",
      "status": "delivered",
      "attempts": 1,
      "maxAttempts": 5,
      "httpStatus": 200,
      "deliveredAt": "2026-06-30T11:00:01Z",
      "createdAt": "2026-06-30T11:00:00Z"
    },
    {
      "id": "e1d2c3b4-a5f6-7890-1bcd-ef2345678901",
      "clientId": "1a2b3c4d-...",
      "event": "test.webhook",
      "status": "failed",
      "attempts": 5,
      "maxAttempts": 5,
      "httpStatus": 500,
      "lastError": "webhook returned status 500",
      "nextRetryAt": null,
      "createdAt": "2026-06-29T09:15:00Z"
    }
  ]
}
```

A delivery's `status` is one of `pending` (awaiting a retry), `delivered` (a `2xx` was returned),
or `failed` (exhausted all attempts). Inspect `lastError` and `httpStatus` to diagnose failures.
