A webhook starts a routine when another service (Zapier, Make, n8n, a script, your own server) sends a signed request to its address. The request’s JSON body is passed to the routine along with its prompt, as data. The request gets an answer right away; the run happens afterward, and its result lands in the routine’s conversation.
Create a triggerLink to the Create a trigger section
- Open the routine in “Routines”, then the “Webhooks” tab.
- Click “Create a webhook trigger”. The “Name” is optional: it helps you tell your triggers apart, for example “Zapier new leads”.
- Copy the “Webhook URL” and the “Signing key”.
- Click “I saved the key”.
“Show a call example” displays a ready-to-copy command with your address and key.
Each routine can have up to 5 triggers. The “Webhooks” tab shows how many calls each trigger accepts per minute and per day: the daily number depends on the workspace plan (see the plans).
Endpoint referenceLink to the Endpoint reference section
POST /api/v1/routines/hooks/{id} Link to the POST /api/v1/routines/hooks/{id} section
Starts a run of the routine linked to the trigger.
| Item | Value |
|---|---|
| Full address | https://, given in the “Webhooks” tab. It contains no secret. |
| Authentication | The request’s HMAC signature. No session token or API key is used. |
| Scope | A single routine: the trigger’s. The routine’s owner must be an active member of its workspace, and the workspace plan must include routines. |
| Body | JSON, 64 KB at most. An empty body counts as {}. |
ParametersLink to the Parameters section
| Name | Location | Type | Required | Description |
|---|---|---|---|---|
id | Path | String | Yes | The trigger’s public ID, 16 to 64 characters (letters, digits, _ and -). |
Content-Type | Header | String | Yes | application/, or an application/ type. |
X-Moon-Timestamp | Header | String | Yes | The time of the call in Unix seconds, digits only. It must be within 5 minutes of the server time, either way. |
X-Moon-Signature | Header | String | Yes | v1= followed by the signature in lowercase hexadecimal (64 characters). Several signatures can be separated by commas. |
| Body | Body | JSON object | No | The event data, passed to the routine. |
ResponseLink to the Response section
An accepted request returns 202 before the routine runs:
{"run_id": "4b0c1f0e-2a7d-4c41-9e3b-6f1d2c8a5b90", "status": "queued"}| Field | Type | Description |
|---|---|---|
run_id | String | The run ID, visible in the routine’s “History” tab. |
status | String | Always queued: the run is waiting its turn. |
The run appears in the routine’s “History” tab with the “Webhook” trigger. Keep the sending machine’s clock accurate.
The signatureLink to the The signature section
The signature is an HMAC-SHA256:
- the key is the signing key, exactly as shown (it starts with
mwk_); - the message is the timestamp, a period, then the raw body, exactly the bytes sent;
- the result is written in lowercase hexadecimal, 64 characters.
<X-Moon-Timestamp>.<raw body>To check your computation, use these sample values:
| Item | Value |
|---|---|
| Key | mwk_exempleDeCle |
X-Moon-Timestamp | 1762300800 |
| Body | {"event":"new_lead", |
| Expected signature | 0d61b9fd29504eb3bd85a2e00361373c8f786822ddb73162bb825ca45abfcb67 |
The X-Moon-Signature header can hold up to 5 signatures separated by commas (v1=…,v1=…): the request is accepted if one of them matches. A signature without the v1= prefix is accepted too.
ExamplesLink to the Examples section
Replace the address and the key with your trigger’s.
KEY='mwk_your_key'
URL='https://chat.realmoon.ai/api/v1/routines/hooks/your_id'
BODY='{"event":"new_lead","id":"evt_123"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$KEY" -hex | sed 's/^.* //')
curl -sS -X POST "$URL" \
-H 'Content-Type: application/json' \
-H "X-Moon-Timestamp: $TS" \
-H "X-Moon-Signature: v1=$SIG" \
--data-binary "$BODY"import hashlib, hmac, json, time, requests
key = "mwk_your_key"
url = "https://chat.realmoon.ai/api/v1/routines/hooks/your_id"
body = json.dumps({"event": "new_lead", "id": "evt_123"}).encode()
ts = str(int(time.time()))
sig = hmac.new(key.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
r = requests.post(url, data=body, headers={
"Content-Type": "application/json",
"X-Moon-Timestamp": ts,
"X-Moon-Signature": f"v1={sig}",
})
print(r.status_code, r.json())import { createHmac } from 'node:crypto';
const key = 'mwk_your_key';
const url = 'https://chat.realmoon.ai/api/v1/routines/hooks/your_id';
const body = JSON.stringify({ event: 'new_lead', id: 'evt_123' });
const ts = Math.floor(Date.now() / 1000).toString();
const sig = createHmac('sha256', key).update(`${ts}.${body}`).digest('hex');
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Moon-Timestamp': ts,
'X-Moon-Signature': `v1=${sig}`
},
body
});
console.log(res.status, await res.json());In Python, pass the already encoded body in data= (not json=), so the signed bytes are what gets sent. The JavaScript example runs on Node.js 18 or later.
In Zapier, Make or n8n, use a code step or an HTTP request step that computes the signature on the exact body, then sends it with both headers.
ErrorsLink to the Errors section
An error body has the form {"detail": . The detail is either a text or a {"code": object. The texts are returned in French; their meaning is given in parentheses.
| Status | Code or text | Cause |
|---|---|---|
400 | stale_timestamp | X-Moon-Timestamp is more than 5 minutes off the server time. |
400 | invalid_json | The body is not valid JSON. |
403 | feature_scheduled_routines | The workspace plan does not include routines. |
404 | Introuvable. (“Not found.”) | Unknown address, trigger disabled or deleted, signature missing or wrong, timestamp missing or malformed, routine paused or deleted, or an owner who can no longer run it. |
409 | replayed | This request was already received (same signature). |
413 | Corps (“Request body too large, 64 KB at most.”) | The body is larger than 64 KB. |
415 | Le (“The body must be JSON.”) | Content-Type is not a JSON type. |
429 | rate_limited, Limite (“This trigger’s limit is reached.”) | The trigger received too many calls within the minute or the day. |
429 | rate_limited, Trop (“Too many requests.”) | Too many refused requests from your address (more than 60 per minute). |
429 | queue_full | 20 runs are already waiting for this routine. |
503 | Service (“Service temporarily unavailable. Try again in a moment.”) | Temporary outage. |
Sample bodies:
{"detail": "Introuvable."}{"detail": {"code": "rate_limited", "message": "Limite de ce déclencheur atteinte."}}The same 404 is returned for every cause listed, so that nothing reveals whether a trigger exists. Check the address, the key and the signature computation first.
Resend a requestLink to the Resend a request section
- A request refused with
429,403or503does not count toward the trigger’s limits. You can resend it as is while its timestamp is less than 5 minutes old, or sign it again with a recent time. - Two identical requests (same body, same second) have the same signature: the second one gets
409. Add a unique event ID to the body, for example"id":."evt_123" - An accepted request is never run twice, even if it is resent.
What the routine doesLink to the What the routine does section
The routine receives its prompt, followed by the request body presented as data sent by an outside service. The AI uses it as material for the task, without following any instructions it might contain. The body goes through Moon Blur like the rest of the request.
A run started by a webhook:
- never acts in your connectors (no email sent, no message posted), even if the routine has the “Allow actions” option;
- does not open web pages, run code, or generate images or videos;
- does not create a public link to your files or attach any to a message;
- can read your connectors and search the web;
- is not retried automatically after an error;
- counts toward your usage limit like a regular run.
The prompt and the answer are marked as containing outside content: nothing remote loads in the answer until you click. As long as this message stays in the conversation, the following turns keep the same precautions: every action in your tools needs your approval.
Manage triggersLink to the Manage triggers section
| Action | Effect |
|---|---|
| “Enable” followed by the name (toggle) | When disabled, the trigger returns 404. |
| “Rename” | Changes the displayed name. |
| “Renew the signing key” | Issues a new key. The old one stops working immediately. |
| “Delete” | Requests sent to its address are refused. This cannot be undone. |
Disabling a trigger, renewing its key or deleting it also cancels its queued runs that have not started yet. The list shows, for each trigger, the “Calls received” and the date of the last call. Deleting the routine deletes its triggers.