> ## Documentation Index
> Fetch the complete documentation index at: https://docs.closient.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Closient API Quickstart

> Zero-to-first-call quickstart for any agent integrating with Closient — discover APIs, register an OAuth client (RFC 7591) or get an API key, complete an authorization_code + PKCE flow, make the first authenticated request, and debug the common 401/403/429 failure modes.

The meta-skill. If your agent has never spoken to Closient before, run
this end-to-end and you'll have a working authenticated call against a
real endpoint in under five minutes.

## When to use

* An agent (MCP host, partner SDK, internal automation, third-party
  integration) is being onboarded against Closient for the first time.
* A developer wants the canonical "hello world" path before reading any
  per-app API docs.
* Token, scope, or rate-limit confusion needs to be reset to a known-good
  baseline.

Once the quickstart works, jump to `closient-openapi-tour` for a
breadth-first map of the 22 per-app APIs and individual skills like
`local-product-search` for depth on a specific workflow.

## Environments

| Environment | Site URL                   | Use for                                         |
| ----------- | -------------------------- | ----------------------------------------------- |
| Production  | `https://www.closient.com` | Real traffic, real customers.                   |
| Testing     | `https://www.closient.dev` | Integration testing — same code, isolated data. |
| Local       | `http://localhost:8000`    | Local dev only.                                 |

All endpoints in this doc are path-relative; prefix them with the env
URL of your choice. Examples use `https://www.closient.com`.

## Step 1 — Discover what's available

Closient publishes machine-readable discovery surfaces under
`/.well-known/`. Hit them first to enumerate APIs and capabilities
without grepping documentation:

```
GET /.well-known/api-catalog
Accept: application/linkset+json
```

The response is an RFC 9727 linkset — one entry per per-app NinjaAPI,
each with a `service-desc` pointing at that app's OpenAPI document:

```json theme={null}
{
  "linkset": [
    {
      "anchor": "https://www.closient.com/search/api/v1/",
      "service-desc": [{
        "href": "https://www.closient.com/search/api/v1/openapi.json",
        "type": "application/openapi+json"
      }]
    }
  ]
}
```

Sibling discovery endpoints worth knowing:

| Endpoint                                  | What it gives you                                                                                                  |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `/.well-known/agent-skills/index.json`    | Index of every Agent Skill (this is one).                                                                          |
| `/.well-known/mcp.json`                   | MCP server card — connect via the MCP host of your choice.                                                         |
| `/.well-known/openid-configuration`       | OIDC / OAuth 2.0 discovery (token + auth endpoints, JWKS URI, supported scopes, RFC 7591 `registration_endpoint`). |
| `/.well-known/oauth-authorization-server` | Plain OAuth 2.0 AS metadata (RFC 8414).                                                                            |

## Step 2 — Pick a credential type

Closient accepts **two** authentication shapes on every per-app API:

| Mechanism                                            | When to use                                                                                                                        |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `X-API-Key` header (`csb_<token>_<crc32>`)           | Server-to-server jobs, ETL, internal automation. One key per service. Tier rate limits apply (default 300/min, 10k/day).           |
| OAuth 2.0 access token (`Authorization: Bearer ...`) | Agents acting on behalf of a Closient user. Supports per-scope authorization, refresh, revocation, and RFC 7591 self-registration. |

Browser-based clients can also use the Django session cookie, but that
isn't a useful agent path.

**Quick rule:** if your agent's caller is *a person who has a Closient
account*, use OAuth. If your caller is *another machine you own*, use an
API key. Don't share OAuth client credentials between operators.

### 2a — Get an API key (fastest path)

Sign in to Closient, open **Settings → API Keys**, and create a new
key. The key is shown once; copy it then. Format: `csb_<body>_<crc32>`.

Verify by hitting the catalog search endpoint with no scope required:

```
GET /search/api/v1/search?q=trail+mix
X-API-Key: csb_<body>_<crc32>
```

If you get `200 OK` with a JSON body containing `items` and `total`,
the key is alive and you can skip to Step 4. (Catalog search is one of
the few unauthenticated endpoints, so this also works without a key — a
401 here means the key string is malformed.)

### 2b — Register an OAuth client (RFC 7591 Dynamic Client Registration)

For agents that need to act on behalf of *users*, register a client
dynamically — no human admin required:

```
POST /oauth/register/
Content-Type: application/json

{
  "client_name": "Acme Shopping Agent",
  "redirect_uris": ["https://agent.example.com/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "scope": "search:read products:read resolver:read",
  "token_endpoint_auth_method": "client_secret_basic",
  "software_id": "com.example.acme-shopping-agent",
  "software_version": "1.2.3"
}
```

Response (RFC 7591 §3.2.1):

```json theme={null}
{
  "client_id": "...",
  "client_secret": "...",
  "client_id_issued_at": 1734567890,
  "client_secret_expires_at": 0,
  "redirect_uris": ["https://agent.example.com/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "scope": "search:read products:read resolver:read",
  "token_endpoint_auth_method": "client_secret_basic"
}
```

Notes:

* The endpoint is **rate-limited per IP** (10 registrations/hour). Use a
  stable `software_id` so repeat runs deduplicate cleanly.
* `client_secret_expires_at: 0` means the secret never expires (RFC 7591
  §3.2.1) — but you can rotate at any time via the connected-apps
  dashboard.
* The redirect URI must use HTTPS in production; `http://localhost`
  redirects are accepted in testing/local environments.
* For a confidential server-to-server agent, use `client_credentials` in
  the `grant_types` array; you'll get tokens without a user in the loop.

## Step 3 — Run the authorization\_code + PKCE flow

Public clients (mobile, SPA, CLI tools) MUST use PKCE — Closient's AS
returns `invalid_request` if the code-verifier is missing.

### 3a — Mint the PKCE pair

```python theme={null}
import base64
import hashlib
import secrets

code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
```

Hang onto `code_verifier` — you need it at the token exchange step.

### 3b — Redirect the user to the authorize endpoint

```
GET /oauth/authorize/?
  response_type=code&
  client_id=<your_client_id>&
  redirect_uri=https://agent.example.com/oauth/callback&
  scope=search:read+products:read&
  state=<random_anti_csrf_token>&
  code_challenge=<code_challenge>&
  code_challenge_method=S256
```

The user sees a Closient-branded consent screen listing the agent name,
logo, and the human-readable description of each requested scope. They
click **Allow**; Closient redirects to your `redirect_uri` with `code`
and `state` in the query string.

If they click **Deny**, you'll receive `?error=access_denied` — surface
it as "the user declined" rather than retrying.

### 3c — Exchange the code for tokens

```
POST /oauth/token/
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=<code_from_redirect>&
redirect_uri=https://agent.example.com/oauth/callback&
code_verifier=<the_verifier_from_step_3a>
```

Response:

```json theme={null}
{
  "access_token": "...",
  "expires_in": 3600,
  "token_type": "Bearer",
  "refresh_token": "...",
  "scope": "search:read products:read",
  "id_token": "..."
}
```

The `id_token` is a signed OIDC JWT — verify against the JWKS URI from
`/.well-known/openid-configuration` before trusting the claims.

### 3d — Refresh when the access token expires

```
POST /oauth/token/
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token=<refresh_token>
```

## Step 4 — Make your first authenticated call

Pick any per-app endpoint. The search session endpoint is the canonical
"hello world" for an agent because it returns local-first product
results that exercise the geo-distance ranking pipeline:

```
POST /search/api/v1/search/session
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "query": "organic peanut butter",
  "latitude": 48.4284,
  "longitude": -123.3656,
  "radius_km": 25
}
```

A successful response includes a session ID you can use to refine the
query (subsequent POSTs to `/search/session/{id}/refine`) and an
array of `items`, each annotated with `store`, `offer`, and
`distance_km`.

For something even simpler that doesn't require any auth at all:

```
GET /search/api/v1/search?q=trail+mix
```

If both of those work, you're done. The first row of data is the
deliverable.

## Step 5 — Debug the common failures

### `401 Unauthorized`

| Cause                                  | What to check                                                                                  |
| -------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Missing header                         | `X-API-Key` or `Authorization` header not sent — check your HTTP client's middleware.          |
| Malformed API key                      | Format is `csb_<body>_<crc32>`. Three components, two underscores.                             |
| Expired access token                   | Use the refresh token; OAuth access tokens are 1 h by default.                                 |
| Wrong environment                      | A testing-env API key won't work against production, and vice versa.                           |
| Wrong client\_secret on token exchange | Re-derive `Authorization: Basic ...` — base64 of `client_id:client_secret`, no padding tricks. |

### `403 Forbidden`

| Cause              | What to check                                                                                                                                        |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Scope mismatch     | OAuth token doesn't carry the scope the endpoint requires. Re-run authorize with `scope=` including the right per-app scope (e.g. `products:write`). |
| Role mismatch      | The user behind the token is a `VIEWER` on the org but the endpoint requires `MANAGER` or `OWNER`.                                                   |
| Wrong organization | Cross-org access — the token's user isn't a member of the target organization.                                                                       |
| API key disabled   | The key may have been revoked; create a new one.                                                                                                     |

### `429 Too Many Requests`

| Header                                      | What it says                                          |
| ------------------------------------------- | ----------------------------------------------------- |
| `RateLimit-Policy: 300;w=60, 10000;w=86400` | Two active windows: 300/minute and 10k/day.           |
| `RateLimit-Limit: 300`                      | Cap for the most-restrictive currently-active window. |
| `RateLimit-Remaining: 12`                   | What's left in that window.                           |
| `RateLimit-Reset: 7`                        | Seconds (relative) until that window resets.          |

These are standard IETF `RateLimit-*` headers (informed-governor). Use
`RateLimit-Reset` to schedule the next retry — don't poll on 429 with no
backoff.

Legacy `X-RateLimit-*` aliases are also emitted; `X-RateLimit-Reset`
keeps the absolute Unix-timestamp shape for back-compat.

### `400` with `error: invalid_grant` on token exchange

* `code` was already redeemed (one-time-use).
* `code_verifier` doesn't match the original `code_challenge`.
* `redirect_uri` differs from what you sent at `/oauth/authorize/`.

### `400` with RFC 7591 `error: invalid_redirect_uri` on registration

* HTTP redirect URI on production (HTTPS-only outside localhost).
* Loopback URI uses a hostname instead of `127.0.0.1` / `[::1]`.

## Scopes you'll actually use

Closient's OAuth scope set is per-app — every NinjaAPI gets a `:read`
and a `:write`. The most common starter set:

| Scope                                | What it grants                                               |
| ------------------------------------ | ------------------------------------------------------------ |
| `search:read`                        | Catalog and session search.                                  |
| `products:read`                      | Product lookups, attributes, GTIN resolution.                |
| `resolver:read`                      | GS1 Digital Link resolution + linkset queries.               |
| `brands:read` / `brands:write`       | Brand records (write needed for `claim-brand` flow).         |
| `retailers:read` / `retailers:write` | Offers + inventory (write needed for POS-driven onboarding). |
| `account:read`                       | `whoami`, org membership, API-key listing.                   |
| `qr:generate`                        | The QR-URL MCP tool (not REST-fronted).                      |

The full canonical list is the `scopes_supported` array in
`/.well-known/openid-configuration`. Always request the **narrowest
scope set** that lets you complete the workflow — agents asking for
write scope when they only read are flagged as suspicious in the consent
UI.

## Related skills

* `closient-openapi-tour` — bird's-eye map of the 22 per-app APIs.
* `decode-gs1-ai` — once authenticated, parse GS1 AI strings into
  structured queries against `/products/api/v1/`.
* `build-gs1-digital-link` — inverse of `decode-gs1-ai`.
* `local-product-search`, `resolve-gtin`, `check-product-availability` —
  the most common workflows for an agent's *first* useful call.
* `claim-brand`, `onboard-retailer` — onboarding-shaped workflows for
  authenticated org members.
