#### Automations

# Webhook Triggers

Webhook triggers let an external service start a Grok automation by sending an authenticated HTTP
POST request. Each saved trigger has a unique endpoint and signing secret.

## Create a webhook trigger

1. Create or edit an automation in Grok.
2. Add a **Webhook** trigger.
3. Save the automation.
4. Copy the endpoint and signing secret shown under the trigger.

> [!CAUTION]
>
> The signing secret is shown only when it is created or rotated. Store it securely before leaving
> the automation.

## Sign and send a request

Grok verifies requests using the
[Standard Webhooks](https://www.standardwebhooks.com/) signature format. Sign the exact request body
bytes with HMAC-SHA256 and send the signature with these headers:

| Header | Value |
|---|---|
| `content-type` | The payload type, usually `application/json`. |
| `webhook-id` | A unique identifier for this delivery. |
| `webhook-timestamp` | The current Unix timestamp in seconds. |
| `webhook-signature` | `v1,` followed by the base64-encoded HMAC digest. |

The signed content is:

```text
{webhook-id}.{webhook-timestamp}.{raw-request-body}
```

Remove the `whsec_` prefix from the signing secret and base64-decode the remainder before using it
as the HMAC key. Generate the digest from the raw body bytes—not from a parsed or reformatted copy
of the payload.

Set the endpoint and secret as environment variables:

```bash customLanguage="bash"
export WEBHOOK_URL="<endpoint copied from Grok>"
export WEBHOOK_SECRET="<signing secret copied from Grok>"
```

Then send a signed request:

```python customLanguage="pythonWithoutSDK"
import base64
import hashlib
import hmac
import json
import os
import time
import urllib.request
import uuid

url = os.environ["WEBHOOK_URL"]
secret = os.environ["WEBHOOK_SECRET"]
body = json.dumps(
    {"event": "customer.created", "customer_id": "cus_123"},
    separators=(",", ":"),
).encode()
webhook_id = f"msg_{uuid.uuid4().hex}"
timestamp = str(int(time.time()))

key = base64.b64decode(secret.removeprefix("whsec_"))
signed_content = f"{webhook_id}.{timestamp}.".encode() + body
signature = base64.b64encode(
    hmac.new(key, signed_content, hashlib.sha256).digest()
).decode()

request = urllib.request.Request(
    url,
    data=body,
    headers={
        "content-type": "application/json",
        "webhook-id": webhook_id,
        "webhook-timestamp": timestamp,
        "webhook-signature": f"v1,{signature}",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    print(response.status)
```

```javascript customLanguage="javascriptWithoutSDK"
import { createHmac, randomUUID } from "node:crypto";

const url = process.env.WEBHOOK_URL;
const secret = process.env.WEBHOOK_SECRET;

if (!url || !secret) {
  throw new Error("Set WEBHOOK_URL and WEBHOOK_SECRET");
}

const body = JSON.stringify({
  event: "customer.created",
  customer_id: "cus_123",
});
const webhookId = `msg_${randomUUID().replaceAll("-", "")}`;
const timestamp = Math.floor(Date.now() / 1000).toString();

const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const signedContent = `${webhookId}.${timestamp}.${body}`;
const signature = createHmac("sha256", key)
  .update(signedContent)
  .digest("base64");

const response = await fetch(url, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "webhook-id": webhookId,
    "webhook-timestamp": timestamp,
    "webhook-signature": `v1,${signature}`,
  },
  body,
});

console.log(response.status);
```

A valid request returns HTTP `202 Accepted`. Grok starts the automation asynchronously, so this
response confirms that the delivery was accepted rather than that the automation has finished.

## Troubleshooting

| Status | Meaning |
|---|---|
| `400` | A required header or timestamp is missing or malformed. |
| `401` | Signature rejected, timestamp stale, or trigger not accepting deliveries. |
| `413` | The request body exceeds 1 MiB. |
| `429` | The trigger received too many requests. |
| `503` | Delivery is temporarily unavailable; retry with backoff. |

Sign a newly serialized body for every request. Reusing a signature after changing whitespace,
field ordering, or any other byte in the body causes verification to fail. Timestamps more than five
minutes from Grok's current time are rejected.

## Rotate the signing secret

Use the rotate button next to **Signing secret** if a secret is exposed or must be replaced. Rotation
invalidates the previous secret, and the new plaintext secret is shown once. Update the sender before
making another request.
