Authentication
There are three ways to authenticate. Two of them are machine credentials, served on different hostnames, and you are onboarded onto exactly one of the two - you do not choose at call time and you cannot switch by changing a header.
OAuth2 client_credentials against a Cognito user pool. Most partners.
A client certificate presented on the TLS handshake. No token, no token endpoint.
The third is not a machine credential and is the one case where a choice is made: if you are
one of a partner’s own staff pointing an MCP client at POST /v1/mcp, you can
sign in interactively as yourself instead of being
issued either of the above. That choice is made once, when you configure the client.
Whichever you hold, the outcome is the same: your credential resolves to one partner
identity, with one set of granted sites and one set of capabilities. GET /v1/me reports
which method resolved your request in its authMethod field - COGNITO_M2M, MTLS, or
COGNITO_USER for interactive sign-in.
Bearer tokens
Section titled “Bearer tokens”Obtaining a credential
Section titled “Obtaining a credential”Partner Integrations provisions a Cognito app client for you and returns a client_id and a
client_secret. The client is created for client_credentials only, with no callback URLs
and no username or password flows - it is a machine credential and cannot be used in any
browser flow.
Alongside it they configure two things that are not part of the credential and not something you can extend yourself:
- the site ids you are granted, and
- the capabilities your partner record carries, from which your scopes are derived.
Minting a token
Section titled “Minting a token”curl -s -X POST \ "https://api.partners.collaterate.com/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=$CLIENT_ID" \ -d "client_secret=$CLIENT_SECRET" \ -d "scope=partner-api/orders:read"The token endpoint is on the same host as the API itself:
| Environment | Token endpoint |
|---|---|
| Production | https://api.partners.collaterate.com/oauth2/token |
| Staging | https://api.partners.collaterail.com/oauth2/token |
| Test | https://api.partners.test-b-collaterate.net/oauth2/token |
It is unversioned, as are the other /oauth2/... paths that serve interactive sign-in. Everything
else lives under /v1/, because everything
else is ours to version; the request and response here are plain OAuth 2.0 client credentials
(RFC 6749 §4.4), so your existing
OAuth client library can talk to it unchanged.
You may also send client_id and client_secret as HTTP Basic credentials in an Authorization
header instead of in the form body. Both are accepted; pick whichever your HTTP client makes
easier, and prefer whichever keeps the secret out of your logs.
Scopes are space-separated and each is prefixed with the resource server identifier
partner-api/. Two exist:
| Scope | Grants |
|---|---|
partner-api/orders:read |
GET /v1/orders and GET /v1/orders/{orderNumber}. |
partner-api/sites:read |
Reading the list of sites granted to you. No endpoint in v1 requires it; GET /v1/me already returns your granted site ids and needs no scope at all. |
Send the token on every request:
curl -s -H "Authorization: Bearer $TOKEN" \ https://api.partners.collaterate.com/v1/ordersThe Bearer prefix is required and matched case-insensitively. An Authorization header
that is present but does not start with Bearer is rejected outright.
Token lifetime and caching
Section titled “Token lifetime and caching”Read expires_in from the token response and cache the token for slightly less than that.
Do not hardcode a lifetime, and do not mint a fresh token per request - the token endpoint is
rate limited by Cognito independently of this API’s own limits, and a per-request mint is the
easiest way to throttle yourself out of service.
Refresh tokens are not part of client_credentials. When a token expires you mint a new one
with the same client id and secret.
Rotating a client secret
Section titled “Rotating a client secret”Two live app clients on one partner is a supported state, not a mistake. Rotation is:
- Ask Partner Integrations for a second credential. It is issued with the same grants and capabilities as the first.
- Deploy the new client id and secret to your integration.
- Confirm traffic has moved -
GET /v1/mereturns the samepartnerIdfor both, so verify on your side rather than ours. - Ask for the old credential to be revoked.
Revocation deletes the app client rather than disabling it, so it cannot be re-enabled by
mistake later. Once revoked, tokens already minted from it stop being accepted on their next
use: your credential is resolved from the token’s client_id on every request, not trusted
for the life of the token.
Mutual TLS
Section titled “Mutual TLS”How it differs
Section titled “How it differs”There is no token, no token endpoint, and no Authorization header. Your client certificate
is presented during the TLS handshake with mtls.partners.collaterate.com, and the
certificate itself is your credential.
curl -s \ --cert client.crt --key client.key \ https://mtls.partners.collaterate.com/v1/meThis applies to every path on that host, including /v1/ping. Ping needs no application
credential, but the handshake still demands a certificate, so a bare curl against
mtls.partners.collaterate.com/v1/ping fails at the TLS layer rather than returning a
liveness answer. Use the bearer host for unauthenticated liveness checks.
What is actually being checked
Section titled “What is actually being checked”Two independent things, and the difference matters:
- The TLS handshake checks that your certificate chains to a CA in the truststore for that domain. This is a transport-layer check, and it is not authentication - a CA can issue a certificate to anyone.
- The service then binds the specific certificate to a partner record by its subject distinguished name and its serial number. A certificate that chains correctly but is not registered resolves to no partner and is rejected.
Certificate requirements
Section titled “Certificate requirements”| Requirement | Value |
|---|---|
| Chain | The complete chain of trust, from the issuing CA up to the root CA. |
| Maximum chain length | 4 |
| Signature | SHA-256 or stronger |
| Keys | RSA-2048 or stronger, or ECDSA P-256 / P-384 |
| Self-signed | Permitted |
| Expiry | Must not already be expired at the time it is registered |
Onboarding a certificate
Section titled “Onboarding a certificate”- You generate the key pair and certificate. The private key never leaves your side and is never sent to Collaterate. Send only the certificate, PEM-encoded.
- Partner Integrations registers it. The certificate is parsed, checked for expiry, and recorded against your partner along with its subject DN and serial.
- The truststore is rebuilt. The bundle served to the mTLS domain is derived from the full set of enabled partner certificates every time, never appended to. That is what makes removal possible later.
- The new bundle propagates. API Gateway takes several minutes to distribute a truststore change. A handshake attempted immediately after registration can still be rejected; wait and retry before reporting a problem.
- You verify. Call
GET /v1/meover mTLS. A200with yourpartnerIdand"authMethod": "MTLS"means the whole chain works.
Renewing before expiry
Section titled “Renewing before expiry”Register the replacement certificate before the current one expires. Both can be live at once - they are separate credentials against the same partner - so the sequence is register, wait for propagation, cut over, then ask for the old one to be revoked. Doing it the other way round leaves a gap during which nothing you hold is trusted.
Nothing notifies you that a registered certificate is approaching expiry. API Gateway validates the truststore when it is updated and reports invalid certificates then; it does not warn about one that lapses later. Track your own expiry dates.
How your scopes are resolved
Section titled “How your scopes are resolved”Your effective scopes are not simply what you asked for. The rule differs slightly per method, and both end in the same place.
Bearer tokens. The scopes in your token are intersected with the capabilities on your
partner record. The database grant is the ceiling: a token can narrow what you may do but can
never widen it. Requesting a scope your partner record does not carry does not fail the token
request; it just does not appear in the effective set, and GET /v1/me will show you the
difference.
Mutual TLS. There is no token and therefore no requested scope set, so you receive your partner record’s capabilities directly.
Interactive sign-in. A bearer token as far as this rule is concerned - the token your MCP client holds is intersected with your partner record’s capabilities exactly as above. Signing in as a named person grants nothing your partner record does not already carry.
Either way, GET /v1/me’s scopes array is the authoritative answer to “what can this
credential actually do”.
Interactive sign-in for MCP clients
Section titled “Interactive sign-in for MCP clients”This is the third method the introduction points at. The two schemes above are for machine
integrations; if you are one of a partner’s own staff using an MCP client – Claude Code, Gemini
CLI, Kiro – against POST /v1/mcp, you can sign in interactively as yourself rather than
configuring a shared machine credential.
Add the server by URL only. No header, no client secret, nothing to paste in:
claude mcp add --transport http partner-api https://api.partners.collaterate.com/v1/mcpYour first tool call opens a browser to sign in with your Collaterate staff account. Once authenticated, the client holds a refresh token and silently renews your session for about 24 hours before prompting you to sign in again.
This resolves to the same PartnerPrincipal and the same scopes any other credential would,
scoped to your own partner – it is a different door into the same authorization model, not a
separate API.
When authentication fails
Section titled “When authentication fails”Authentication is resolved before your request reaches any endpoint, and every failure is a
401. The conditions that produce one:
- No credential, or an
Authorizationheader that is not a bearer token. - A token that is expired, malformed, or was not issued by this environment’s user pool.
- A client certificate whose subject DN and serial do not match a registered credential.
- A credential that resolves to a partner who is disabled.
- A credential that resolves to a partner with no granted sites.
- A credential whose effective scope set works out empty.
401 responses are produced by the API gateway before your request reaches any application
code. They still arrive as an application/problem+json document with code: invalid_token
and a requestId, because the gateway is configured to shape them that way - so one parser
and one branch on code covers authentication failures alongside every other error. See
Errors.
What the gateway cannot give you is a reason. Because the refusal happens before any handler
runs, detail is a fixed generic string and the requestId correlates to the authorizer’s
log entry rather than to a request the service ever processed.
The response is also identical for every one of the causes above. That is deliberate: a
credential is either accepted or it is not, and a 401 that explained which check failed
would help an attacker probing with guessed values considerably more than it would help you.
If you cannot tell why a credential stopped working, contact Partner Integrations rather than
inferring it from the response.