Skip to main content
Patchwork and your backend call each other, so each side needs a way to prove it is itself. That is four secrets in total. They are easy to confuse because two of them sign HTTP requests and two of them end up in an Authorization header — so start from the direction each one points.

The four secrets

There is also an API key public id (key_…). It is not a secret. It goes in a session token’s iss claim to name your workspace, and it authorizes nothing on its own.

The two directions

Nothing crosses the middle. A secret that works inbound is never used outbound, and rotating one has no effect on the other.

Inbound: how we know it is you

The form of the bearer token selects the mode. You do not declare one. Relay sends the workspace API key. The secret is the proof — we hold a hash of it, compare, and resolve the workspace. Because that key can assert any subject in your workspace through the Patchwork-Subject header, it must stay server-side. Direct sends a session token your backend signed. Here the proof is a signature, not a secret:
  1. iss names an API key public id, which selects your workspace and the key to check against. This is routing only.
  2. The signature must verify against the public key you registered — your JWKS document, or a key pasted in the dashboard.
  3. The algorithm must be asymmetric. HS* is refused, always. Accepting a symmetric algorithm while holding your public key would let anyone sign a token with a value we publish.
  4. aud must include patchwork, and exp must still be in the future.
A token that fails any of these is rejected outright. It is never downgraded to relay.
You do not sign your requests to us. The bearer token is the whole credential on the inbound leg, which is why sk_… stays on your server and a session token’s exp should sit around 120 seconds.

Outbound: how you know it is us

Every request we make to something you host carries a Patchwork-Signature header — an HMAC over a shared secret, covering the method, path, query and body. Tool calls, the relay mint call, and the health probe are keyed with the workspace request secret. Webhook deliveries are keyed with that endpoint’s own secret. The verification code is identical; only the key differs. Signing and verification has the payload format, the rotation rules, and the six details that break implementations.

Why one is shared and one is not

A shared secret is the right tool for “is this Patchwork?” We are the only other party who could produce the value, so holding the same bytes proves nothing more than the question asks. It is the wrong tool for “who is this user?” If we held a secret that could mint a token for one of your users, then a breach of us would be a breach of every account you have. So identity runs the other way: you keep the private key and we store only the public half. We can verify a subject. We cannot invent one. What we actually store for your workspace is the request secret and its predecessor, your public verification key or JWKS URL, and a hash of each API key secret. No private key of yours is in it.

What you need, by what you are building

The last two rows are the ones people get wrong. A signing key is not enough for direct integration — the moment an agent calls an endpoint you host, you also verify signatures.

Easy to get wrong

  • The request secret is not your signing key. The request secret is symmetric and shared, and you use it to verify calls from us. Your signing key is asymmetric and private, and you use it to sign tokens. You will not find a private key anywhere in the dashboard — we never hold one.
  • key_… is not sk_…. The public id belongs in a token’s iss. The secret belongs in an Authorization header on your own server, and never inside a token.
  • A session token is not a signature. On a minted connection an egress request carries both, because they answer different questions: the signature says the request came from Patchwork, the token says which of your users it is acting for. Verify both. Neither substitutes for the other.
  • A signature header is a list, not a map. During a rotation it carries one value per live secret. Accept the request if any value matches any secret you hold.

Rotation

Every rotation keeps the previous value working for a bounded window, so you can deploy the new one without an atomic switchover. Nothing in flight fails, and the old value stops being accepted when the window closes. During a request-secret or webhook-secret overlap we sign with every live secret and send one v2 value per secret, so the header carries two. That is what lets either side be ahead of the other: a backend still verifying against the old value keeps working, and one that has deployed the new value works too. Accept the request if any value matches any secret you hold — which is why parsing the header as a list is not optional.

If a secret leaked

Do not wait out the window. Rotate, deploy the new value, then Revoke previous — the old secret stops being accepted immediately.