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

# Webhook signing reference

> Reference for signing requests to a Hive incoming webhook: HMAC-SHA256 over the raw body, headers, limits, deduplication, responses and code in curl, Node.js and Python.

This is the technical reference for sending events to a Hive
[incoming webhook](/integrations/incoming-webhooks). Every request must be signed with the
webhook's secret; Hive rejects anything it cannot verify.

## Endpoint

```
POST https://<hive-api>/webhooks/incoming/<orgId>.<webhookId>
```

Copy the full URL from Hive when you create the webhook. The workspace is identified from the
URL alone, so nothing in the body can redirect an event to another workspace.

## Request

<ParamField header="Content-Type" type="string" required>
  `application/json`
</ParamField>

<ParamField header="X-Hive-Signature" type="string" required>
  Lowercase hexadecimal HMAC-SHA256 of the **exact raw request body bytes**, keyed with the
  webhook's signing secret. No prefix (send `3f1a…`, not `sha256=3f1a…`).
</ParamField>

<ParamField body="(body)" type="JSON" required>
  UTF-8 JSON, at most 64 KB (65,536 bytes). Any JSON is accepted; bodies that match the
  proposal contract (`proposal.created`, `proposal.won`, `client.created`) are mapped natively.
  See [Incoming webhooks](/integrations/incoming-webhooks) for the field rules.
</ParamField>

<Warning>
  Sign the bytes you actually send. Re-serialising JSON after signing — changing key order,
  spacing or encoding — changes the bytes and the signature will not match.
</Warning>

## Response

| Status | Body | Meaning |
| - | - | - |
| `202` | `{"received": true}` | Accepted for processing. |
| `202` | `{"received": true, "deduplicated": true}` | This exact signed body was already received by this webhook; it is not processed again. |
| `400` | `{"error": {"code": "invalid_input", …}}` | The body is not valid UTF-8 or not valid JSON. |
| `401` | `{"error": {"code": "invalid_signature", …}}` | The signature is missing or wrong, **or** the webhook does not exist. The two are indistinguishable by design. |
| `404` | `{"error": {"code": "not_found", …}}` | The URL token is malformed. |
| `413` | `{"error": {"code": "payload_too_large", …}}` | The body exceeds 64 KB. |
| `503` | `{"error": {…}}` | Temporarily unavailable, or an identical delivery is still being processed. Retry with backoff. |

Signature verification happens before the body is parsed, and uses a constant-time comparison.

## Deduplication and retries

Hive deduplicates on the SHA-256 of the raw body, per webhook. Retrying the same bytes after a
timeout is safe: you get `202` with `"deduplicated": true`, and the event is processed once. To
send two genuinely separate events with the same content, make the bodies differ — for example
with a distinct `eventId`.

## Examples

<CodeGroup>
  ```bash curl + openssl theme={null}
  SECRET='your-signing-secret'
  URL='https://<hive-api>/webhooks/incoming/<orgId>.<webhookId>'
  BODY='{"type":"client.created","entity":"Acme Ltd","sourceTool":"Zapier"}'

  SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.*= //')

  curl -sS -X POST "$URL" \
    -H 'Content-Type: application/json' \
    -H "X-Hive-Signature: $SIG" \
    --data-binary "$BODY"
  ```

  ```javascript Node.js theme={null}
  import { createHmac } from 'node:crypto';

  const secret = process.env.HIVE_WEBHOOK_SECRET;
  const url = process.env.HIVE_WEBHOOK_URL;

  const body = JSON.stringify({
    type: 'proposal.won',
    entity: 'Acme Ltd',
    sourceTool: 'Ignition',
    amount: { value: 45000, currency: 'GBP' },
    eventId: 'ign_8812',
  });

  const signature = createHmac('sha256', secret).update(body, 'utf8').digest('hex');

  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Hive-Signature': signature },
    body,
  });
  console.log(response.status, await response.json());
  ```

  ```python Python theme={null}
  import hashlib, hmac, json, os, urllib.request

  secret = os.environ["HIVE_WEBHOOK_SECRET"].encode()
  url = os.environ["HIVE_WEBHOOK_URL"]

  body = json.dumps({
      "type": "proposal.created",
      "entity": "Acme Ltd",
      "sourceTool": "Go Proposal",
      "amount": {"value": 12000, "currency": "GBP"},
  }).encode("utf-8")

  signature = hmac.new(secret, body, hashlib.sha256).hexdigest()

  request = urllib.request.Request(
      url,
      data=body,
      method="POST",
      headers={"Content-Type": "application/json", "X-Hive-Signature": signature},
  )
  with urllib.request.urlopen(request) as response:
      print(response.status, response.read().decode())
  ```
</CodeGroup>

<Tip>
  In a Zapier "Code by Zapier" or Make custom-code step, use the Node.js or Python sample to
  compute the signature, then send it with a webhook/HTTP step using the same body string.
</Tip>

## Security notes

* Treat the signing secret like a password. Store it in your automation tool's secret storage,
  never in source control.
* Hive stores only a sealed copy of the secret and cannot show it again. To rotate, create a new
  webhook, switch your sender to it, then delete the old one.
* Event content is treated as untrusted input: every field is bounded and validated before it
  reaches the Business Brain.

## Related

<CardGroup cols={2}>
  <Card title="Incoming webhooks" icon="webhook" href="/integrations/incoming-webhooks">
    Create a webhook and the event contract.
  </Card>

  <Card title="Developer overview" icon="code" href="/developers/overview">
    Everything you can build against.
  </Card>
</CardGroup>
