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
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 thePatchwork-Subject header, it must stay server-side.
Direct sends a session token your backend signed. Here the proof is a signature, not a secret:
issnames an API key public id, which selects your workspace and the key to check against. This is routing only.- The signature must verify against the public key you registered — your JWKS document, or a key pasted in the dashboard.
- 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. audmust includepatchwork, andexpmust still be in the future.
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 aPatchwork-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 notsk_…. The public id belongs in a token’siss. The secret belongs in anAuthorizationheader 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.