Skip to content

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.