# Machine to machine

> Let services and scripts call your API, or the Management API, with the client credentials grant.

Source: https://9in.io/docs/machine-to-machine/

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](https://9in.io/docs/reference/management-api/).

## Call your API

1. **APIs & Scopes → Create API**, e.g. `https://api.acme.com` with a scope `reports:read` (skip if it exists).
2. **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.
3. Ask for a token:

```bash
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"
```

```json
{ "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.

```ts
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:

```ts
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:

```bash
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](https://9in.io/docs/reference/management-api/).

## 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).
