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

# Webhook Signing Policy

> Learn how to authenticate requests sent to Agent Studio webhook triggers.

This article explains how external systems can sign requests sent to an **Agent Studio webhook trigger**.

It covers the webhook credentials, request format, signing procedure, validation rules, API responses, and code examples required to integrate an external system with the webhook trigger.

## Credentials

The webhook URL and signing secret are displayed once in the **Webhook** dialog when the trigger is created or its secret is regenerated.

| Value | Example | Description |
| - | - | - |
| **Webhook URL** | `https://<region-host>/api/agent-builder/webhooks/agents/<token>` | `<region-host>` represents your regional host. Copy the complete URL from the Webhook dialog. The token identifies the trigger. |
| **Signing secret** | `whsec_UF_hXPljvRv8P6RdoEHTAs3nQF_ndB71PJ3NfPGWThY` | Displayed once. Store it securely in a secret manager. |

## Request

Send an HTTP `POST` request to the webhook URL.

```text theme={null}
POST <webhook URL>
```

Include the following headers and request body:

| Field | Location | Required | Type | Description |
| - | - | - | - | - |
| `Content-Type` | Header | Yes | String | Must be `application/json`. |
| `X-Zuper-Timestamp` | Header | Yes | String | Current Unix timestamp in seconds, expressed as a plain integer. Example: `1792597200`. |
| `X-Zuper-Signature` | Header | Yes | String | Must begin with `sha256=` followed by 64 lowercase hexadecimal characters. |
| Body | Body | Yes | JSON | Any JSON object, up to 512 KB, passed to the agent as its input. |

## Signing procedure

Follow these steps to generate a signature for each webhook request.

1. **Serialize the request body:** Convert the JSON body into a string and retain the exact bytes.
2. **Generate the timestamp:** Get the current Unix timestamp in seconds (`ts`).
3. **Construct the signed message:** Combine the timestamp, a period (`.`), and the raw request body.
   ```text theme={null}
   <ts>.<body>
   ```
4. **Generate the HMAC:** Compute HMAC-SHA256 using the signed message and the complete signing secret, including the `whsec_` prefix, as the UTF-8 key.
5. **Format the signature:** Convert the HMAC digest into lowercase hexadecimal.
6. **Send the request:** Include the timestamp and signature in the request headers. Ensure the request body contains the exact bytes used to generate the signature.
   ```text theme={null}
   X-Zuper-Timestamp: <ts>
   X-Zuper-Signature: sha256=<hex>
   ```

### Worked example

The following example illustrates how the signed message and signature are constructed.

```text theme={null}
secret      whsec_testsecret123
ts          1792597200
body        {"invoice_uid":"abc"}
signed      1792597200.{"invoice_uid":"abc"}
signature   sha256=<hex of HMAC-SHA256(secret, signed)>
```

## Rules

Follow these requirements when signing and sending webhook requests.

* **Request body:** Send the exact bytes used to generate the signature. Re-serializing the JSON body, including changing the key order, spacing, or formatting, returns `401 invalid_signature`.
* **Signing secret:** Use the complete signing secret, including the `whsec_` prefix, as the HMAC key. Removing the prefix or Base64-decoding the secret returns `401 invalid_signature`.
* **Timestamp validity:** The timestamp must be within 300 seconds (5 minutes) of the server's current time. Timestamps outside this window, including those caused by clock differences, return `401 stale_timestamp`.
* **Timestamp format:** Send the timestamp as a plain integer in seconds. Milliseconds, leading zeros, or decimal values return `401 stale_timestamp`.
* **Content type:** Set `Content-Type` to `application/json`. Other content types leave the request body unread and cause signature validation to fail.
* **Retries:** Generate a fresh timestamp and signature for every retry. Requests replayed after 300 seconds are rejected.
* **Secret security:** Do not expose the signing secret in client-side code or application logs. Anyone with the secret can initiate agent runs.
* **Duplicate requests:** Accepted requests are not automatically deduplicated. Each accepted request starts a new agent run.
* **Secret regeneration:** Update callers immediately after regenerating the signing secret. The previous secret becomes invalid at once.

## Responses

The webhook returns the following HTTP status codes based on request validation and processing.

| Status | Error | Description | Retry |
| - | - | - | - |
| `202` | — | Request accepted. Response body: `{"data":{"runId":"..."}}`. | No |
| `400` | `missing_headers` | Timestamp or signature header is missing. | No. Correct the request. |
| `400` | `malformed_signature` | Signature does not match the required `sha256=<64 hex>` format. | No. Correct the signature. |
| `400` | `invalid_json` | Request body contains invalid JSON. | No. Correct the payload. |
| `401` | `stale_timestamp` | Timestamp is outside the 300-second window or is not a valid integer. | Yes. Generate a fresh timestamp and signature. |
| `401` | `invalid_signature` | HMAC signature does not match. | No. Verify the secret and signed body. |
| `404` | `not_found` | Webhook token is not recognized. | No |
| `404` | `agent_not_found` | Associated agent has been deleted. | No |
| `410` | `webhook_disabled` | Webhook trigger is disabled. | No |
| `410` | `agent_inactive` | Agent is paused or not deployed. | No |
| `413` | `payload_too_large` | Request body exceeds 512 KB. | No |
| `429` | `rate_limit_exceeded` | Exceeds 60 requests per minute per webhook or 300 requests per minute per IP. | Yes. Retry after the `Retry-After` duration. |
| `429` | `run_limit_reached`, `daily_run_limit_reached` | Agent run limit reached. The `resetsAt` value indicates when the limit resets. | Retry after `resetsAt`. |
| `500` | `internal_error` | Internal server error. | Yes, with backoff. |
| `503` | `enqueue_failed` | Agent run could not be queued. | Yes, with backoff. |

## Examples

The following examples show how to generate a signature and send a signed webhook request using cURL, Node.js, and Python.

```bash cURL theme={null}
SECRET='whsec_...'
URL='https://<region-host>/api/agent-builder/webhooks/agents/<token>'
BODY='{"invoice_uid":"abc","note":"overdue"}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //')

curl -X POST "$URL" \
  -H 'Content-Type: application/json' \
  -H "X-Zuper-Timestamp: $TS" \
  -H "X-Zuper-Signature: sha256=$SIG" \
  --data-raw "$BODY"
```

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

const secret = process.env.ZUPER_WEBHOOK_SECRET
const url = process.env.ZUPER_WEBHOOK_URL
const body = JSON.stringify({ invoice_uid: 'abc', note: 'overdue' })
const ts = String(Math.floor(Date.now() / 1000))
const sig = crypto.createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex')

const res = await fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Zuper-Timestamp': ts,
    'X-Zuper-Signature': `sha256=${sig}`,
  },
  body,
})

console.log(res.status, await res.json())
```

```python Python theme={null}
import hashlib, hmac, json, os, time
import requests

secret = os.environ["ZUPER_WEBHOOK_SECRET"]
url = os.environ["ZUPER_WEBHOOK_URL"]
body = json.dumps({"invoice_uid": "abc", "note": "overdue"})
ts = str(int(time.time()))
sig = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()

res = requests.post(
    url,
    data=body.encode(),  # Send the signed bytes; do not use json=
    headers={
        "Content-Type": "application/json",
        "X-Zuper-Timestamp": ts,
        "X-Zuper-Signature": f"sha256={sig}",
    },
)

print(res.status_code, res.json())
```


## Related topics

- [Webhooks](/Settings/Developer_Hub/Webhooks.md)
- [Login using Google](/Settings/Security/Login_using_google.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.