Skip to main content
When an agent calls an endpoint you host, the request carries a Patchwork-Signature header computed over your workspace request secret. If the connection uses minted egress, it also carries a short-lived token for the subject. Two credentials, two questions: They must never collapse into one, and neither substitutes for the other. This page covers the signature. For where it sits among the other secrets — which key points in which direction, and which ones you need — see The trust model.

The signature

A header carries one timestamp and one or more signature values. Each value has a label that says what it covers. Patchwork sends v2. During a secret rotation it sends one v2 per live secret, so a header can carry several. Each value is HMAC-SHA256 over your request secret.

v2

The same, with the query string included, and a literal v2. in front. Two rules matter:
  • The query is signed exactly as it arrived. No sorting and no re-encoding. ?b=2&a=1 and ?a=1&b=2 are different signatures, and so are %20 and +. Use the raw query string your server received, the way you use the raw body.
  • No query means no ?. If the request has no query string, the target is just the path.
The v2. prefix is there because without it the two labels would cover identical bytes for a request with no query, and one signature could be replayed as the other.

v1, retired

An earlier label, v1, covered the same payload without the query string:
Patchwork no longer sends it. It was retired because a GET tool’s arguments travel in the query, so a v1 signature did not cover them and a request whose arguments were changed in flight still verified. You do not need to implement it. If you already accept it, you can leave that code in place — nothing emits a v1 value, and nobody can produce one without your request secret.

Unknown labels

Ignore a label you do not recognise rather than rejecting the header. That is what lets a new label be added without breaking you, and it is how v2 was introduced.

Verifying

Six details that break implementations:
  • Use the raw body. Signing happens over bytes. Re-serialising parsed JSON changes whitespace and the digest will not match.
  • Use the raw query. Same reason. A parsed-and-rebuilt query string will have been sorted or re-encoded by your framework, and the digest will not match.
  • Compare in constant time. === on an HMAC leaks timing.
  • Collect every value, not one. A header is a list, not a map. During a rotation Patchwork sends one value per live secret, so t=…,v2=…,v2=… is normal. Parsing it into an object keeps only the last and the overlap silently stops verifying.
  • Verify against your current and previous secret. For 24 hours after you rotate, we sign with both, so a backend that has not yet deployed the new value keeps working. Accept if any value matches either secret.
  • Check the length before a constant-time compare. Node’s timingSafeEqual throws when the buffers differ in length, so a short or truncated value becomes a 500 instead of a 401.

Then the subject token

On a minted connection the request also carries a JWT for the subject. Verify it with your own public key, read sub, and hand off to your normal authorization:
Your existing per-user checks then run unchanged. Patchwork never widens what that user could already do.

The health endpoint

Pressing Test on a connection posts a signed nonce to /patchwork/up. Echo it with a proof only computable with the shared secret:
Returning a bare ok will report reachable, not verified — it cannot fake the proof. Only verified means the trust is wired correctly.

Webhooks use the same scheme

Webhook deliveries are signed identically, but keyed with the webhook endpoint’s own secret rather than the workspace request secret. Same verification code, different key. A delivery goes to the URL you registered and carries no query string, so its v2 target is just the path. During a secret rotation it carries one v2 per live secret, so a header can hold two values. Accept the request if any value matches any of your secrets.