Builder mode · Tutorial
Webhooks
Create a webhook, send it a signed request from your terminal, and let events from other services wake Hermes.
A webhook is a web address that wakes your agent when something happens somewhere else. This tutorial creates one, sends it a signed test request from your terminal, and checks that Hermes answers in Telegram.
What you'll need
- A paid plan, Mini or Pro, with your agent set up.
- Telegram connected, so the reply has somewhere to go. Without it your agent still runs, but you won't see its reply. The Telegram tutorial sets it up.
- A terminal where you can run
curlandopenssl, to send the test request.
Steps
Open Settings, Webhooks
Open SettingsWebhooks (open it). In builder mode it's under "Customize" in the Settings list.
You see a "name" box and a "Create webhook" button. Until you've made one, the list says "No webhooks yet. Create one above to wire an external event to your agent."
Name the webhook
Type a name into "name", for example
Test webhook. It can be up to 80 characters."Create webhook" becomes clickable. Pick a name you'll recognise later: it heads every reply in Telegram, and it's quoted in the message your agent receives.
Create it
Press "Create webhook" (or Enter).
The button reads "Creating…" for a moment. Then a box appears with "URL:", "Secret:" and a line about signing, under a warning that the secret is shown only once.
Copy the URL and the secret
Copy both into a safe place, such as a password manager.
The URL ends in
/api/webhooks/wh_followed by 24 letters and digits. The secret starts withwhsec_, followed by 48 more.Close the box
Press "Done".
The box closes, and the secret can't be shown again: MyHermes keeps only an encrypted copy. Your webhook is in the list with its name, the tags "enabled" and "telegram", its URL, and "0 fires · last never".
Fill in the test script
Copy the script below into a text editor, and replace the text inside the quotes on its first two lines with your URL and your secret.
URL='<the URL you copied>' SECRET='<the secret you copied>' BODY='{"event":"test","message":"Hello from curl"}' TS=$(( $(date +%s) * 1000 )) SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}') curl -sS -X POST "$URL" \ -H 'Content-Type: application/json' \ -H "x-myhermes-timestamp: $TS" \ -H "x-myhermes-signature: $SIG" \ --data-raw "$BODY"The script now holds your own URL and secret, ready to run. The rest of it signs a small test body with the current time.
Run the script
Paste the filled-in script into a terminal and press Enter.
It prints
{"ok":true}. That means MyHermes checked the signature, accepted the request and is waking your agent. It doesn't yet mean your agent has answered.Check Telegram
Open your private chat with your bot in Telegram.
Once your agent has worked on the request, a message arrives that starts with 🔔 and your webhook's name, followed by Hermes's reply. If your agent was asleep, it has to wake first, so give it a little time.
Check the fire count
Open SettingsWebhooks again.
The row now reads "1 fires" and when it last fired, for example "last just now" or "last 2m ago". The count goes up for every request MyHermes accepts.
How signing works
Every request needs two headers:
| Header | What goes in it |
|---|---|
x-myhermes-timestamp | The current time as a Unix timestamp in milliseconds, for example 1790000000000 |
x-myhermes-signature | An HMAC-SHA256 of the text described below, keyed with your secret, written as lowercase hex |
The text you sign is the timestamp's value, a dot, then the body exactly as you send it. With a timestamp of 1790000000000 and a body of {"a":1}, you sign 1790000000000.{"a":1}. The line in the create box names the timestamp header; it's the number in that header that goes into the signature, not the header's name.
- The key is the whole secret,
whsec_included. - Lowercase hex only. No
sha256=in front, no base64 and no capital letters: each of those is rejected. - Milliseconds, within 5 minutes. The timestamp must be within 5 minutes of MyHermes's clock, before or after. A timestamp in seconds is too far off and is rejected.
- Send the body byte for byte as you signed it. Reformatting the JSON, adding a trailing newline or changing a single character after signing makes the signature fail.
In the script above, $(( $(date +%s) * 1000 )) turns the current time in seconds into milliseconds, printf '%s' signs the text without adding a newline, and --data-raw sends the body unchanged.
The request
- Method:
POST. - Address: the URL you copied. It needs no login; the signature is the only check.
- Headers: the two above. Send the
Content-Typeyour tool normally uses; it doesn't change what your agent receives. - Body: whatever you want your agent to see, usually JSON.
Responses
| Status | Body | What it means |
|---|---|---|
200 | {"ok":true} | Accepted. Your agent is being woken to handle it. |
401 | {"error":"unauthorized","reason":"missing_signature"} | There's no x-myhermes-signature header. |
401 | …"reason":"missing_timestamp" | There's no x-myhermes-timestamp header. |
401 | …"reason":"bad_timestamp" | The timestamp isn't a positive number. |
401 | …"reason":"stale_timestamp" | The timestamp is more than 5 minutes from MyHermes's clock. Usually it's in seconds instead of milliseconds. |
401 | …"reason":"bad_signature" | The signature doesn't match: a wrong secret, the wrong text signed, or a body that changed after signing. |
403 | {"error":"webhook_disabled"} | The webhook is marked "disabled". |
404 | {"error":"webhook_not_found"} | No webhook has this address, for example because it was deleted. |
404 | {"error":"instance_not_found"} | The agent this webhook belongs to has been removed. |
429 | {"error":"rate_limited","retryAfterMs":…} | More than 30 requests to this webhook within a minute. |
429 | {"error":"daily_firing_cap","retryAfterMs":…,"cap":…} | This webhook has used its daily limit. cap is that limit. |
retryAfterMs is how many milliseconds to wait before sending again.
A 200 only means the request was accepted. Your agent runs afterwards, and whether it succeeds isn't reported back to the sender.
Limits
- 20 webhooks per agent. Creating another past that fails; delete one first.
- 30 requests a minute per webhook. The minute starts with the first request, and anything over the limit gets a
429until it ends. - A daily limit per webhook: 100 accepted requests on Mini, 1,000 on Pro, in a 24-hour window that starts with the first request. When the window ends, the count starts again.
- 16,000 characters of body. Your agent sees at most the first 16,000 characters of the body. For bigger events, send a short summary or a link instead.
Only requests with a valid signature count toward these limits.
What your agent receives
Each accepted request becomes one message to your agent. It reads:
You have been triggered by the "Test webhook" webhook. An external event fired with this payload:
followed by your body inside a JSON code block, then:
Handle the event according to your instructions. Be concise.
- The body goes in as it arrived, whatever its content type. A form-encoded body shows up as raw text.
- Your agent handles it like a message in the chat, with the same skills and tools.
- Say what you want done. The message asks Hermes to follow your instructions, so include them. The body itself is one place: a field such as
"instructions": "Summarise this in two lines"reaches your agent word for word. - After your plan has ended, requests can still be accepted, but your agent doesn't run.
Where replies go
- Telegram, always. Every webhook you create in the dashboard replies to Telegram, and there's no setting to change that. The reply goes to your Telegram home channel, which is your private chat with your bot if you followed the Telegram tutorial.
- What it looks like: 🔔 and the webhook's name, a blank line, then the reply. A message longer than 4,096 characters is cut off at that length.
- Without Telegram connected, your agent still runs, but its reply isn't sent anywhere.
- Sometimes nothing arrives. If your agent can't be woken, fails, returns an empty reply, or takes longer than 110 seconds to answer, no message is posted.
Connecting GitHub, Stripe or a form tool
GitHub, Stripe and most form tools send webhooks signed in their own way, or not signed at all. MyHermes turns away any request without its own two headers, so pasting your webhook URL into GitHub's or Stripe's webhook settings won't work.
Put a signing step in between:
- Point the other service at something you control that can run a little code: a code step in an automation tool such as Zapier or n8n, or a small server of your own.
- In that step, build the body you want your agent to see and sign it as described above.
- Send it on to your MyHermes URL with the two headers.
Here's the same signature in JavaScript, for Node.js 18 or later. Saved as send.mjs with your URL and secret filled in, node send.mjs sends one signed request, just like the terminal script:
import { createHmac } from "node:crypto";
const url = "<the URL you copied>";
const secret = "<the secret you copied>";
const body = JSON.stringify({ event: "test", message: "Hello from Node" });
const timestamp = String(Date.now());
const signature = createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
const res = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
"x-myhermes-timestamp": timestamp,
"x-myhermes-signature": signature,
},
body,
});
console.log(res.status, await res.text());
Automation tools run code in different ways, so check your tool's own documentation for how its code step computes an HMAC and sends a web request. Keep the secret in that step only, never in the service that sends the event.
Deleting a webhook
Press "Delete" on its row. The webhook is removed straight away, with no confirmation, and its address stops working: requests to it then get 404 with webhook_not_found.
There's no switch to pause a webhook or turn it back on. The "enabled" tag only shows its state. To stop a webhook, delete it; to start again later, create a new one.
Keeping it secure
- The secret is the only lock. The address needs no login, so anyone with both the URL and the secret can wake your agent. Keep the secret out of shared code and out of the service that sends events.
- A secret can't be viewed or changed. If it leaks, or you've lost it, create a new webhook, move your sender over to it, and delete the old one.
- The 5-minute window means a captured request can't be replayed once it's more than 5 minutes old.
- Your agent reads the body as written. Text inside it can steer what your agent does, so only wire up events from sources you trust.
- Secrets are encrypted before MyHermes stores them.
If something goes wrong
401withbad_signature. Check that you used the whole secret,whsec_included; that you signed the timestamp's value, a dot and the body; that the signature is lowercase hex; and that you sent exactly the body you signed.401withstale_timestamp. Send the time in milliseconds, and check your computer's clock is right.200, but no message in Telegram. Check that Telegram is connected under SettingsChannels and that your plan is active. Then check the webhook's fire count went up. If it did, the request was accepted and your agent may have taken too long or failed; try a shorter request.429. Wait forretryAfterMsmilliseconds, then send again.- You've lost the secret. It can't be shown again. Create a new webhook and delete the old one.
404withwebhook_not_found. Check you copied the whole URL. If you deleted the webhook, create a new one.- Creating a webhook fails. You may already have 20. Delete one you don't use and try again.
Common questions
Can I point GitHub or Stripe straight at the URL?
No. They don't send MyHermes's two headers, so every request would be turned away. Use a signing step in between, as described in Connecting GitHub, Stripe or a form tool.
Does a 200 mean Hermes has handled the event?
No. It means MyHermes accepted the request. Your agent runs afterwards, and its reply arrives in Telegram.
Can a webhook reply somewhere other than Telegram?
No. Replies go to your Telegram home channel, or nowhere if Telegram isn't connected.
Can I turn a webhook off for a while?
No. Delete it, and create a new one when you need it again. The new one has a new URL and a new secret.
What's next
- API endpoints: call your agent from your own code and get the reply straight back.
- Telegram: where webhook replies arrive.
- Connectors: apps your agent can use while it handles an event.