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

# Webhook source

> One HTTPS URL for any sender, with optional signature checks.

A webhook source gives you a URL. Any system that can send an HTTP request can send events to it.

```text theme={null}
POST https://hooksnode.com/p/<source-id>
```

The default source of a project has the same ID as the project.

## Send an event

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://hooksnode.com/p/<source-id> \
    -H "Content-Type: application/json" \
    -H "X-Idempotency-Key: order_9311_paid" \
    -d '{"event":"order.paid","order_id":"9311"}'
  ```

  ```javascript Node.js theme={null}
  await fetch("https://hooksnode.com/p/<source-id>", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Idempotency-Key": "order_9311_paid",
    },
    body: JSON.stringify({ event: "order.paid", order_id: "9311" }),
  });
  ```

  ```python Python theme={null}
  import requests

  requests.post(
      "https://hooksnode.com/p/<source-id>",
      headers={"X-Idempotency-Key": "order_9311_paid"},
      json={"event": "order.paid", "order_id": "9311"},
  )
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest("POST", "https://hooksnode.com/p/<source-id>",
  	strings.NewReader(`{"event":"order.paid","order_id":"9311"}`))
  req.Header.Set("Content-Type", "application/json")
  req.Header.Set("X-Idempotency-Key", "order_9311_paid")
  http.DefaultClient.Do(req)
  ```
</CodeGroup>

hooksnode keeps the body byte for byte, and keeps the request headers. The body can be up to 4 MiB. It does not have to be JSON, but filters and transforms need a JSON object.

## Responses

| Status | Body                                                 | Meaning                                                                                   |
| ------ | ---------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `200`  | `{"status":"accepted","callback_id":…,"enqueued":…}` | Saved and queued.                                                                         |
| `200`  | `{"status":"buffered","callback_id":…}`              | Pro project with no destination. See [buffered events](/delivery/replay#buffered-events). |
| `200`  | `{"status":"duplicate — already accepted"}`          | The `X-Idempotency-Key` was seen before.                                                  |
| `400`  | `{"error":"no destinations configured"}`             | Free project with no destination.                                                         |
| `401`  | `{"error":"invalid signature"}`                      | Signature check failed.                                                                   |
| `402`  | `{"error":…}`                                        | No credits, unpaid plan, or source over the plan limit.                                   |
| `404`  |                                                      | No such source, or the source has no URL.                                                 |
| `423`  | `{"error":"source paused"}`                          | The source is paused.                                                                     |
| `429`  |                                                      | Over the source rate limit. Wait for `Retry-After` (60 s).                                |
| `503`  |                                                      | A short outage on our side. Retry after `Retry-After`.                                    |

Most providers retry on `5xx` and `429`. Do not drop an event when you get these codes.

## Check signatures on the way in

You can make hooksnode refuse events that are not signed with a secret. Use this for your own senders.

1. Open the source and turn on **Signing**. hooksnode makes a secret.
2. In your sender, compute an HMAC-SHA256 of the raw body with the secret.
3. Send it in the `X-Webhook-Signature` header as `sha256=<hex>`.

```bash theme={null}
BODY='{"event":"order.paid"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://hooksnode.com/p/<source-id> \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=$SIG" \
  -d "$BODY"
```

A missing or wrong signature gets `401`.

<Note>
  This check uses a secret that hooksnode makes. Providers such as Paystack and Stripe sign with their own secret and header. For those providers, leave inbound signing off, and check the provider's signature in your destination. hooksnode sends the provider's headers on unchanged.
</Note>

## Rate limit

Each source takes up to 1,000 events a minute by default. On a Pro project you can raise the limit up to 1,000,000 a minute. Over the limit, hooksnode answers `429` with `Retry-After: 60`.
