How HTTP request signatures bound AI agent API calls
HTTP request signatures bind AI-operated API calls to a method, target, body, and time. Learn secure fields, replay controls, and clock drift rules.

An AI agent with a bearer token has the same practical freedom as any other process that can read that token. It can call a permitted endpoint now, retry it later, send the token to a different host if the code permits it, and make a request whose body has little connection to the user instruction that started the run. The server sees possession of the credential and little else.
HTTP request signatures improve that situation by binding authorization to a particular message. A sound design can make a signature valid only for POST https://api.example.test/v1/releases/42, only with the exact body the agent prepared, only for a short time, and only under one signing identity. That is useful control. It is not a substitute for authorization, approval, or credential isolation.
Teams get this wrong in two opposite ways. Some keep bearer tokens because signing appears complicated, then discover that an agent can use a broadly scoped token anywhere it can make an outbound request. Others sign every header their HTTP library emits and build a verifier so fragile that ordinary proxies, clock skew, or a library update breaks production. The right design binds fields that change the meaning or destination of an action, then makes freshness and verification boring enough to operate.
Signatures constrain a message while bearer tokens only prove possession
A bearer token answers one question: did this request present a currently accepted secret? It does not normally bind the secret to the request method, target, body, or time. OAuth access tokens can carry scopes, audiences, and expiration, which helps, but the holder can still use each allowed scope until expiry.
A request signature answers a narrower cryptographic question: did the holder of this signing key authorize this defined set of request components? The server reconstructs the signed input, verifies it with the registered public key or shared secret, checks the time bounds, then applies normal authorization. That ordering matters. Signature verification establishes message integrity and signer identity. Authorization decides whether that identity may perform the action.
Consider a deployment service with a bearer token scoped to release creation. An agent first creates a harmless staging release. A prompt injection later directs the same agent to create a production release. The token lets both requests through if its scope covers both environments. Replacing the token with a signed request does not fix that authorization error by itself. If the signing identity may create production releases, it can sign a production request.
Signatures do give the service protections that an ordinary bearer header does not:
- A captured signed request expires quickly and cannot be replayed forever.
- A copied signature cannot usually move from
POST /v1/staging/releasestoPOST /v1/production/releases. - A changed JSON body fails verification when the body digest is signed.
- The service can identify a signing credential without accepting it as a reusable header value.
- A verifier can record exactly which fields the signer approved.
Do not describe this as replacing access tokens in every architecture. Many systems use both. An access token may identify the delegated user or workload, while a request signature binds the individual message to an agent-specific signing credential. In other systems, a signed request authenticates the caller directly and the server derives permissions from the signing identity.
The distinction has a practical consequence for AI agents: a signature makes the action visible as a concrete object that you can inspect, approve, and log. A bearer token mostly appears as an ambient capability. That difference makes review possible, but only if your tool boundary prevents the agent from extracting the signing secret.
Use RFC 9421 instead of inventing a canonical string
RFC 9421, HTTP Message Signatures, defines a structured way to declare covered components and send a signature. It avoids the usual private format where one side joins fields with newline characters, another side normalizes a URL differently, and both sides blame cryptography when verification fails.
The RFC separates two ideas. Signature-Input declares a label, the components covered, and parameters such as created, expires, nonce, alg, and keyid. Signature carries the resulting cryptographic value under the same label. The verifier builds the signature base from the declared components and parameters, then verifies it.
A compact request might look like this:
POST /v1/releases/42?environment=staging HTTP/1.1
Host: api.example.test
Content-Type: application/json
Content-Digest: sha-256=:rPMyV6WTE4Duf0JApE9tXvDYy9EzrgFQbq3e2XTCwbs=:
Signature-Input: sig1=("@method" "@authority" "@path" "@query" "content-digest" "date");created=1735689600;expires=1735689660;keyid="agent-release-17";alg="ed25519"
Signature: sig1=:BASE64_SIGNATURE_BYTES:
Date: Wed, 01 Jan 2025 00:00:00 GMT
{"version":"2025.01.01","notes":"staging validation"}
The digest value above illustrates the wire shape, not a digest for the example JSON. A production client computes the digest from the exact bytes it will send. If it serializes JSON a second time after signing, it has built a failure generator.
RFC 9421 is deliberately flexible. That flexibility is useful for intermediaries and different HTTP versions, but it means your API contract must name an exact profile. State the permitted algorithm, mandatory covered components, maximum signature lifetime, keyid format, accepted digest algorithm, and whether a nonce is required. If the contract merely says requests must be signed, every client author will make a different set of assumptions.
Ed25519 is a sensible default when your service can register public keys. The server stores a public verification key, and loss of that public record does not expose a signing secret. HMAC signatures can work when one trusted component and the API share a secret, but that secret must exist on both sides. For agent workflows, that often increases the number of places where a reusable secret can leak.
Avoid a proprietary scheme unless a protocol constraint forces it. Proprietary formats tend to sign the raw URL in one client, a decoded path in another, and a different host representation in the verifier. RFC 9421 gives you defined component identifiers and structured fields. Use them, then test the exact profile you publish.
Bind the method, destination, and query that define the action
An agent should sign every request field that changes where the request goes or what server operation it invokes. For most API calls, the minimum useful set is @method, @authority, @path, and @query. You may use @target-uri instead when you want one component that covers the full target URI, but do not sign both formats without a reason your implementers can explain.
@method prevents someone from reusing a signature intended for GET as DELETE. This sounds obvious, yet hand-built schemes regularly omit it because an engineer assumes the path implies the operation. REST APIs often reuse a path with different methods. The method changes the action.
@authority binds the host and port authority. It prevents a signature issued for one API origin from validating against another origin that accepts the same credential. This matters in organizations with preview, staging, and production hosts. A signing identity intended for staging should not acquire production authority because an agent or a redirect changed the host.
@path and @query need equal care. Many APIs put meaningful parameters in the query string:
POST /v1/invoices/817/refund?amount=2500¤cy=USD
If the signature covers only the path, an attacker who can alter the request in transit can change the amount or currency. If the server takes dry_run, environment, force, page_size, include_deleted, or a tenant selector from the query, those values are part of the action. Sign @query.
RFC 9421 also defines @query-param, which can cover a specific named parameter. It has a place in protocols where some query parameters are explicitly outside the security decision, such as tracing data. For an internal API used by agents, signing the entire query is usually less surprising. Every parameter becomes part of the approved request, and reviewers do not need to remember an exception list.
Do not treat URL normalization casually. Your verifier must use the component semantics specified by RFC 9421 and your chosen library. Do not decode percent escapes and re-encode them by hand. Do not sort repeated query parameters unless the selected component definition does that. A request target is bytes on the wire before it becomes a convenient application object.
Redirects deserve a firm rule: do not automatically carry a signed request across a redirect to a different authority. A signature that covers the original authority must fail at the new host, which is correct. Let the client receive the redirect, apply an explicit allowlist, construct a new request, and sign that new request. For mutating requests, many teams should reject redirects outright.
A signed body needs a digest, not optimism about JSON
Sign content-digest when the body influences the result. That includes nearly every JSON mutation request, multipart upload, form submission, and bulk operation. Signing content-type can also make sense when the server parses the same bytes differently under different media types.
The IETF defines Content-Digest in RFC 9530. It carries a digest of the HTTP message content using Structured Fields syntax. The signature covers the digest header rather than an enormous body directly, while the recipient computes the body digest and compares it before accepting the signature. This gives the signature a fixed-sized representation of the exact content.
A safe send sequence is simple, and it must remain in this order:
- Build the final request object, including query parameters and headers that affect interpretation.
- Serialize the body once into bytes and retain those bytes for transmission.
- Compute
Content-Digestover those bytes. - Create the
Signature-Inputover the chosen components, then sign its signature base. - Send the unchanged request bytes and headers.
The common failure is more mundane than a cryptographic attack. An application serializes an object to calculate the digest, signs it, then an HTTP helper serializes the object again. JSON object member order, escaping, whitespace, numeric formatting, or a timestamp field changes. The verifier correctly reports a digest mismatch. Developers then remove body protection to get the release out. That is the wrong repair.
Pass a byte buffer, stream, or immutable request body to the transport. If streaming makes a complete digest impossible before transmission, use a protocol designed for that case and test it thoroughly. Do not quietly omit a body digest from a high-impact operation because streaming was inconvenient.
Headers deserve a narrower rule. Sign a header when a recipient or intermediary can use it to change the request's security meaning. content-type is a candidate. A tenant header is a candidate if the server uses it to choose an account. An idempotency header is a candidate if retries and duplicate effects matter. A diagnostic header is usually not.
Signing user-agent, accept, trace IDs, connection headers, and every header your library emits creates brittle clients. Proxies may add, combine, or rewrite ordinary headers. HTTP has legitimate transformations. You want the signature to reject semantic changes, not to turn harmless transport variation into an outage.
Freshness windows should tolerate drift but reject queued work
Time limits make captured signatures short-lived. They also cause needless incidents when teams pretend every workstation, container, and VM has perfect time. The answer is a bounded acceptance rule and decent clock synchronization, not a two-hour grace period.
Use created and expires in Signature-Input. A sixty-second lifetime works well for interactive action calls when the agent signs immediately before sending. A few minutes may be appropriate for an unreliable network or a workflow that retries after a transient failure. The API should document one maximum lifetime and enforce it. Do not let clients choose an arbitrary expiry because they find expiration inconvenient.
The verifier should evaluate three cases separately:
- Reject a request whose
createdtime sits too far in the future, beyond a small configured skew allowance. - Reject a request whose
expirestime has passed. - Reject a request whose lifetime,
expires - created, exceeds the API maximum even if it has not expired.
A server can accept a client clock that is slightly behind or ahead without accepting stale work. For example, a service might allow a modest future skew and a short expiry interval. The exact values depend on where clients run, but the principle does not change: a drift tolerance is not a replay window you leave open all afternoon.
Do not use the HTTP Date header as the only freshness mechanism. It can be useful as a covered component for compatibility and diagnostics, but created and expires live directly in the signature parameters and are less ambiguous. If you sign both, state which values the server uses for enforcement when they disagree. A verifier that accepts either value gives attackers an unnecessary edge.
Queued agent tasks expose a hidden design error. Suppose an agent creates signed requests at 09:00, a human approval waits until 09:20, and a worker sends the old request after approval. The service should reject it. The task needs a new signing event after approval, because the approved action should have a meaningful current time and current destination.
For retryable operations, store an idempotency identifier in a header or signed body field, then cover it with the signature. The client can create a fresh signature for each retry while the server recognizes the logical operation and avoids duplicate side effects. Reusing an expired signed request is not a retry strategy.
Nonces stop replay only when the server remembers them
A short expiry limits replay, but it does not stop an attacker from replaying a captured request repeatedly during that window. Whether that matters depends on the endpoint. Replaying a harmless read has little effect. Replaying a money transfer, account deletion, or infrastructure change can be serious.
A nonce addresses that risk when the server treats each nonce as single use for a signing identity. The client generates an unpredictable value, puts it in the signature parameters or a covered header, and the server records successful use until the signature expires. A second request with the same signer and nonce fails even if its signature remains valid.
This is the part teams skip when they say they use nonces. A nonce that the server does not retain is just an extra random string. It does not prove uniqueness. A shared cache or database table needs an atomic create operation so concurrent replays cannot both pass verification.
Use a nonce store where replay damage warrants the operational cost. You need to choose its retention period, size bounds, failure behavior, and partitioning. Retain an accepted nonce until the latest time the server could accept the request. Scope records by signer identity and nonce, not by nonce alone, because separate signing identities can generate the same value without security impact.
Do not make a nonce mandatory for every low-risk, high-volume call simply because it sounds safer. That choice can turn an API outage in the nonce store into an outage for harmless reads. A practical profile might require it for irreversible mutations and rely on short expiry plus idempotency controls elsewhere. Write that rule endpoint by endpoint.
Nonce checks also do not replace idempotency. The nonce says this signed message must be accepted once. An idempotency identifier says several separately signed retry attempts represent one intended business operation. They solve different failures.
Verification must fail closed before application code sees the request
The API gateway or application entry point should verify the request before a route handler parses action parameters, starts a job, or consults downstream services. A handler that reads a body and performs work before verification has already surrendered the security property the signature was meant to provide.
A verifier needs a predictable sequence:
- Parse
Signature-InputandSignatureas Structured Fields, rejecting malformed syntax and duplicate ambiguity. - Select an allowed signature label and reject unknown algorithms, missing mandatory components, or prohibited component combinations.
- Resolve
keyidto an active signing identity and obtain its verification material. - Reconstruct the signature base according to RFC 9421, using the received request rather than a reconstructed application URL.
- Check the cryptographic signature, body digest, time bounds, nonce state where required, and then authorization.
Keep cryptographic failure and authorization failure separate inside your logs. Public responses can remain deliberately plain. A 401 or 403 with a stable error code is enough for callers. Internally, record whether the service rejected the request for an unknown keyid, expired signature, invalid digest, mismatched authority, reused nonce, or insufficient permission.
Do not log signature bytes as if they were harmless debugging material. Public-key signatures are not secrets in the way HMAC credentials are, but full request logs often contain authorization headers, personal data, and bodies. Log a request identifier, signer identity, covered component names, a digest value if your retention policy permits it, and the decision. Restrict raw capture to a deliberate incident procedure.
Test vectors matter more than prose. RFC 9421 includes examples, but your API profile needs its own fixtures. Maintain requests that must verify, plus mutations that must fail: a changed method, changed query, changed body byte, expired signature, future created value, wrong authority, altered keyid, and a repeated nonce. Run them in every supported client implementation.
A signature verifier should reject ambiguity, even if a permissive parser could guess the sender's intent. Duplicate headers, inconsistent component serialization, and unsupported algorithms are protocol errors. An agent does not need the server to be helpful; it needs the server to be exact.
Keep signing material outside the agent context
Giving an AI coding agent a private signing key or an HMAC secret defeats much of the point of signing. The key may appear in tool output, shell history, temporary files, error reports, or a prompt that instructs the agent to print its environment. Even a well-behaved agent has too much indirect surface area for a reusable credential.
Instead, expose a narrow action boundary. The agent supplies the method, permitted destination, headers, and body to a trusted local or remote component. That component validates the allowed target, obtains approval when your workflow requires it, builds the covered component list, signs immediately before sending, and returns the response. The agent receives neither a plaintext secret nor a fake placeholder that it can accidentally forward.
This boundary also makes authority review concrete. A signing identity can be limited to a service, environment, route family, and action class. If the agent only needs to open a staging release, do not grant it a credential that signs billing adjustments or production deletion calls. Server authorization remains responsible for enforcing those limits after signature verification.
Sallyport follows this separation for HTTP actions: the app holds API credentials in its encrypted vault and performs the HTTP call itself, so an MCP-capable agent receives the result rather than the plaintext credential.
Do not confuse an action gateway with a request-signature standard. RFC 9421 tells two HTTP parties how to authenticate selected message components. An action gateway decides where signing material lives, when a human sees an approval, and what audit record exists around an agent run. You may use one without the other, but the combination is useful when autonomous tools act on consequential APIs.
If you rely on a local signer, treat its local interface as an authorization boundary. Bind requests to the calling process where possible, reject arbitrary destinations, and ensure one agent process cannot silently use another process's approved session. A local service that signs any URL submitted by any local program has merely moved a bearer capability behind a socket.
Approval and signatures answer different questions
A human approval records that someone allowed a particular class of agent action. A request signature records that a signing identity authorized a defined HTTP message. Neither record proves the other unless your design explicitly binds them.
For high-impact calls, make the approval view show the fields the signature will cover: method, authority, path, material query parameters, body digest or a readable body summary, signing identity, and expiry. If a user approves POST /v1/releases/42?environment=staging, the signer must not later substitute production through an unsigned query parameter.
This is where digesting only an opaque body can hurt human review. The cryptographic digest proves byte identity, but it tells a person almost nothing. Keep both artifacts: a canonical request representation for review and a digest for integrity. The review view should derive from the exact immutable request object that the signer will send, not from a separately rendered plan.
Session approval can be appropriate for a short agent run with many low-impact calls. Per-call approval is better for deletion, external publication, financial changes, or any operation that an attacker could hide among routine traffic. Do not ask for approval on every read just to claim human control. People will click through, then you have created approval fatigue without a meaningful decision.
The audit record should capture the signer identity, covered components, signature lifetime, authorization decision, approval reference if one exists, response status, and a request identifier. A readable audit trail helps an operator answer a plain question after an incident: what did the agent send, under whose authority, and did the server accept it?
The failures worth rehearsing are mostly ordinary engineering failures
The most dangerous implementation bugs are not broken elliptic curves. They are unsigned fields, mismatched canonicalization, stale work, and secrets placed where agents can read them.
One failure appears when a team signs @method, @path, and date, but leaves @query out. Their release API accepts ?environment=staging during normal use. Later, a maintenance change adds ?environment=production to the same endpoint. A proxy bug or hostile local component changes the parameter after signing. The signature verifies, the handler sees production, and the audit record misleadingly says the agent sent a valid signed request. The signature did exactly what the component list asked. The component list was incomplete.
Another failure appears when developers accept ten-minute signatures to reduce clock-related tickets. An agent signs a delete request, writes the complete headers to a debug log, and a developer copies the log into an issue. Anyone with access to that issue can replay the request for most of the workday. Short expiry would not erase the original leak, but it sharply limits its usefulness. A nonce requirement for deletion removes the remaining replay window after first acceptance.
A third failure occurs with shared HMAC credentials. Several agents use the same secret because provisioning individual identities seemed like overhead. When an audit finds a destructive call, the team can identify the shared integration but not the agent run, the user approval, or the process that originated it. Give distinct signing identities to distinct authority boundaries. Attribution is part of incident response, not a reporting luxury.
Start with one mutating endpoint and write the profile before selecting a library. Specify its allowed authority, mandatory components, body digest rule, signature duration, skew allowance, replay rule, signing algorithm, and authorization mapping. Then build negative tests that mutate every covered field. If a test can alter a meaningful request field and still verify, do not ship the profile.
A signed request should be easy to reject for the right reason. That standard forces the design toward narrow authority, short-lived messages, immutable bodies, and logs that show what happened after an agent acts.
FAQ
What is the difference between a bearer token and an HTTP request signature?
A bearer token authorizes whoever possesses it. A request signature proves that the holder of a signing key approved a specific request shape, such as a particular method, target, timestamp, and body digest. It reduces replay and request substitution, but it does not decide whether the agent should have been allowed to act.
Do HTTP request signatures make autonomous agents safe?
No. A signature binds a request to a signing key, but an agent that can use that key can still sign a destructive request within its authority. You still need limited credentials, server-side authorization, approval for sensitive actions, and an audit trail.
Which HTTP fields should an agent sign?
Start with @method, @target-uri or @authority plus @path and @query, content-digest for requests with bodies, and a freshness value such as date or @created. Add headers only when the server makes a security decision from them. Do not sign irrelevant headers merely because an SDK happens to send them.
Should a request signature include the HTTP request body?
Sign content-digest whenever the body changes the action, which covers most POST, PUT, PATCH, and DELETE requests with JSON. Without a digest, an intermediary or a bug between signing and sending can replace the body while leaving the signature valid. A signed method and path do not protect an unsigned payload.
How should APIs handle clock drift in signed requests?
Use a short validity period and allow a small, explicit tolerance for normal clock error. Reject old requests, reject requests too far in the future, and make clients correct their clocks through normal time synchronization instead of widening the acceptance window indefinitely. If agents queue work for long periods, sign just before transmission.
Do signed API requests need a nonce?
A nonce can stop replay within a validity window if the server stores each accepted nonce per credential or signing identity. That storage has a cost and needs expiration, so many APIs use short-lived signatures for low-risk calls and add nonces only for transfer, deletion, or other actions where one replay is unacceptable. A nonce must be signed or an attacker can replace it.
Is RFC 9421 the right standard for API request signing?
RFC 9421 defines HTTP Message Signatures and gives standard component names such as @method, @authority, @path, and @query. It is a good wire-format choice when both client and server control the integration. It does not prescribe your authorization model, replay store, or signing-key distribution.
Should an AI agent hold the API signing key?
Usually no. A raw secret copied into an agent context turns every prompt injection, log leak, and tool bug into a credential incident. Give the agent a narrow action interface, let a separate trusted component hold the credential, and record the exact request that component sends.
Why do valid request signatures still get rejected?
First distinguish malformed signatures from valid signatures that lack permission, and use different status codes and internal reason codes. Keep public errors sparse so attackers cannot probe your verification logic, but log the failed component list, signer identifier, clock values, and canonical input on the server. Never log the signing secret.
Can HTTP request signatures replace TLS?
No. TLS protects the connection while it exists; a message signature travels with the request and lets the recipient verify selected fields. Most API calls need TLS regardless, because signatures do not hide request contents, response contents, or bearer tokens carried alongside them.