Machine to machine
Let services and scripts call your API, or the Management API, with the client credentials grant.
Services, cron jobs and scripts get tokens for themselves with the client credentials grant: no user, just the application's own ID and secret. Use it to call your own APIs, or 9in's Management API.
#Call your API
- APIs & Scopes → Create API, e.g.
https://api.acme.comwith a scopereports:read(skip if it exists). - Applications → Create application → Machine-to-Machine. On its API Access tab, grant it the API and the scopes it may request. Copy the client ID and, from Credentials, the secret.
- Ask for a token:
curl -X POST https://acme.9in.io/oauth/token \
-u "app_svc123:$NINE_CLIENT_SECRET" \
-d grant_type=client_credentials \
-d audience=https://api.acme.com \
-d scope="reports:read"{ "access_token": "eyJhbGciOi…", "token_type": "Bearer", "expires_in": 3600, "scope": "reports:read" }Leave scope out to get every scope the application was granted for that API. Asking for a scope it wasn't granted fails with invalid_scope; an API it has no access to fails with access_denied; an unknown audience with invalid_target.
The token's sub and client_id are the application's client ID, and there is no sid: that is how your API can tell a machine from a user.
#In code
Cache the token until shortly before expires_in runs out; don't fetch one per request.
let cached: { token: string; until: number } | null = null;
export async function serviceToken(): Promise<string> {
if (cached && cached.until > Date.now() + 30_000) return cached.token;
const res = await fetch("https://acme.9in.io/oauth/token", {
method: "POST",
headers: {
Authorization: `Basic ${btoa(`${process.env.NINE_CLIENT_ID}:${process.env.NINE_CLIENT_SECRET}`)}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ grant_type: "client_credentials", audience: "https://api.acme.com" }),
});
if (!res.ok) throw new Error(`token request failed: ${res.status} ${await res.text()}`);
const { access_token, expires_in } = await res.json();
cached = { token: access_token, until: Date.now() + expires_in * 1000 };
return access_token;
}openid-client (client.clientCredentialsGrant(config, { audience })) and most OAuth libraries do the same.
#Call the Management API
Give the application management scopes on its API Access tab (management:users:read, management:users:write, management:sessions:write), then either use the SDK, which gets and caches tokens itself:
import { ManagementClient } from "@9in-io/sdk/server";
const nine = new ManagementClient({ issuer: "https://acme.9in.io", clientId: process.env.NINE_CLIENT_ID!, clientSecret: process.env.NINE_CLIENT_SECRET! });
const { user } = await nine.findUserByEmail("jane@acme.com");
await nine.updateUser(user.id, { metadata: { plan: "pro" } });or request a token without audience, with the management scopes you need:
curl -X POST https://acme.9in.io/oauth/token -u "app_svc123:$NINE_CLIENT_SECRET" \
-d grant_type=client_credentials -d scope="management:users:read"and call https://acme.9in.io/api/v1/users?q=jane with Authorization: Bearer <token>. The sandbox has its own users: call https://acme-sandbox.9in.io for them. See the Management API reference.
#Good to know
- Only confidential applications can use this grant, and management scopes can only be given to them.
- Removing a scope or deleting the application in the console takes effect at once for the Management API: every call re-checks the application.
- The access token lifetime is the application's setting if it has one, else the API's, else the tenant default (1 hour).