Put the Secret in the Header, Not the URL: Safer Webhook Triggers for Self-Hosted AI Agents

Most self-hosted agent setups grow a webhook endpoint sooner or later. A form submission should wake the agent. A payment event should start a follow-up. A cron on another box should poke the research job. The fastest way to protect that endpoint is also the most common mistake: stick a secret token on the end of the URL.

https://agent.example.com/hooks/lead-intake?token=8f3a...c91

It works. It is also a quiet leak waiting to happen. This post covers why the URL is the wrong place for the secret, what to do instead, and how to migrate without breaking everything that already calls you.

Why secrets in URLs leak

A URL is not treated like a secret by any system that touches it. It gets copied around constantly:

  • Access logs. Reverse proxies, load balancers, and tunnels log the full request line by default. Your token is now sitting in a log file, probably retained for weeks.
  • Referrer headers and browser history. If anyone ever opens the URL in a browser to test it, the token lands in history and sync services.
  • Error trackers and monitoring. Uptime checks and exception reporters capture the failing URL and ship it to a third party.
  • Screenshots and chat pastes. “Is this endpoint right?” is a sentence that ends with someone pasting the whole link into Slack or Discord.
  • Third-party dashboards. Many SaaS tools that send webhooks display the destination URL in plain text to every teammate with access.

Headers are not magic, but the default tooling treats them differently. Most proxies do not log request headers, most dashboards mask them, and browsers do not store them in history. You are moving the secret from a place everything records to a place most things ignore.

The minimum fix: a header token

The smallest upgrade is to require the token in a header and keep the URL clean.

POST /hooks/lead-intake
X-Agent-Token: <secret>

On the receiving side, the check is three steps:

  1. Read the header. If it is missing, return 401 and do nothing else.
  2. Compare it to the stored secret using a constant-time comparison, not a plain string equality check. Plain equality can leak timing information, and the fix costs one function call in any language.
  3. Only after the check passes, parse the body and hand it to the agent.

Order matters. A surprising number of agent endpoints parse and even log the payload before authenticating the caller. If your agent reads untrusted text from the body, you want that text nowhere near the model until the caller has proven who they are.

The better fix: sign the request

A shared header token proves the caller knows the secret. It does not prove the body was not changed in transit, and it does not stop someone who captured one request from replaying it forever.

A signature fixes both. The sender computes an HMAC over the timestamp and the raw body using the shared secret, and sends the result in a header:

X-Agent-Timestamp: 1792587600
X-Agent-Signature: sha256=<hmac of timestamp + "." + body>

The receiver recomputes the HMAC over the raw bytes it received and compares in constant time. It also rejects any request whose timestamp is more than a few minutes old. Now the secret never travels at all, a modified body fails verification, and a captured request expires on its own.

This is the same pattern major payment and code-hosting providers use for their webhooks. If the service sending you events already signs them, use that and verify it. Do not downgrade to a URL token just because the setup page makes it easy.

Two details that bite people:

  • Verify the raw body. If your framework parses the JSON first and you re-serialize it to check the signature, whitespace and key order will differ and verification will fail randomly. Capture the raw bytes before parsing.
  • Decide what a replay means. The timestamp window blocks old replays. For anything that spends money or sends messages, also store a request or event ID for a day and refuse duplicates. This pairs naturally with idempotency keys, which you should already have on write actions.

Give each caller its own secret

One shared secret for every integration means one leak compromises all of them, and you cannot tell which caller was responsible. Issue one secret per source: the form tool, the billing system, the other server’s cron. Name them in a small table and log which one authenticated each request.

That table also becomes your rotation checklist. When a teammate leaves or a vendor has an incident, you know exactly which secrets to replace and which callers to update.

Rotate without an outage

Rotation is where good intentions die, because replacing a secret breaks every caller at once. The fix is to accept two secrets during a short overlap window:

  1. Generate the new secret and add it alongside the old one on the receiver.
  2. Update the callers one at a time to send the new value.
  3. Watch the authentication log. When no request has used the old secret for a day, remove it.

Because you log which secret matched, step three is a lookup, not a guess. Put a calendar reminder on a 90-day cycle and treat an unexpected use of an old secret as a signal worth investigating.

Keep the secret out of the agent’s reach

One more boundary specific to AI agents: the model should never see the webhook secret. Store it in the environment or a secrets file readable only by the process that handles the request, not in the agent’s workspace, memory notes, or prompt. An agent that can read its own authentication secret can be talked into repeating it. The receiving code verifies the request; the agent only ever sees the payload that passed verification.

If you are on a platform that lets you configure hook paths and tokens in a config file, check whether it supports header auth and prefers it. If it only supports a path token, put the endpoint behind a reverse proxy that enforces the header check before forwarding.

A five-minute audit

Run this against your own setup today:

  • List every webhook endpoint your agents expose.
  • For each one, note where the secret lives. If it is in the URL, mark it for migration.
  • Search your proxy and tunnel logs for the token string. If you find it, treat that secret as already exposed and rotate it.
  • Confirm the body is not parsed or logged before authentication.
  • Confirm each caller has its own secret and that you know the rotation date.

Most setups fail at least two of these. None of the fixes require new infrastructure, just a header check, a signature, and a habit of rotating.

The principle

Automation endpoints are doors into a system that can act on your behalf. A door whose key is printed on the outside of it, in every log and screenshot, is not a lock. Move the secret to the header, sign what matters, give each caller its own key, and keep the whole thing out of the model’s reach. It is a one-afternoon change that removes one of the most common ways a self-hosted agent gets triggered by someone it should never have listened to.

More from the build log

Suggested

Want the full MarketMai stack?

Get the core MarketMai guides and operator playbooks in one premium bundle for $49.

View Bundle