Ensemble Docs

OAuth clients

Client credentials built on an API key, the token exchange, rotating a secret, revoking, and how a client's state relates to its key's.

An OAuth client is a second credential for one API key. The caller holds a client id and secret, exchanges them for a short-lived access token, and sends that token instead of the key.

The point is that nothing long-lived travels on the wire. The secret goes to the token endpoint and nowhere else; the token is what every other request carries, and it expires on its own.

Creating one

A client is created on an API key, and that binding is fixed. It authenticates as that key, reaches exactly what that key reaches, and there are no separate permissions to grant.

FieldNotes
NameFor you. Shown in the client list.
API keyWhich key it acts as. Chosen at creation and not changeable afterwards.
Expires inOptional, 1 to 3650 days. Omitted means it never expires.

The key cannot be changed later, deliberately. Repointing a credential at a different identity is not an edit, it is a new credential, and treating it as an edit would mean a client silently gaining whatever the other key could reach.

The client secret is in the creation response and nowhere else, ever again.

Getting a token

curl -X POST https://service.ensembleapp.dev/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=cli_... \
  -d client_secret=cls_...

Credentials may also be sent as HTTP Basic, which the specification prefers:

curl -X POST https://service.ensembleapp.dev/oauth/token \
  -u 'cli_...:cls_...' \
  -d grant_type=client_credentials
{ "access_token": "ens_at_...", "token_type": "Bearer", "expires_in": 3600 }

Only client_credentials is supported, which is the whole of what a machine caller needs.

Then use the token anywhere an API key is accepted:

curl https://service.ensembleapp.dev/external/v1/benefits/coverage/E-4417 \
  -H "Authorization: Bearer $TOKEN"
StatusErrorCause
200The credentials verified
400invalid_requestgrant_type missing
400unsupported_grant_typeA grant other than client_credentials
401invalid_clientUnknown client, wrong secret, disabled, or expired
405invalid_requestAnything other than POST

The 401 is one answer for four causes deliberately: distinguishing them would let someone enumerate client ids. When a request you believe is correct gets it, the server log says which of the four it was.

Three ways to stop a client

Tokens already issuedReversibleFor
DisableRefused while offYesPausing an integration
Rotate secretDeletedNoA secret that may have leaked
RevokeDeletedNoAn integration that is over

Disable is the one to reach for first. The client's tokens are refused while it is off rather than deleted, so turning it back on restores them with nothing reconfigured.

Rotating issues a new secret and keeps the client id, so the caller is reconfigured with one new value rather than two, and there is no window where neither credential works.

Revoking deletes the client and its tokens. Unlike letting a token expire, this takes effect immediately.

How a client relates to its key

This is the part that surprises people, so it is worth stating plainly:

To the keyEffect on its clients
DisabledNone. The key stops working as a header; its clients keep working.
ExpiredIts clients stop. Expiry is the end of the identity, not of one credential.
DeletedIts clients are deleted with it, and their tokens.

Disabling a key is not a way to stop an OAuth-protected integration, including an event trigger using the OAuth mode. That is what the client's own switch is for. The asymmetry is deliberate: avoiding a long-lived key on the wire is most of the reason to use a client, so disabling the key and keeping the clients is a posture to support rather than prevent.

Tokens

Tokens are opaque strings the platform stores, not signed JWTs. That is what makes revocation immediate: there is a row to delete, rather than a signature that stays valid until it expires.

A token authenticates as the API key behind its client, so everything downstream that decides what a caller may reach is unchanged, including grants, per-key rate limits and the request log. None of them needs to know a token was involved.

The API

GET    /api/oauth-clients                             List this tenant's clients
POST   /api/oauth-clients                             Create one against an API key
PATCH  /api/oauth-clients/{clientId}/disable          Switch it off, reversibly
PATCH  /api/oauth-clients/{clientId}/enable           Switch it back on
POST   /api/oauth-clients/{clientId}/rotate-secret    New secret, same client id
DELETE /api/oauth-clients/{clientId}                  Revoke it, and its tokens

POST takes name, the apiKeyHash to act as, and an optional expiresInDays.

These endpoints refuse a credential, even one whose role would otherwise qualify. An API key carries the user id of whoever created it, so a leaked key could otherwise issue clients that survive the key being rotated. Minting credentials with a credential is a loop worth refusing.

On this page