# Authentication


Exchange your Intuizi Console credentials for a bearer token, then send that
token on every other call in the reference. The login endpoint is the only one
that does not itself require a token.

## Login {#post-apiv2authlogin}

`POST /api/v2/auth/login`

Issues a bearer token for the supplied user credentials. Send the token on every
subsequent request as `Authorization: Bearer <token>`.

**Auth:** none (this is how you obtain a token). Send `Content-Type:
application/json` and `Accept: application/json`. Rate limit: 180 requests/min
**per client IP** - login sits outside the v2 group and is metered by the
shared API throttle keyed per IP, not by the per-token v2 write bucket.

### Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | The account email. Must be a valid email address. |
| `password` | string | Yes | The account password. |

{{< tabs >}}

  {{< tab name="cURL" >}}
  ```bash
  curl -X POST "https://console.intuizi.com/api/v2/auth/login" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d '{
      "email": "you@example.com",
      "password": "your-password"
    }'
  ```
  {{< /tab >}}

  {{< tab name="Python" >}}
  ```python
  import requests

  res = requests.post(
      "https://console.intuizi.com/api/v2/auth/login",
      headers={"Content-Type": "application/json", "Accept": "application/json"},
      json={"email": "you@example.com", "password": "your-password"},
  )
  token = res.json()["data"]["token"]
  ```
  {{< /tab >}}

  {{< tab name="JavaScript" >}}
  ```javascript
  const res = await fetch("https://console.intuizi.com/api/v2/auth/login", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
    body: JSON.stringify({ email: "you@example.com", password: "your-password" }),
  });
  const token = (await res.json()).data.token;
  ```
  {{< /tab >}}

  {{< tab name="PHP" >}}
  ```php
  $res = Http::acceptJson()->post(
      'https://console.intuizi.com/api/v2/auth/login',
      ['email' => 'you@example.com', 'password' => 'your-password']
  );
  $token = $res->json('data.token');
  ```
  {{< /tab >}}

{{< /tabs >}}

### Response

```json
{
  "status": "success",
  "code": 200,
  "message": "Token generated successfully.",
  "data": {
    "token": "FxGrKXTS0iLgonnQZtELC0dWu73xslzHjGjaBVef"
  }
}
```

Failed requests use the shared error envelope - see [Errors](/concepts/errors).

> **Login tokens rotate.** Each successful login deletes the account's
> previous login token - one live login token per account - and the token is
> also revoked when the user logs out of the console or changes their
> password. For CLI sessions, CI
> pipelines, and any long-lived integration, use an [API token](#post-apiv2authapitoken)
> instead: API tokens are not rotated by logins and survive logout.

## Sending the token

Every other endpoint in this reference is authenticated and JSON-only. Send both
headers on every call:

```
Authorization: Bearer <YOUR_TOKEN>
Accept: application/json
```

A missing, invalid, or expired token returns `401`. A request that omits
`Accept: application/json` returns `406`. POST bodies must also send
`Content-Type: application/json` (or `multipart/form-data` for file uploads).

## Create MCP Token {#post-apiv2authmcptoken}

`POST /api/v2/auth/mcp-token`

Issues a long-lived bearer token (scope: `mcp`) for the
[MCP server](/mcp). MCP tokens are independent of login tokens: logins do not
rotate them and console logout does not revoke them. Changing the account
password revokes all MCP tokens. They expire after one year by default.
Up to 10 can be active at once; further requests return a `422` until you
revoke.

The primary way to manage MCP tokens is the My Account > MCP Tokens page in
the Intuizi console; this endpoint exists for CLI and headless use.

**Auth:** none (credential exchange). Send `Content-Type: application/json`
and `Accept: application/json`. Rate limit: 10 requests/min by default.

### Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | The account email. Must be a valid email address. |
| `password` | string | Yes | The account password. |

{{< tabs >}}

  {{< tab name="cURL" >}}
  ```bash
  curl -X POST "https://console.intuizi.com/api/v2/auth/mcp-token" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d '{
      "email": "you@example.com",
      "password": "your-password"
    }'
  ```
  {{< /tab >}}

  {{< tab name="Python" >}}
  ```python
  import requests

  res = requests.post(
      "https://console.intuizi.com/api/v2/auth/mcp-token",
      headers={"Content-Type": "application/json", "Accept": "application/json"},
      json={"email": "you@example.com", "password": "your-password"},
  )
  token = res.json()["data"]["token"]
  ```
  {{< /tab >}}

  {{< tab name="JavaScript" >}}
  ```javascript
  const res = await fetch("https://console.intuizi.com/api/v2/auth/mcp-token", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
    body: JSON.stringify({ email: "you@example.com", password: "your-password" }),
  });
  const token = (await res.json()).data.token;
  ```
  {{< /tab >}}

  {{< tab name="PHP" >}}
  ```php
  $res = Http::acceptJson()->post(
      'https://console.intuizi.com/api/v2/auth/mcp-token',
      ['email' => 'you@example.com', 'password' => 'your-password']
  );
  $token = $res->json('data.token');
  ```
  {{< /tab >}}

{{< /tabs >}}

### Response

```json
{
  "status": "success",
  "code": 200,
  "message": "MCP token generated successfully.",
  "data": {
    "token": "eyJ0eXAiOiJKV1QiLCJh...",
    "expires_at": "2027-07-18T00:00:00+00:00"
  }
}
```

Failed requests use the shared error envelope - see [Errors](/concepts/errors).

## Revoke MCP Tokens {#post-apiv2authmcptokenrevoke}

`POST /api/v2/auth/mcp-token/revoke`

Revokes every active MCP token owned by the supplied credentials. Connected
MCP clients stop authenticating immediately.

The primary way to manage MCP tokens is the My Account > MCP Tokens page in
the Intuizi console, which also revokes tokens individually; this endpoint
exists for CLI and headless use and is all-or-nothing.

**Auth:** none (credential exchange). Send `Content-Type: application/json`
and `Accept: application/json`. Rate limit: 10 requests/min by default.

### Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | The account email. Must be a valid email address. |
| `password` | string | Yes | The account password. |

{{< tabs >}}

  {{< tab name="cURL" >}}
  ```bash
  curl -X POST "https://console.intuizi.com/api/v2/auth/mcp-token/revoke" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d '{
      "email": "you@example.com",
      "password": "your-password"
    }'
  ```
  {{< /tab >}}

  {{< tab name="Python" >}}
  ```python
  import requests

  res = requests.post(
      "https://console.intuizi.com/api/v2/auth/mcp-token/revoke",
      headers={"Content-Type": "application/json", "Accept": "application/json"},
      json={"email": "you@example.com", "password": "your-password"},
  )
  revoked = res.json()["data"]["revoked"]
  ```
  {{< /tab >}}

  {{< tab name="JavaScript" >}}
  ```javascript
  const res = await fetch("https://console.intuizi.com/api/v2/auth/mcp-token/revoke", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
    body: JSON.stringify({ email: "you@example.com", password: "your-password" }),
  });
  const revoked = (await res.json()).data.revoked;
  ```
  {{< /tab >}}

  {{< tab name="PHP" >}}
  ```php
  $res = Http::acceptJson()->post(
      'https://console.intuizi.com/api/v2/auth/mcp-token/revoke',
      ['email' => 'you@example.com', 'password' => 'your-password']
  );
  $revoked = $res->json('data.revoked');
  ```
  {{< /tab >}}

{{< /tabs >}}

### Response

```json
{
  "status": "success",
  "code": 200,
  "message": "MCP tokens revoked successfully.",
  "data": {
    "revoked": 2
  }
}
```

Failed requests use the shared error envelope - see [Errors](/concepts/errors).

## Create API Token {#post-apiv2authapitoken}

`POST /api/v2/auth/api-token`

Issues a long-lived bearer token (name: `api`, no scopes) that authenticates
every general v2 endpoint except [`POST /api/v2/mcp`](/mcp), which requires
the `mcp` scope and rejects `api` tokens with `403 insufficient_scope`.
Tokens expire after one year by default. Up to 10 can be active at once;
beyond that, minting returns a `422` with `API token limit reached (10
active tokens). Revoke existing tokens via POST /api/v2/auth/api-token/revoke
and retry.`

API tokens are long-lived bearer tokens for the [Intuizi CLI](/cli), CI pipelines,
and custom integrations. They are not rotated by logins, survive console
logout, and are revoked on password change, on explicit revocation, or at
expiry. Each account can hold up to 10 active API tokens; manage them
under **My Account > API Tokens** in the console, where CI tokens should
be minted. In CI, pass the token via the `INTUIZI_API_TOKEN` environment
variable rather than calling the mint endpoint on every run.

**Auth:** none (credential exchange). Send `Content-Type: application/json`
and `Accept: application/json`. Rate limit: 10 requests/min by default.

### Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | The account email. Must be a valid email address. |
| `password` | string | Yes | The account password. |

{{< tabs >}}

  {{< tab name="cURL" >}}
  ```bash
  curl -X POST "https://console.intuizi.com/api/v2/auth/api-token" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d '{
      "email": "you@example.com",
      "password": "your-password"
    }'
  ```
  {{< /tab >}}

  {{< tab name="Python" >}}
  ```python
  import requests

  res = requests.post(
      "https://console.intuizi.com/api/v2/auth/api-token",
      headers={"Content-Type": "application/json", "Accept": "application/json"},
      json={"email": "you@example.com", "password": "your-password"},
  )
  token = res.json()["data"]["token"]
  ```
  {{< /tab >}}

  {{< tab name="JavaScript" >}}
  ```javascript
  const res = await fetch("https://console.intuizi.com/api/v2/auth/api-token", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
    body: JSON.stringify({ email: "you@example.com", password: "your-password" }),
  });
  const token = (await res.json()).data.token;
  ```
  {{< /tab >}}

  {{< tab name="PHP" >}}
  ```php
  $res = Http::acceptJson()->post(
      'https://console.intuizi.com/api/v2/auth/api-token',
      ['email' => 'you@example.com', 'password' => 'your-password']
  );
  $token = $res->json('data.token');
  ```
  {{< /tab >}}

{{< /tabs >}}

### Response

```json
{
  "status": "success",
  "code": 200,
  "message": "API token generated successfully.",
  "data": {
    "token": "eyJ0eXAiOiJKV1QiLCJh...",
    "expires_at": "2027-07-28T00:00:00+00:00"
  }
}
```

Failed requests use the shared error envelope - see [Errors](/concepts/errors).

## Revoke API Tokens {#post-apiv2authapitokenrevoke}

`POST /api/v2/auth/api-token/revoke`

Revokes every active API token owned by the supplied credentials. Connected
CLI sessions, CI pipelines, and integrations stop authenticating
immediately.

The primary way to manage API tokens is the My Account > API Tokens page in
the Intuizi console, which also revokes tokens individually. This endpoint is
for headless use and is all-or-nothing: it also revokes the tokens that CI
pipelines and the Intuizi CLI use.

**Auth:** none (credential exchange). Send `Content-Type: application/json`
and `Accept: application/json`. Rate limit: 10 requests/min by default.

### Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Yes | The account email. Must be a valid email address. |
| `password` | string | Yes | The account password. |

{{< tabs >}}

  {{< tab name="cURL" >}}
  ```bash
  curl -X POST "https://console.intuizi.com/api/v2/auth/api-token/revoke" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d '{
      "email": "you@example.com",
      "password": "your-password"
    }'
  ```
  {{< /tab >}}

  {{< tab name="Python" >}}
  ```python
  import requests

  res = requests.post(
      "https://console.intuizi.com/api/v2/auth/api-token/revoke",
      headers={"Content-Type": "application/json", "Accept": "application/json"},
      json={"email": "you@example.com", "password": "your-password"},
  )
  revoked = res.json()["data"]["revoked"]
  ```
  {{< /tab >}}

  {{< tab name="JavaScript" >}}
  ```javascript
  const res = await fetch("https://console.intuizi.com/api/v2/auth/api-token/revoke", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
    body: JSON.stringify({ email: "you@example.com", password: "your-password" }),
  });
  const revoked = (await res.json()).data.revoked;
  ```
  {{< /tab >}}

  {{< tab name="PHP" >}}
  ```php
  $res = Http::acceptJson()->post(
      'https://console.intuizi.com/api/v2/auth/api-token/revoke',
      ['email' => 'you@example.com', 'password' => 'your-password']
  );
  $revoked = $res->json('data.revoked');
  ```
  {{< /tab >}}

{{< /tabs >}}

### Response

```json
{
  "status": "success",
  "code": 200,
  "message": "API tokens revoked successfully.",
  "data": {
    "revoked": 2
  }
}
```

Failed requests use the shared error envelope - see [Errors](/concepts/errors).

## OAuth one-click connect {#oauth-one-click-connect}

The [MCP server](/mcp) additionally supports OAuth 2.1 with the
authorization-code + PKCE flow. This is how any OAuth-capable client connects,
whether it is a command-line tool or a UI that adds MCP servers as custom
connectors: it takes a server URL only, discovers the authorization server,
registers itself, and sends you to an Intuizi consent screen in the browser.
You never handle a credential. A client configured with a static
`Authorization` header never reaches this flow, because it does not receive the
`401` challenge that bootstraps it. For the user-facing walkthrough see
[Getting started](/mcp/getting-started); this section documents the protocol
surface for client implementers.

### Discovery

Both discovery documents are public JSON, and they are the source of truth
for the supported scopes, grant types, and endpoint URLs - read them rather
than hardcoding values:

- Protected-resource metadata (RFC 9728):
  `https://console.intuizi.com/.well-known/oauth-protected-resource`
- Authorization-server metadata (RFC 8414):
  `https://console.intuizi.com/.well-known/oauth-authorization-server`

An unauthenticated request to the MCP endpoint returns `401` with a
`WWW-Authenticate` header pointing at the protected-resource document, so a
compliant client can bootstrap the whole flow from the server URL alone.

### Endpoints

| Endpoint | Purpose |
| --- | --- |
| `POST /oauth/register` | Dynamic client registration (RFC 7591) |
| `GET /oauth/authorize` | Authorization request; renders the consent screen on your console session |
| `POST /oauth/token` | Authorization-code exchange and refresh |
| `POST /oauth/revoke` | Token revocation (RFC 7009) |

### Registration constraints

Registration is open to OAuth clients but tightly constrained, because the
redirect target is what protects the authorization code:

- `redirect_uris` must be exact, absolute `https` URIs on an approved host
  list (the major agent platforms are pre-approved). No wildcards, fragments,
  or userinfo.
- Loopback redirects (`localhost`, `127.0.0.1`, `[::1]`) are exempt from the
  host list and may use plain `http` on any port. This is the form
  command-line and desktop clients use to receive the authorization code, so
  they connect without any host being added.
- Registered clients can use only the `authorization_code` and
  `refresh_token` grant types and the `mcp` scope. Any other scope, including
  the wildcard `*`, is rejected with `invalid_scope`.
- Errors use the standard RFC 7591 shape (`error` /
  `error_description`), not the v2 envelope.

If your client's redirect host is rejected, contact your Account Manager to
have it reviewed and added.

### PKCE

PKCE is required for every authorization request, and only the `S256`
challenge method is accepted - requests without a `code_challenge`, or using
the `plain` method, are rejected. This applies to confidential clients too,
per OAuth 2.1.

### Token lifetimes

- Access tokens live 1 hour and carry the `mcp` scope.
- Refresh tokens live 60 days and rotate: each refresh returns a new pair and
  invalidates the old one.
- A refresh token that was rotated out, revoked, or expired is rejected with
  `400` and `error: invalid_grant` (RFC 6749). Discard the stored tokens and
  send the user through authorization again.
- Manual MCP tokens (the [Create MCP Token](#post-apiv2authmcptoken) path)
  are unchanged: one year, separate cap, separate lifecycle.

### Revocation

A connection ends when any of these happens:

- the client calls `POST /oauth/revoke` with the access or refresh token
  (revoking either half invalidates both);
- the refresh token expires (60 days) without renewal;
- you change your Intuizi console password, which immediately revokes every
  token on the account: login tokens, API tokens, MCP tokens, and every OAuth
  connection.

Console logins and logouts never affect OAuth connections or MCP tokens.
