Authentication and scopes
Every protected endpoint of the Tarno read contract is gated by an API key. This guide
covers how to present the key, the two scopes, the key lifecycle, and the exact 401 / 403 /
429 semantics — all matching the live handlers.
The X-API-Key header
Send your key in the X-API-Key request header on every call to a protected route:
curl -sS "$TARNO_BASE_URL/v1/brands?limit=1" -H "X-API-Key: $TARNO_API_KEY"
The key is an opaque string issued by the operator (placeholder <API_KEY> in these docs). The
server verifies it on every request; the raw key is never logged.
The MCP server uses the same X-API-Key header, sent per request over the Streamable HTTP
transport — see the MCP guide. (For the stdio transport, the key is read from
the TARNO_API_KEY environment variable instead.)
Public routes that need no key
/health, /docs, and /metrics are the only paths that work without a key. Everything
under /v1 requires one.
Scopes
A key carries a set of scopes that determine which parts of the contract it may read:
| Scope | Grants access to |
|---|---|
brands:read |
The brand contract: GET /v1/brands, GET /v1/brands/:nroSolicitud. |
insights:read |
The operational contract: GET /v1/freshness, GET /v1/sync-runs. |
watch:read |
Watchlist reads: GET /v1/watch, GET /v1/watch/:id, GET /v1/watch/:id/hits. |
watch:write |
Watchlist writes: POST /v1/watch, PATCH /v1/watch/:id, DELETE /v1/watch/:id. |
An empty scopes array is unrestricted — a key issued with no scopes is granted all read
scopes. A key with a non-empty scopes array is granted only the scopes listed; calling a
route whose required scope is absent is rejected with 403 forbidden.
The watch:* scopes additionally require the key to have an owning org (consumerId); see the
Watchlists guide for tenancy, dual delivery, and webhook signing.
The MCP tools follow the same rule: both search_brands and get_brand_detail require
brands:read (or an unrestricted, empty-scope key).
Key lifecycle (operator-managed)
Keys are created, rotated, revoked, and expired by the operator via the admin CLI. As an integrator you do not manage keys yourself; you request changes from your operator.
- Rotate — the operator issues you a new key and decommissions the old one. Switch the
X-API-Keyvalue your client sends; no code change beyond the secret. - Revoke — a revoked key stops working immediately: subsequent calls return
401 unauthorized, exactly as for an unknown key. - Expiry — a key may carry an expiry date. Once past expiry it is treated as invalid and
returns
401 unauthorized. Ask your operator to reissue before expiry to avoid downtime.
Each key also belongs to a consumer and carries a monthly quota (see below).
Status semantics
These are the exact outcomes the auth and quota layers produce:
401 unauthorized
Returned when the key is missing, malformed, unknown, or revoked/expired.
{ "error": { "code": "unauthorized", "message": "Missing or invalid API key", "requestId": "..." } }
403 forbidden
Returned when the key is valid and live, but lacks the scope the route requires. The message names the missing scope:
{ "error": { "code": "forbidden", "message": "API key lacks required scope: insights:read", "requestId": "..." } }
429 quota_exceeded
Returned when the key's monthly quota is exhausted. Carries a Retry-After header with the
seconds until the quota window resets:
HTTP/1.1 429 Too Many Requests
Retry-After: 1209600
{ "error": { "code": "quota_exceeded", "message": "Monthly quota exceeded", "requestId": "..." } }
429 rate_limited
There are two rate limits, both additive to the monthly quota, and both return 429 with a
Retry-After header (seconds to wait):
- Per-key burst (
code: rate_limited) — a second tier keyed on the key hash that caps a single tenant: by default 300 requests every 60 s. It applies identically on REST and on the MCP (both transports share the same counter), is independent of the client IP (so a rotating IP pool cannot dodge it), and does not change the/v1contract schema — it is a purely additive new429condition.
```text HTTP/1.1 429 Too Many Requests Retry-After: 42
{ "error": { "code": "rate_limited", "message": "Too many requests (per-key burst limit)", "requestId": "..." } } ```
- Per-IP flood defense (
code: rate_limited) — a limit keyed on the client IP (default 100 requests/minute) that blunts a pre-auth flood (rotating keys). It is independent of the per-key tier.
Consumer action: honor the Retry-After and retry after that many seconds. The monthly quota
(quota_exceeded) and both rate limits (rate_limited) apply identically on REST and on the MCP,
since both transports authenticate against the same keys and share the same counters.
The uniform error envelope
{ error: { code, message, requestId } }is described in the getting-started guide. UserequestId(also echoed on thex-request-idresponse header) when reporting an issue to your operator.
CORS and browser access
IRIS is server-side-only by default: the X-API-Key is a server-side credential and must never
be embedded in a browser client. Therefore, by default the API emits no Access-Control-Allow-Origin
header — a third-party browser fetch cannot read the responses. This is the default posture and changes
nothing relative to prior versions.
Cross-origin browser access is opt-in per deploy via the IRIS_CORS_ALLOWLIST environment variable
(a comma-separated list of exact origins):
IRIS_CORS_ALLOWLIST=https://app.tarno.cl,https://docs.tarno.cl
- Empty / unset → no CORS (no
Access-Control-Allow-Originheader) — the default no-op. - With origins → only those exact origins receive the
Access-Control-Allow-Originheader (never a wildcard, never arbitraryOriginreflection); no credentials are sent (credentials:false). A preflightOPTIONSfrom those origins is answered with the allow headers.
The MCP server does not use CORS. MCP is a server-to-server transport (AI agents call it over the wire, not a browser
fetch), so CORS — a browser safeguard — does not apply there.
See also
- Getting started — base URL, first call, pagination, error envelope.
- Connect your AI agent (MCP) — same key, same scopes, over MCP.
- The live OpenAPI reference at
/docs— per-endpoint security and schema.