Skip to content

Builder mode · Tutorial

Webhooks

Create a webhook, send it a signed request from your terminal, and let events from other services wake Hermes.

Builder mode

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 curl and openssl, to send the test request.

Steps

  1. 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."

  2. 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.

  3. 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.

  4. 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 with whsec_, followed by 48 more.

  5. 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".

  6. 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.

  7. 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.

  8. 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.

  9. 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:

HeaderWhat goes in it
x-myhermes-timestampThe current time as a Unix timestamp in milliseconds, for example 1790000000000
x-myhermes-signatureAn 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-Type your tool normally uses; it doesn't change what your agent receives.
  • Body: whatever you want your agent to see, usually JSON.

Responses

StatusBodyWhat 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 429 until 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:

  1. 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.
  2. In that step, build the body you want your agent to see and sign it as described above.
  3. 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

  • 401 with bad_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.
  • 401 with stale_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 for retryAfterMs milliseconds, then send again.
  • You've lost the secret. It can't be shown again. Create a new webhook and delete the old one.
  • 404 with webhook_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.