Getting started
IRIS exposes a neutral read contract over the INAPI trademark corpus: a versioned REST API plus an MCP server, both backed by the same query layer and the same data. This guide gets you from zero to a first authenticated call against the REST API.
The full, always-current per-field schema lives in the interactive OpenAPI reference at
/docs(Scalar). This guide explains how to onboard and links to that reference — it does not restate the schema. When a field is not documented here,/docsis authoritative.
1. Base URL and the /v1 prefix
The REST API is served by your operator over HTTPS. Every endpoint of the read contract lives
under the /v1 version prefix, e.g. https://<iris-host>/v1/brands. Throughout this guide
the base URL is written as the env var IRIS_BASE_URL — substitute the host your operator gave
you.
export IRIS_BASE_URL="https://<iris-host>"
A handful of paths are intentionally public (no key required):
| Path | Purpose |
|---|---|
/health |
Liveness / readiness probe (200 ok / 503 degraded) |
/docs |
Interactive OpenAPI (Scalar) reference |
/metrics |
Prometheus metrics (internal network only) |
Everything under /v1 is protected and requires an API key (see below and the auth guide).
2. Get an API key
API keys are issued by the operator, out of band, via the admin CLI. There is no
self-service signup endpoint — you do not create your own key. Ask your operator for a reader
key; they will hand you an opaque key string and tell you which scopes it carries
(brands:read, insights:read, or unrestricted). See the auth guide for scopes,
lifecycle (rotate / revoke / expiry), and the exact error semantics.
Store the key as an env var and never commit it to source control:
export IRIS_API_KEY="iris_xxx" # the opaque key string your operator issued
All examples in this portal use the obvious placeholder
iris_xxx. Replace it with your real key at runtime; never hard-code a key into a committed file.
3. Your first authenticated call: GET /v1/brands
Send the key in the X-API-Key request header. GET /v1/brands searches or lists brand
summaries.
curl
curl -sS "$IRIS_BASE_URL/v1/brands?q=cafe&limit=5" \
-H "X-API-Key: $IRIS_API_KEY"
TypeScript (fetch)
const baseUrl = process.env.IRIS_BASE_URL!;
const apiKey = process.env.IRIS_API_KEY!; // "iris_xxx"
const res = await fetch(`${baseUrl}/v1/brands?q=cafe&limit=5`, {
headers: { "X-API-Key": apiKey },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message} (requestId=${error.requestId})`);
}
const page = await res.json(); // { items: BrandSummary[], nextCursor: string | null }
console.log(page.items.length, "results; nextCursor =", page.nextCursor);
A successful response is a keyset page:
{
"items": [
{
"nroSolicitud": "123456",
"nroRegistro": null,
"denominacion": "CAFE EXAMPLE",
"estado": "...",
"tipoSigno": "...",
"fechaPresentacion": "2023-01-15",
"fechaVencimiento": null,
"score": 0.0163
}
],
"nextCursor": "b2Zmc2V0OjUw"
}
score is the fusion (RRF) relevance score and is present only on the free-text search
path (q=); it is omitted on a plain filtered listing. For the exact field set of
BrandSummary and BrandDetail, see the /docs reference.
4. Filtering and keyset pagination
GET /v1/brands accepts these query parameters:
| Param | Type | Notes |
|---|---|---|
q |
string | Free-text search (full-text + fuzzy). When present, results are ranked and carry score. Omit it for a plain filtered listing. |
clase |
integer | Niza class filter, 1–45. |
estado |
string | Status code (controlled catalog). |
tipoSigno |
string | Sign-type code (controlled catalog). |
fechaDesde |
YYYY-MM-DD |
Lower bound on the filing date (fecha_presentacion >=). |
fechaHasta |
YYYY-MM-DD |
Upper bound on the filing date (fecha_presentacion <=). |
limit |
integer | Page size. Default 20, maximum 100. |
cursor |
string | Opaque keyset cursor (see below). Treat it as a black box. |
Pagination is keyset-based, not offset-based. Each response carries a nextCursor:
- To fetch the next page, pass it back unchanged as the
cursorquery param. nextCursor: nullmeans end of results — stop.
# page 1
curl -sS "$IRIS_BASE_URL/v1/brands?q=cafe&limit=20" -H "X-API-Key: $IRIS_API_KEY"
# page 2 — pass the nextCursor from page 1
curl -sS "$IRIS_BASE_URL/v1/brands?q=cafe&limit=20&cursor=<nextCursor>" -H "X-API-Key: $IRIS_API_KEY"
To fetch one brand in full (classes + persons + Estado-Diario annotations), call
GET /v1/brands/:nroSolicitud:
curl -sS "$IRIS_BASE_URL/v1/brands/123456" -H "X-API-Key: $IRIS_API_KEY"
An unknown application number returns 404 not_found.
The insights:read family (GET /v1/freshness, GET /v1/sync-runs) is documented in the
auth guide and the /docs reference.
5. The uniform error envelope
Every error — validation, auth, not-found, rate-limit, and unexpected server errors — is returned as one uniform shape:
{ "error": { "code": "unauthorized", "message": "Missing or invalid API key", "requestId": "..." } }
code— a stable, neutral token you can switch on (see the table below).message— a human-readable description.5xxresponses use a generic message and never leak internal details.requestId— correlates your report with the server log. It is also echoed on thex-request-idresponse header; you may supply your own via theX-Request-Idrequest header.
| HTTP | code |
When |
|---|---|---|
| 400 | validation_error |
Invalid query / params (failed Zod validation). |
| 401 | unauthorized |
Missing, malformed, unknown, or revoked API key. |
| 403 | forbidden |
Valid key, but it lacks the scope the route requires. |
| 404 | not_found |
Unknown brand application number, or an unknown route. |
| 429 | rate_limited |
Short-term burst limit exceeded (see below). |
| 429 | quota_exceeded |
Monthly quota exhausted (see below). Carries Retry-After. |
| 500 | internal_error |
Unexpected server error (no internal details are leaked). |
6. Rate limits and quota
Two independent limits protect the read contract:
- Short-term burst limit — a per-key request rate cap (default 100 requests / minute). Exceeding it returns HTTP 429.
- Monthly quota — a per-key monthly request budget. When you exhaust it the API returns
HTTP 429 with
code: "quota_exceeded"and aRetry-Afterheader (seconds until the quota window resets at month end). This same quota applies identically on REST and on the MCP — see the MCP guide.
HTTP/1.1 429 Too Many Requests
Retry-After: 1209600
Content-Type: application/json
{ "error": { "code": "quota_exceeded", "message": "Monthly quota exceeded", "requestId": "..." } }
Honour Retry-After: wait the indicated number of seconds before retrying.
Next steps
- Authentication and scopes —
X-API-Key,brands:read/insights:read, key lifecycle, and the full401/403/429semantics. - Connect your AI agent (MCP) — the
mcp.obviouy.comendpoint and thesearch_brands/get_brand_detailtools. - Code samples — copy-paste REST and MCP clients.
- The live OpenAPI reference at
/docs— the authoritative per-field schema.