Authentication
API keys and OAuth clients, how each is minted and rotated, and what live mode asks for first.
Every request to https://api.cuvo.co carries Authorization: Bearer <credential>. Two kinds of
credential open the same door, and they resolve to the same principal: one integration, one mode,
one organization, one set of scopes.
API keys
An API key is what an integration uses for its own traffic. Mint it in the developer portal, behind a fresh multi-factor challenge. The plaintext is returned once and stored only as a hash, so a key you did not write down is gone rather than recoverable.
cuvo_sk_test_… the sandbox
cuvo_sk_live_… production
The mode is in the prefix, so you can tell the two apart before anything reads them. A key expires 90 days after it is minted. Rotating one mints a replacement and leaves the old key working for 24 hours, which is the window you have to deploy. Revoking one takes effect at once.
Never put a live key in a browser, a mobile binary or a repository. It is a bearer credential for protected health information.
curl https://api.cuvo.co/v1/organization \
-H "Authorization: Bearer cuvo_sk_test_…"
OAuth clients
An OAuth client is what a platform uses when it acts for the organizations that granted it access,
and it is how an agent reaches the MCP server. Create one at
/portal/<integration>/oauth-clients, under the same guard keys use. You get a client_id and a
secret shown once.
The authorization server is https://developers.cuvo.co. Its metadata is published at
https://developers.cuvo.co/.well-known/oauth-authorization-server.
Exchange the client credentials for an access token:
curl https://developers.cuvo.co/api/auth/oauth2/token \
-u "$CUVO_CLIENT_ID:$CUVO_CLIENT_SECRET" \
-d grant_type=client_credentials \
-d 'scope=patients:write cases:write events:read' \
-d resource=https://api.cuvo.co
client_secret_basic, as above, is the only accepted method. Every client is registered for
exactly one token-endpoint auth method, and that method is Basic; the same credentials sent in the
form body are refused.
resource is required. It is the RFC 8707 parameter, and it is the only thing the authorization
server sets the token's audience from. A token minted without it names no audience, and the API
refuses it at the door. Send resource=https://api.cuvo.co for the REST API, or
resource=https://mcp.cuvo.co for the MCP server.
The response is a signed JWT that lives for one hour. Present it as you would a key:
curl https://api.cuvo.co/v1/cases \
-H "Authorization: Bearer eyJhbGciOi…" \
-H "Cuvo-Organization: org_c8k2q1w9"
Those two are the only resources, and every client is linked to both. A token naming either is accepted at either door, because the two hosts are one deployment. A token naming anything else is refused, and so is a portal session token, which never names a client.
Rotating a client secret replaces it on the same client_id and takes effect immediately: the old
secret stops exchanging at once, while tokens already issued run out their hour. Revoking a client
stops issuance and closes the door inside the same request.
Each resource server says what it is at /.well-known/oauth-protected-resource, and a 401 from the
MCP door names that URL in its WWW-Authenticate header, so a client that has never seen Cuvo can
find its way in.
The device grant
A client on a machine with no browser starts at POST https://developers.cuvo.co/api/auth/device/code,
form-encoded, carrying client_id, scope and resource. The answer holds a device_code, a short
user_code for the person to confirm, the verification page to confirm it on, and an interval in
seconds. The client then polls the same token endpoint as every other grant, with
grant_type=urn:ietf:params:oauth:grant-type:device_code, the device_code, its client_id and the
same resource. Name the same resource at both steps: one the person did not authorize is refused
as invalid_target.
Poll no faster than interval. Until the person confirms, the token endpoint answers
authorization_pending; a poll that came too soon answers slow_down, and the client adds five
seconds to its interval; expired_token and access_denied mean there is nothing left to wait for.
The client must be registered for that grant type, which is checked both when the code is issued and
when it is redeemed, so a client registered for authorization_code alone is refused at both. This
is the flow cuvo login --device runs; see the CLI.
Agents and user consent
An agent that signs in as a portal member, rather than holding a client secret, picks the integration and the scopes it may use at the consent screen. That token is always a test mode principal. Reaching live data needs a live key or a live client, through the MCP server exactly as through the API.
What live mode asks for
A live credential is refused until the integration has accepted the business associate agreement
that is currently in force. The refusal is its own code, baa_required, because the answer is to
sign something in the portal rather than to change the request. Accepting a superseded version does
not count: when counsel publishes a new agreement, live access closes until someone signs it. Test
mode is untouched.
A live credential is also refused while the integration is pending activation, suspended or closed.
Failed attempts
A request with a missing, malformed, expired or revoked credential answers 401 unauthorized.
Failed attempts are rate limited per source address, 20 per minute, so guessing a key is throttled.
Successful calls are never limited by that counter; see Rate limits and
idempotency.