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
- Create or edit an automation in Grok.
- Add a Webhook trigger.
- Save the automation.
- Copy the endpoint and signing secret shown under the trigger.
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 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
export WEBHOOK_URL="<endpoint copied from Grok>"
export WEBHOOK_SECRET="<signing secret copied from Grok>"
Then send a signed request:
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)
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.
Last updated: August 17, 2026