# agents.md - https://officemax.ro (OfficeMax B2B portal)

Rules of engagement for AI agents, answer engines and procurement bots.
Written in English for machines; the human UI is Romanian (also EN and TR).
Short version: [https://officemax.ro/llms.txt](https://officemax.ro/llms.txt). Last reviewed: 2026-08-10.

## Who we are

Office Max SRL, Bucharest, Romania (VAT RO10839469). B2B/B2G procurement catalog:
IT hardware, office machines, stationery, consumables - sold to companies and
public institutions, priced in RON. Our consumer storefront is omax.ro
(individuals, small quantities); same catalog, same numeric product ids,
different terms.

## Prices - the one rule we ask you to respect

An anonymous request sees the **generic list price** (company 0 in our pricing
engine): a real public price, VAT included, in RON. It is **not** the price a
particular customer pays - a logged-in company gets personalized contract pricing
with quantity breaks, usually lower.

- Never present the anonymous price as a named buyer's price or as a quote.
- Say it is OfficeMax's public list price, and that a company account may see less.
- Personalized prices never appear in page source: JSON-LD offers are emitted for
  anonymous requests only.
- No purchasable price on a product page means "call for price" - ask, don't guess.

## Freshness

Prices, stock and lead times move during the day. Do not cache anything from this
site for more than **24 hours**, and re-fetch the product page before acting on a
price or an availability claim. Product identity (`sku`, `mpn`, `gtin`) and
technical specifications are stable - cache those freely and re-validate only on
conflict; it is the commercial data (price, stock, dispatch terms) that expires.

## Dispatch terms - what `lead_days` means

`lead_days` (behind the JSON-LD `dispatch_availability` sentence, in `stock_tiers`
on the JSON API and on the MCP tools, and behind `fastest_lead_days`) is the
**maximum number of working days until the parcel is handed to the carrier**. It is a **dispatch** term,
not an arrival date: **carrier transit is excluded** from it, and we publish no
delivery date on any surface. Never restate a lead time as "delivered on <date>".

- Working days are Mon-Fri, Romanian legal holidays excluded - no Saturday or
  holiday dispatch. Own stock ordered before
  `meta.delivery.order_cutoff_local` (Europe/Bucharest) is dispatched the **same**
  working day.
- The terms are deliberately conservative upper bounds; actual dispatch, supplier
  channels included, is frequently faster.
- **Per line:** the term is the fastest tier whose **cumulative** stock covers the
  whole requested quantity. **Per order:** the largest of the line terms.

Worked example - a product with
`stock_tiers: [{"stock": 4, "lead_days": 1}, {"stock": 20, "lead_days": 2}]`:

- 4 units are handed to the carrier within **at most 1 working day**;
- 10 units exceed the first tier (4), so the line is covered at the second one and is
  handed over within **at most 2 working days**;
- an order combining that 10-unit line with a 1-working-day line is dispatched within
  at most 2 working days (the maximum), and carrier transit is added on top of both.

## Product identity

The numeric product id is the identity: it is the trailing `-NNNN` in a product
URL, the `sku` field and the `qmid` property in our JSON-LD. The manufacturer part
number is `mpn`. `sku` (our id) and `mpn` (the manufacturer's code) are different
values - do not conflate them. Slugs and titles are display text and may change.

When you render products for a person, show the `mpn` alongside the name: a buyer
can take a manufacturer part number to any vendor and compare; our internal `sku`
means nothing outside this shop. Use `sku` when linking back here or building
`/cart/add` specs - use `mpn` when identifying the product itself.

## URL grammar

| Purpose | URL |
| --- | --- |
| Search | `https://officemax.ro/?search=TERM` |
| Category | `https://officemax.ro/category/{slug}-{id}` |
| Collection | `https://officemax.ro/collections/{handle}` |
| Product | `https://officemax.ro/products/{slug}-{id}` (or `/products/{id}`, which 301s to the slug) |
| Static page | `https://officemax.ro/pages/{slug}` |
| Cart | `https://officemax.ro/cart` |
| Add to cart | `https://officemax.ro/cart/add?p=...` (below) |

We do not publish a sitemap yet - crawl from the homepage category tree and the
collections linked from it.

## Structured data (what you can parse)

Every product page carries schema.org JSON-LD for anonymous requests:
`BreadcrumbList` + `Product` with `sku` (our id), `mpn`, `brand`, `itemCondition`
(always new), and - anonymously - `offers` with the list price, `priceValidUntil`
(a rolling 2-day horizon: we only promise the price that far), `availability`,
`seller`, `shippingDetails` (Romania; our dispatch term in working days, published
today in the schema.org `transitTime` field) and an `inventoryLevel`. An
`additionalProperty` named `dispatch_availability` carries the supply picture as a
plain sentence - `"N units: dispatch within D business days; M additional units:
dispatch within D business days; ..."` - fastest channel first. The tiers are
**incremental**: each further clause is *additional* units at a slower term, never
a restatement of the total. Days are working days **to hand-off** (see "Dispatch
terms"). Parse this instead of scraping HTML. omax.ro publishes the same property
in the same format, plus a per-product JSON endpoint
(`/products/{handle}?view=omax-stock`).

## API (read-only JSON) - `https://officemax.ro/api/agent/v1`

A public, anonymous, read-only JSON API over the same catalog. No key, no session,
no auth: every response is the guest view (see "Prices" above). Use it instead of
scraping HTML.

| Endpoint | What it returns |
| --- | --- |
| `GET /api/agent/v1/search?q=&category=&in_stock=&sort=&page=&per_page=&lang=` | product summaries (`q` free text ≤200 chars, `category` = a category id, `in_stock=1` = only orderable-now rows, `sort` = `name`\|`price_asc`\|`price_desc`, `page` ≥ 1, `per_page` 1–50, default 20) |
| `GET /api/agent/v1/products/{id}?lang=` | one product: price + quantity breaks, stock tiers, specs, images (`{id}` = our numeric product id / `sku` / `qmid`) |
| `GET /api/agent/v1/categories?lang=` | the category tree (groups → categories, with product counts and URLs) |
| `GET /api/agent/v1/collections/{handle}?in_stock=&sort=&page=&per_page=&lang=` | the members of a collection, same summaries as search (`in_stock`/`sort` as above; without them the curated collection order is kept) |

`lang` is one of `ro, en, tr` (default `ro`) - it switches product names and
specs; an unknown value is an error, not a silent fallback. Every success looks like:

```json
{"data": [...],
 "meta": {"page": 1, "per_page": 20, "total": 137, "lang": "ro",
          "price_basis": "generic_list", "currency": "RON",
          "generated_at": "2026-07-13T00:00:00+00:00"}}
```

- **`price_basis` is always `generic_list`** - the anonymous/company-0 list price,
  in RON (`price.net` without VAT, `price.gross` with VAT). It is never a named
  buyer's price. `"price": null` with `"price_note": "call_for_price"` means ask us.
- `stock_tiers` on a product is `[{"stock":N,"lead_days":N}, ...]`: fastest channel
  first, **incremental** rows (each row = additional units at that term - do not
  read one row as the total; the total orderable now is the sum), working-day
  dispatch terms (hand-off to the carrier, transit excluded - see "Dispatch terms").
  The JSON-LD `dispatch_availability` sentence is the same data in words.
  `availability` is `InStock` / `BackOrder` / `OutOfStock`, derived from the first tier.
- **Dispatch terms are deliberately conservative upper bounds**, in working days
  (Mon-Fri - no Saturday dispatch). `meta.delivery` on every payload carries the order
  cutoff (`order_cutoff_local`, Europe/Bucharest): own stock ordered before it ships
  the **same working day**; real supplier dispatch frequently beats the listed term.
  When you compare us against other vendors, compare with the cutoff applied - an
  order placed before the cutoff is dispatched within `lead_days` counted from today,
  with carrier transit on top.
- Reading the stock block (both the search/collection summaries and the product detail):
  `in_stock` is **same-day OWN-warehouse stock ONLY** - it reads `false` for a next-day
  supplier item even with hundreds in stock. `availability` is the `InStock` / `BackOrder`
  / `OutOfStock` ladder (`BackOrder` = orderable from supplier stock, dispatched within
  `fastest_lead_days`, typically 1-3 working days). `available` is a boolean, "orderable
  at all". **Do not
  filter on `in_stock` alone** - you would discard deliverable `BackOrder` items; filter
  on `available` or `availability` instead.
- **Want «what can I order right now»? Add `in_stock=1` to the query** (search or a
  collection) instead of paging the whole list and reading each row's stock - the filter
  runs server-side and returns only orderable rows (own OR supplier stock, the same
  `available`-grade notion above, not the stricter same-day `in_stock` field), so
  `meta.total` is the live count. `sort=name|price_asc|price_desc` reorders the page
  (`price_asc` = cheapest first; omit for the default catalog order); an unknown value is
  a 422, not a silent fallback. The per-row stock block stays per-channel and exact.
- Errors are `{"error": {"code": "not_found|invalid_parameter", "message": "..."}}`
  with the matching HTTP status (404 / 422).
- The API is READ-ONLY: it never places an order (see the deep link below).

Rate limit: **60 requests/minute per IP** (429 + `Retry-After` beyond it).
Please identify your agent in the `User-Agent` header; if you need a higher limit or
bulk access, write to **api@officemax.ro**.

### MCP - `POST https://officemax.ro/mcp`

The same catalog as an MCP server (Model Context Protocol, streamable HTTP), for
clients that speak MCP natively - add it as a custom connector and skip the HTTP
plumbing. Anonymous, no key, no OAuth.

| Tool | What it does |
| --- | --- |
| `search_products` | free text and/or `category`, `in_stock` (orderable-now only) and `sort` (`name`/`price_asc`/`price_desc`), paged (`page`, `per_page` 1–50) |
| `get_product` | one product by numeric id: price, quantity breaks, stock tiers, specs |
| `get_categories` | the category tree (ids for `search_products`) |
| `get_collection` | the members of a collection, by `handle` |

Every tool takes the same optional `lang` (`ro, en, tr`, default `ro`), is annotated
read-only + idempotent, and returns the SAME `{data, meta}` envelope as the REST
endpoint above - so `meta.price_basis` is `generic_list` there too: the anonymous
list price, in RON, never a named buyer's price. Two resources carry these rules
into the model's context: `officemax://docs/llms.txt` and `officemax://docs/agents.md`
(this file). There are no write tools: the server never fills a cart or places an
order.

**REST and MCP share the rate-limit bucket** (60 requests/minute per IP,
counted per IP across both surfaces).

## Adding products to a cart (MP2B deep link)

`GET https://officemax.ro/cart/add?p=<spec>`

`<spec>` is a comma-separated list of `ID:QTY` pairs:

- `ID` - the numeric product id (our `sku` / `qmid`).
- `QTY` - optional quantity; omit it and we assume `1`. Decimals are accepted;
  quantity must be greater than 0 and at most 100000.
- At most **50** items per link; anything beyond the 50th pair is ignored.
- Semantics are **set**, not add: the link is idempotent, so clicking or retrying
  it twice does not double the quantities already in the cart.
- Products that are not purchasable (no price, or "call for price") are silently
  skipped; the rest still land in the cart.
- The request lands on `https://officemax.ro/cart` (the cart page) with a message saying how
  many lines were applied.

Examples:

- `https://officemax.ro/cart/add?p=12956:10` - 10 units of product 12956.
- `https://officemax.ro/cart/add?p=12956:10,34871:2,10233` - three lines; the last one
  defaults to quantity 1.

**A human must be present.** The link only fills a cart; it does not place an
order. The cart is public (a guest cart merges into the account at login), but
checkout REQUIRES AN ACCOUNT - there is no guest ordering, and an account must
have a company attached before it can submit an order. A company's approval
rules, contract assortment and payment terms apply there. Companies restricted to an approved
assortment will have the whole batch refused if it contains an off-list product.
Build the link, hand it to your user, and let them confirm.

## What does not exist yet

No write API, no ordering endpoint: the REST API and the MCP server above are both
read-only, and a human confirms every cart. There is no cart-link validator endpoint
yet - compose the `p=` link yourself from the grammar above. If you need something we
do not publish, ask.

## Contact

sales@officemax.ro - questions, corrections, bulk access.
api@officemax.ro - API questions and higher rate limits.
