# Stylumia Catalog API

Version 0.2.0 · OpenAPI 3.1.0

Base URL: https://api.stylumia.com

Auth: `Authorization: Bearer <your access token>`

Read-only access to Stylumia's retailer product catalog crawl.

**Every operation in this document is a draft** (`x-status: draft`). Shapes,
field names, and error codes may still change before general availability;
treat this as a preview, not a stability guarantee.

## One filter set, three operations

`retailer`, `category`, `department`, `rcd`, `start_date`, and `end_date`
are the same six parameters on every operation below — narrow once and
reuse the query anywhere:

- `GET /v1/sample` — up to 100 rows, for judging the shape of the data.
- `GET /v1/catalog` — the complete filtered set, cursor-paginated.
- `GET /v1/coverage` — how much exists for those filters, before you pull
  any of it.

There is no discovery chain to walk. A guessed `retailer`, `category`, or
`department` fails with `valid_options` naming the real values, which is
the only vocabulary lookup an integrator needs. `rcd` is shorthand for a
`retailer:category:department` combination — send one parameter instead
of three, or several comma-separated to select more than one combination.

Every response echoes the filters the server actually resolved in
`meta.resolved`, and carries a compact `meta.coverage` block, so one
request answers both "what did I get" and "how much is there".

## Error contract

Every non-2xx response is an RFC 9457 problem document
(`application/problem+json`) shaped by the `Problem` schema, carrying
`code`, `retryable`, `recovery`, and — on vocabulary errors —
`valid_options`. `retryable` is the field to branch on: `false` means fix
the request before resending, `true` means back off and retry. `recovery`
is human-readable guidance that matches the public error catalogue
byte-for-byte. Each operation below documents the exact set of codes it
can emit via `x-error-codes`.

## Credentials

Sign in to the developer portal, create a named API key, and copy it — the
secret is shown exactly once. Send it as
`Authorization: Bearer <your API key>`.

Keys do not expire; revoking is the only thing that ends one, and it takes
effect on the very next request. Create as many as you need — one per
service, script or agent — so a leak or a revocation never takes down more
than the one integration.

## GET /v1/sample

Sample rows

Up to 100 rows for the given filters. Uncursored — a sample is for
judging the shape of the data before committing to a full
`GET /v1/catalog` walk; anything larger belongs there instead.

Requires the `catalog:sample` scope, distinct from the `catalog:read`
scope `GET /v1/catalog` and `GET /v1/coverage` need.

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `retailer` | string | optional | Restrict to this retailer's slug. An unrecognized value fails with `unknown_retailer`, whose `valid_options` names the closest real slugs. |
| `category` | string | optional | Restrict to this category's slug, scoped by `retailer` if given. An unrecognized value fails with `unknown_category`, whose `valid_options` names the closest real slugs. |
| `department` | string | optional | Restrict to this department's slug, scoped by `retailer` if given. An unrecognized value fails with `unknown_department`, whose `valid_options` names the closest real slugs. |
| `rcd` | string | optional | One or more `retailer:category:department` triples, comma-separated. Shorthand for sending `retailer`, `category`, and `department` individually — send this instead when you already know the exact combination(s) you want. A malformed value fails with `invalid_request`. |
| `start_date` | string (date) | optional | Only rows with `crawl_date` on or after this date, inclusive. ISO 8601 (`YYYY-MM-DD`). Malformed, or after `end_date`, fails with `invalid_date_range`. |
| `end_date` | string (date) | optional | Only rows with `crawl_date` on or before this date, inclusive. ISO 8601 (`YYYY-MM-DD`). Malformed, or before `start_date`, fails with `invalid_date_range`. |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | ProductListResponse | Up to 100 sample product rows matching the given filters. |
| 400 | Problem | The request is malformed, or references an unrecognized retailer, category, department, date range, cursor, or `rcd` value. |
| 401 | Problem | No credentials were presented, or the presented token is unknown, expired, or revoked. |
| 403 | Problem | The token is valid but the team is not entitled to this operation. |
| 429 | Problem | The published per-team rate limit was exceeded. |
| 5XX | Problem | Something unforeseen failed on the server side, the service is down or degraded, or the server was too slow to answer. The request was not necessarily the problem — see `retryable`. |

### Errors

`invalid_request`, `unknown_retailer`, `unknown_category`, `unknown_department`, `invalid_date_range`, `missing_credentials`, `invalid_credentials`, `insufficient_scope`, `rate_limited`, `service_unavailable`, `upstream_timeout`, `internal_error`

## GET /v1/catalog

Get catalog

The complete filtered set, walked with a cursor. Results are sorted
by `sku` so a page's contents never shift under a caller mid-walk.

The cursor encodes position only — the same filters must be resent
unchanged on every page. Continue with the `cursor` from
`pagination.next_cursor` until `pagination.has_more` is `false`.

Requires the `catalog:read` scope.

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `retailer` | string | optional | Restrict to this retailer's slug. An unrecognized value fails with `unknown_retailer`, whose `valid_options` names the closest real slugs. |
| `category` | string | optional | Restrict to this category's slug, scoped by `retailer` if given. An unrecognized value fails with `unknown_category`, whose `valid_options` names the closest real slugs. |
| `department` | string | optional | Restrict to this department's slug, scoped by `retailer` if given. An unrecognized value fails with `unknown_department`, whose `valid_options` names the closest real slugs. |
| `rcd` | string | optional | One or more `retailer:category:department` triples, comma-separated. Shorthand for sending `retailer`, `category`, and `department` individually — send this instead when you already know the exact combination(s) you want. A malformed value fails with `invalid_request`. |
| `start_date` | string (date) | optional | Only rows with `crawl_date` on or after this date, inclusive. ISO 8601 (`YYYY-MM-DD`). Malformed, or after `end_date`, fails with `invalid_date_range`. |
| `end_date` | string (date) | optional | Only rows with `crawl_date` on or before this date, inclusive. ISO 8601 (`YYYY-MM-DD`). Malformed, or before `start_date`, fails with `invalid_date_range`. |
| `cursor` | string | optional | Opaque pagination cursor from a previous response's `pagination.next_cursor`. Omit for the first page. A cursor this server did not issue fails with `invalid_cursor`. |
| `limit` | integer | optional | Maximum rows to return in this page. |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | ProductListResponse | A page of product rows matching the given filters. |
| 400 | Problem | The request is malformed, or references an unrecognized retailer, category, department, date range, cursor, or `rcd` value. |
| 401 | Problem | No credentials were presented, or the presented token is unknown, expired, or revoked. |
| 403 | Problem | The token is valid but the team is not entitled to this operation. |
| 429 | Problem | The published per-team rate limit was exceeded. |
| 5XX | Problem | Something unforeseen failed on the server side, the service is down or degraded, or the server was too slow to answer. The request was not necessarily the problem — see `retryable`. |

### Errors

`invalid_request`, `unknown_retailer`, `unknown_category`, `unknown_department`, `invalid_date_range`, `invalid_cursor`, `missing_credentials`, `invalid_credentials`, `insufficient_scope`, `rate_limited`, `service_unavailable`, `upstream_timeout`, `internal_error`

## GET /v1/coverage

Get coverage

Coverage metrics for each retailer/category/department combination
matching the given filters: how many rows the full corpus holds,
how many match this filter, and the crawl-date range they span. Use
this to decide which `start_date` / `end_date` window on
`GET /v1/catalog` will actually return rows before spending a call
finding out.

Requires the `catalog:read` scope.

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `retailer` | string | optional | Restrict to this retailer's slug. An unrecognized value fails with `unknown_retailer`, whose `valid_options` names the closest real slugs. |
| `category` | string | optional | Restrict to this category's slug, scoped by `retailer` if given. An unrecognized value fails with `unknown_category`, whose `valid_options` names the closest real slugs. |
| `department` | string | optional | Restrict to this department's slug, scoped by `retailer` if given. An unrecognized value fails with `unknown_department`, whose `valid_options` names the closest real slugs. |
| `rcd` | string | optional | One or more `retailer:category:department` triples, comma-separated. Shorthand for sending `retailer`, `category`, and `department` individually — send this instead when you already know the exact combination(s) you want. A malformed value fails with `invalid_request`. |
| `start_date` | string (date) | optional | Only rows with `crawl_date` on or after this date, inclusive. ISO 8601 (`YYYY-MM-DD`). Malformed, or after `end_date`, fails with `invalid_date_range`. |
| `end_date` | string (date) | optional | Only rows with `crawl_date` on or before this date, inclusive. ISO 8601 (`YYYY-MM-DD`). Malformed, or before `start_date`, fails with `invalid_date_range`. |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | CoverageListResponse | Coverage metrics for every combination matching the given filters. |
| 400 | Problem | The request is malformed, or references an unrecognized retailer, category, department, date range, cursor, or `rcd` value. |
| 401 | Problem | No credentials were presented, or the presented token is unknown, expired, or revoked. |
| 403 | Problem | The token is valid but the team is not entitled to this operation. |
| 429 | Problem | The published per-team rate limit was exceeded. |
| 5XX | Problem | Something unforeseen failed on the server side, the service is down or degraded, or the server was too slow to answer. The request was not necessarily the problem — see `retryable`. |

### Errors

`invalid_request`, `unknown_retailer`, `unknown_category`, `unknown_department`, `invalid_date_range`, `missing_credentials`, `invalid_credentials`, `insufficient_scope`, `rate_limited`, `service_unavailable`, `upstream_timeout`, `internal_error`

## Schemas

### Product

A single crawled product row.

| Field | Type | Nullable | Description |
| --- | --- | --- | --- |
| `product_id` | string | no | Stable id, a content hash of the row. Listings are re-crawled, not diffed against a permanent catalog: a product relisted under a new title mints a new `product_id`, it does not update the old one. |
| `sku` | string | no | Retailer-specific product identifier. Format varies by retailer. Unique within a retailer only — the same `sku` string can appear under a different retailer for an unrelated product. |
| `url` | string (uri) | no | Canonical URL of the product page as crawled. |
| `domain` | string | no | Domain the product page was crawled from. |
| `retailer` | string | no | Slug of the retailer this row was crawled from. |
| `department` | string | no | Slug of the department this row belongs to. |
| `category` | string | no | Slug of the category this row belongs to. |
| `brand` | string | no | Brand name as listed on the retailer's page. |
| `model_number` | string | no | Manufacturer model number as listed on the retailer's page. |
| `title` | string | no | Product title as listed on the retailer's page. |
| `mrp` | number | no | Manufacturer's listed (undiscounted) price, in `currency`. |
| `selling_price` | number | no | Actual price charged at crawl time, in `currency`. |
| `discount_pct` | number | no | Discount off `mrp`, as a percentage, at crawl time. |
| `currency` | string | no | ISO 4217 currency code for `mrp` and `selling_price`. |
| `color` | string | yes | Listed color, when the retailer publishes one for this product. `null` when not applicable or not listed. |
| `image_url` | string (uri) | no | URL of the primary product image as crawled. |
| `crawl_date` | string (date) | no | Date (`YYYY-MM-DD`) this row was crawled. |

### Coverage

Coverage metrics for one retailer/category/department combination.

| Field | Type | Nullable | Description |
| --- | --- | --- | --- |
| `retailer` | string | no | Slug of the retailer. |
| `department` | string | no | Slug of the department. |
| `category` | string | no | Slug of the category. |
| `rcd` | string | no | This combination as a `retailer:category:department` triple, ready to pass back as `rcd`. |
| `corpus_rows` | integer | no | Total rows the full dataset holds for this combination, independent of any filter applied. |
| `matching_rows` | integer | no | Rows within this combination that also satisfy the request's date filters, if any. |
| `earliest_date` | string (date) | no | Earliest `crawl_date` (`YYYY-MM-DD`) among matching rows. |
| `latest_date` | string (date) | no | Most recent `crawl_date` (`YYYY-MM-DD`) among matching rows. |

### CoverageSummary

The compact coverage block carried on every response, answering "how much exists" alongside "what did I get" — summed across every combination matching the request's filters.

| Field | Type | Nullable | Description |
| --- | --- | --- | --- |
| `combinations` | integer | no | Number of retailer/category/department combinations matching the request's filters. |
| `matching_rows` | integer | no | Total rows across those combinations that also satisfy the request's date filters, if any. |
| `corpus_rows` | integer | no | Total rows the full dataset holds across those combinations, independent of any date filter. |
| `earliest_date` | string (date) | yes | Earliest `crawl_date` (`YYYY-MM-DD`) among matching rows. `null` when no combination matches. |
| `latest_date` | string (date) | yes | Most recent `crawl_date` (`YYYY-MM-DD`) among matching rows. `null` when no combination matches. |

### Pagination

Cursor pagination state for this response. `limit` and `total_matching` are present on `GET /v1/catalog`; `GET /v1/sample` and `GET /v1/coverage` are uncursored and omit them.

| Field | Type | Nullable | Description |
| --- | --- | --- | --- |
| `limit` | integer | yes | Maximum rows requested for this page. |
| `total_matching` | integer | yes | Total rows matching the request's filters, across every page. |
| `next_cursor` | string | yes | Opaque cursor for the next page. Pass back as the `cursor` parameter to continue. `null` when there is no further page. |
| `has_more` | boolean | no | Whether a further page exists beyond this one. |

### Meta

Response metadata.

| Field | Type | Nullable | Description |
| --- | --- | --- | --- |
| `request_id` | string | no | Unique id for this request. Quote it when reporting a problem, especially a 5xx. |
| `resolved` | object | no | Every filter the server actually applied, including omitted ones as `null`, so a response is never ambiguous about what was queried. |
| `coverage` | CoverageSummary | no |  |

### ProductListResponse

Response envelope for `GET /v1/sample` and `GET /v1/catalog`.

| Field | Type | Nullable | Description |
| --- | --- | --- | --- |
| `data` | array<Product> | no | The matching product rows for this page. |
| `pagination` | Pagination | no |  |
| `meta` | Meta | no |  |

### CoverageListResponse

Response envelope for `GET /v1/coverage`.

| Field | Type | Nullable | Description |
| --- | --- | --- | --- |
| `data` | array<Coverage> | no | Coverage metrics, one entry per matching retailer/category/department combination. |
| `pagination` | Pagination | no |  |
| `meta` | Meta | no |  |

### Problem

RFC 9457 problem details, extended for programmatic handling. Every non-2xx response has this shape, served as `application/problem+json`.

| Field | Type | Nullable | Description |
| --- | --- | --- | --- |
| `type` | string (uri) | no | Stable URI identifying this error type; resolves to its entry in the public error catalogue (`{base}/errors#{code}`). |
| `title` | string | no | Short, human-readable summary of the error type. Constant per `code`. |
| `status` | integer | no | HTTP status code, repeated from the response line for convenience. |
| `detail` | string | no | Human-readable explanation specific to this occurrence of the problem. |
| `code` | string | no | Stable machine-readable error code. Append-only — safe to branch on in code. |
| `retryable` | boolean | no | `false` means fix the request before resending; `true` means back off and retry — the request itself was not the problem. |
| `recovery` | string | no | What to do about this error, byte-identical to the public error catalogue. |
| `request_id` | string | no | Unique id for the request that produced this error. Quote it when reporting a problem. |
| `field` | string | yes | Name of the request parameter at fault. Present only on errors tied to one parameter. |
| `valid_options` | array<string> | yes | On vocabulary errors, the closest real values to what was sent (or the first few valid values), so a failed call can self-correct without a documentation round trip. |
