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.
| Field | Notes |
|---|---|
| Name | For you. Shown in the client list. |
| API key | Which key it acts as. Chosen at creation and not changeable afterwards. |
| Expires in | Optional, 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"| Status | Error | Cause |
|---|---|---|
200 | The credentials verified | |
400 | invalid_request | grant_type missing |
400 | unsupported_grant_type | A grant other than client_credentials |
401 | invalid_client | Unknown client, wrong secret, disabled, or expired |
405 | invalid_request | Anything 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 issued | Reversible | For | |
|---|---|---|---|
| Disable | Refused while off | Yes | Pausing an integration |
| Rotate secret | Deleted | No | A secret that may have leaked |
| Revoke | Deleted | No | An 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 key | Effect on its clients |
|---|---|
| Disabled | None. The key stops working as a header; its clients keep working. |
| Expired | Its clients stop. Expiry is the end of the identity, not of one credential. |
| Deleted | Its 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 tokensPOST 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.