---
title: OAuth2 Integration
description: Let users authorize your app to sign X402 transactions on their behalf
---

# OAuth2 Integration

OAuth2 lets your application request permission from Agnic users to sign X402 transactions on their behalf. Users set their own spending limits and can revoke access anytime.

## Why OAuth2?

- **User-controlled limits** — Users set daily/monthly spending caps at consent
- **Revocable access** — Users can disconnect your app anytime
- **Network selection** — Users choose which networks to allow
- **Long-lived tokens** — Access tokens last 30–60 days with refresh

## Register Your Application

Before your first OAuth2 call, register your app and get it approved.

### 1. Create a client in the dashboard

Go to **[app.agnic.ai/oauth-clients](https://app.agnic.ai/oauth-clients)** and click **New Client**. Registration is a three-step form.

#### Step 1 — KYC information

Required to operate an OAuth client on Agnic. These fields **lock after approval**.

| Field | Required | Notes |
|-------|----------|-------|
| Full name | Yes | Legal name of the responsible party |
| Phone number | Yes | |
| Country | Yes | |
| Address | Yes | |

#### Step 2 — App information

Shown to your users on the consent screen; editable any time.

| Field | Required | Notes |
|-------|----------|-------|
| App name | Yes | Displayed to users when they authorize |
| User support email | Yes | Where users reach you about your app |
| Contact emails | No | Where Agnic reaches you about your client (add as many as you like) |

#### Step 3 — OAuth configuration

| Field | Required | Notes |
|-------|----------|-------|
| Authorized JavaScript origins | No | CORS-origin allowlist for browser flows (e.g. `https://yourapp.com`) |
| Authorized redirect URIs | Yes, ≥1 | Exact-match callback URLs. Add one per environment |
| Accept terms | Yes | |

### 2. Save your credentials

On creation you receive:

- **Client ID** — always visible on the client's detail page (copyable)
- **Client Secret** — **shown once** in a modal. Copy it immediately; it is never displayed again. You can **regenerate** from the detail page, which invalidates the previous secret.

Store the secret in a server-side vault. Public clients (SPAs, mobile, CLIs) should use [PKCE](#pkce-required-for-public-clients) instead of embedding a secret.

### 3. Wait for approval

New clients start in **Pending Approval** status. You can see this as a badge on the client detail page.

<Callout type="warn">
  While pending, the `/oauth/authorize` endpoint will reject your `client_id`. Your client becomes usable only after Agnic approves it. You'll be notified at the contact emails you provided.
</Callout>

Status badges you may see on the detail page:

| Status | Meaning |
|--------|---------|
| Pending Approval | Awaiting Agnic review — cannot run OAuth flows |
| Approved | Ready for production |
| Rejected | Review rejected — reason shown on the page |
| Revoked | Access permanently disabled |

## Authorization Flow

### Step 1: Redirect to Authorization

Redirect users to our authorization endpoint. Include PKCE for any non-confidential client.

```
https://api.agnic.ai/oauth/authorize?
  client_id=<your client id>
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=payments:sign+balance:read
  &state=<random csrf token>
  &code_challenge=<base64url(sha256(verifier))>
  &code_challenge_method=S256
```

### Step 2: User Grants Permission

The user logs in, sets spending limits (per-transaction, daily, monthly), selects allowed networks, and approves your app.

### Step 3: Receive Authorization Code

User is redirected back to your app with an authorization code:

```
https://yourapp.com/callback?code=abc123&state=<original state>
```

On failure, the callback carries `error` and (usually) `error_description` instead:

```
https://yourapp.com/callback?error=access_denied&error_description=User%20declined
```

Always verify `state` matches the value you sent before doing anything else.

### Step 4: Exchange for Tokens

Exchange the code for access and refresh tokens. `redirect_uri` must match the one you sent in Step 1 exactly.

```bash
curl -X POST https://api.agnic.ai/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "abc123",
    "redirect_uri": "https://yourapp.com/callback",
    "client_id": "<your client id>",
    "code_verifier": "<the PKCE verifier>"
  }'
```

Confidential clients (server-side apps with a stored secret) include `client_secret` instead of, or in addition to, `code_verifier`.

## Token Response

```json
{
  "access_token": "agnic_at_abc123...",
  "refresh_token": "agnic_rt_xyz789...",
  "token_type": "Bearer",
  "expires_in": 2592000,
  "scope": "payments:sign balance:read"
}
```

<Callout type="info">
  **Token Expiration:**
  - Access tokens: 30–60 days (varies by client type)
  - Refresh tokens: 90 days
  - N8N tokens: 1 year (for automation workflows)
</Callout>

Persist `expires_at = now + expires_in * 1000` and refresh **before** expiry (a 5-minute buffer is a good default) so an in-flight request never hits a 401.

## Using the Access Token

```bash
# Check user's balance
curl https://api.agnic.ai/api/balance \
  -H "Authorization: Bearer agnic_at_abc123..."

# Make an AI Gateway request
curl https://api.agnic.ai/v1/chat/completions \
  -H "Authorization: Bearer agnic_at_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-4o",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'

# Get transaction history
curl https://api.agnic.ai/api/transactions \
  -H "Authorization: Bearer agnic_at_abc123..."
```

## Refreshing Tokens

When the access token is about to expire, exchange the refresh token for a new pair:

```bash
curl -X POST https://api.agnic.ai/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "agnic_rt_xyz789...",
    "client_id": "<your client id>"
  }'
```

If refresh fails (expired, revoked, or user disconnected your app), destroy the local session and restart the authorization flow.

## Authorization Parameters

| Parameter | Required | Description |
|-----------|----------|-------------|
| `client_id` | Yes | The Client ID issued in `/oauth-clients` |
| `redirect_uri` | Yes | Must exact-match one of your registered Authorized Redirect URIs |
| `response_type` | Yes | Must be `code` |
| `scope` | No | Space-separated scopes (default: `payments:sign balance:read`) |
| `state` | Yes | CSRF token — returned unchanged on the callback |
| `code_challenge` | Recommended | Base64url(SHA256(code_verifier)) — **required** for public clients |
| `code_challenge_method` | With `code_challenge` | Must be `S256` |
| `prompt` | No | Controls consent behavior — see below |
| `display` | No | `popup` for popup-style auth windows |
| `login_hint` | No | Pre-fill the email on the login screen |

### Scopes

See the full list at [Available Scopes](/docs/authentication/scopes). Common combinations:

- `payments:sign balance:read` — most apps (charge the user, read balance)
- `payments:sign balance:read transactions:read` — apps that show spend history

### Prompt Parameter

The `prompt` parameter follows the OpenID Connect standard:

| Value | Behavior |
|-------|----------|
| *(omitted)* | Auto-approve if user previously consented |
| `none` | Silent auth only — error if consent needed |
| `consent` | Force consent screen even if previously consented |
| `login` | Force re-authentication before authorization |

<Callout type="tip">
  Returning users are auto-approved by default (like Google/GitHub). Use `prompt=consent` to force users to review permissions again.
</Callout>

## PKCE (Required for Public Clients)

Public clients — single-page apps, mobile, CLIs — cannot safely store a `client_secret`. PKCE replaces it with a per-flow secret.

```javascript
// 1. Generate a verifier (random 32+ bytes, base64url)
const codeVerifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));

// 2. Derive the challenge
const codeChallenge = base64UrlEncode(
  await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
);

// 3. Send the challenge on /oauth/authorize
const authUrl = new URL('https://api.agnic.ai/oauth/authorize');
authUrl.search = new URLSearchParams({
  client_id: '<your client id>',
  redirect_uri: 'https://yourapp.com/callback',
  response_type: 'code',
  scope: 'payments:sign balance:read',
  state: crypto.randomUUID(),
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
}).toString();

// 4. After the callback, send the verifier on /oauth/token
const tokenResponse = await fetch('https://api.agnic.ai/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code: authCode,
    redirect_uri: 'https://yourapp.com/callback',
    client_id: '<your client id>',
    code_verifier: codeVerifier,
  }),
});
```

Store the `code_verifier` in a short-lived session cookie or server-side session between Step 1 and Step 4, and clear it after use.

## CLI Client

The `agnic` CLI (v2.0.0+) uses OAuth2 Authorization Code + PKCE to authenticate via the browser — the same pattern as `gh auth login`, `vercel login`, and `stripe login`.

**Client ID:** `agnic_cli`

**Flow:**
1. CLI starts a localhost HTTP server on a random port
2. Browser opens to `api.agnic.ai/oauth/authorize` with PKCE challenge
3. User signs in and sets spending limits on the consent screen
4. Browser redirects to `http://localhost:<port>/callback` with auth code
5. CLI exchanges code + PKCE verifier for tokens (90-day expiry)

**Security:**
- PKCE is **required** for CLI clients (server rejects without `code_challenge`)
- Localhost redirect only (`http://localhost:<port>/callback` or `http://127.0.0.1:<port>/callback`)
- HTTP is correct for loopback per [RFC 8252 §7.3](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3)
- Server binds to `127.0.0.1` only (no network exposure)
- Random ephemeral port, state parameter verified, 5-minute timeout

```bash
agnic auth login
# Opens browser → sign in → set limits → authenticated!
```

## Reference Implementation

A full working OAuth2 integration (Next.js, PKCE, refresh, popup flow, session storage) lives in the **PixelAI** reference app. Use it as a template when wiring up your own client.

## Next Steps

<Cards>
  <Card title="Available Scopes" href="/docs/authentication/scopes" />
  <Card title="API Reference" href="/docs/api-reference" />
</Cards>
