# Create an API token

Create a token with the given scopes.

`POST /tokens`

Auth **Console session** | Role **Owner or Manager**

Create a token with the given scopes. The response carries `tokenValue`, the secret, exactly once: store it in a secret manager, it cannot be shown again. Scopes are explicit catalogue names (`GET /tokens/scopes` lists them, with who may grant each and whether an expiry is required); a wildcard, a bundle name or an unknown scope is refused with SCOPE_INVALID. Some scopes are limited to an Owner (OWNER_REQUIRED), need an active membership (MEMBERSHIP_REQUIRED) or a sufficient role (INSUFFICIENT_ROLE), and some require an expiry within a maximum lifetime (EXPIRY_REQUIRED, EXPIRY_TOO_LONG): `pii:read` needs an Owner and expires within 90 days; `jml:read` expires within 365 days; `supplier_template:write` needs an expiry. A token cannot be created or revoked from a support session (SUPPORT_SESSION_NO_MINT). At most 20 tokens per organization. Requires a console session of an Owner or Manager.

## Request body

- `name`: string A name that says where the token is used. minLength 1, maxLength 100
- `scopes`: array of string Explicit scope names from GET /tokens/scopes. Unknown names, wildcards and bundle names are refused with SCOPE_INVALID. minItems 1, maxItems 40
- `expiresAt`: optional string (date-time) or null Future expiry. Required, within the scope maximum, for scopes whose catalogue entry has requiresExpiry (for example pii:read within 90 days, jml:read within 365 days, supplier_template:write).
- No other fields.

## Returns

The `data` field of the standard `{ data, error, meta }` envelope:

- `tokenId`: string
- `name`: string
- `prefix`: string
- `scopes`: array of string
  - `sbom:read`
  - `sbom:write`
  - `mcp:read`
  - `mcp:write`
  - `supplier_template:read`
  - `supplier_template:write`
  - `pii:read`
  - `jml:read`
  - `asset:read`
  - `risk:read`
  - `supplier:read`
  - `iso:read`
  - `compliance:read`
  - `evidence:read`
  - `access_review:read`
  - `employee:read`
  - `audit_log:read`
  - `audit_log:verify`
- `expiresAt`: optional string (date-time)
- `revokedAt`: optional string (date-time)
- `revokedBy`: optional string
- `lastUsedAt`: optional string (date-time)
- `suspendedAt`: optional string (date-time) Set when the token was suspended because its creator no longer holds the role its scopes require. A suspended token is refused like a revoked one and is not resumed.
- `suspendedReason`: optional string Why the token was suspended.
- `activity`: optional object Usage of the token on the current UTC day. Absent until the token has been used.
  - `day`: string UTC date, YYYY-MM-DD.
  - `requests`: integer Requests made that day.
  - `rows`: integer Data rows returned that day.
  - `distinctRoutes`: integer Number of distinct routes called that day (capped at 50).
  - `lastIp`: string Client address of the last request.
  - `lastRouteId`: string Route template of the last request.
  - No other fields.
- `createdBy`: string
- `createdAt`: string (date-time)
- `updatedAt`: string (date-time)
- `tokenValue`: string The secret. Returned only in this response; send it as `Authorization: Bearer <token>`.
- No other fields.

## Errors

| Status | Code | Retry | Meaning |
|---|---|---|---|
| 400 | `VALIDATION_FAILED` | no | The body does not match the request schema (VALIDATION_FAILED), cannot be read (INVALID_BODY) or names an unknown scope (SCOPE_INVALID). |
| 401 | `UNAUTHORIZED` | no | No signed-in session. code UNAUTHORIZED. |
| 403 | `OWNER_REQUIRED` | no | Not allowed to create this token. FORBIDDEN (not an Owner or Manager, or an API token), SUPPORT_SESSION_NO_MINT (support session), OWNER_REQUIRED (the scope needs an Owner, for example pii:read), MEMBERSHIP_REQUIRED (an active membership of this organization is required) or INSUFFICIENT_ROLE (the caller role cannot grant the scope). |
| 422 | `EXPIRY_REQUIRED` | no | The request is well-formed but cannot be granted. SCOPE_UNAVAILABLE (pii:read not enabled yet), INVALID_EXPIRY (expiresAt not in the future), EXPIRY_REQUIRED, EXPIRY_TOO_LONG or TOKEN_LIMIT_EXCEEDED (20 tokens per organization). |
| 422 | `TOKEN_LIMIT_EXCEEDED` | no | The request is well-formed but cannot be granted. SCOPE_UNAVAILABLE (pii:read not enabled yet), INVALID_EXPIRY (expiresAt not in the future), EXPIRY_REQUIRED, EXPIRY_TOO_LONG or TOKEN_LIMIT_EXCEEDED (20 tokens per organization). |
| 500 | `DB_ERROR` | no | Unexpected server error. code DB_ERROR or TOKEN_GENERATION_FAILED; retry later and quote error.id if it persists. |

## Example

### curl

```bash
curl -X POST "https://api.autociso.io/api/v1/tokens" \
  -H "Authorization: Bearer <console-session-token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"GitHub Actions SBOM upload","scopes":["sbom:write"]}'
```

### Python

```python
import requests

response = requests.post(
    "https://api.autociso.io/api/v1/tokens",
    headers={"Authorization": "Bearer <console-session-token>", "Content-Type": "application/json"},
    json={
        "name": "GitHub Actions SBOM upload",
        "scopes": [
            "sbom:write",
        ],
    },
)
print(response.json())
```

### JavaScript

```javascript
const response = await fetch("https://api.autociso.io/api/v1/tokens", {
  method: "POST",
  headers: { "Authorization": "Bearer <console-session-token>", "Content-Type": "application/json" },
  body: JSON.stringify({
    "name": "GitHub Actions SBOM upload",
    "scopes": [
      "sbom:write"
    ]
  }),
});
console.log(await response.json());
```

## Response (201)

```json
{
  "data": {
    "tokenId": "tok_1hgz7c0aw2mf",
    "name": "GitHub Actions SBOM upload",
    "prefix": "aci_XXXX",
    "scopes": [
      "sbom:write"
    ],
    "createdBy": "usr_1hgyq2mw7c3k",
    "createdAt": "2026-10-09T09:00:00Z",
    "updatedAt": "2026-10-09T09:00:00Z",
    "tokenValue": "aci_<redacted>"
  }
}
```
