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.
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:
{
"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):
{
"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
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:
{
"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.
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.