# Conventions

> Base URL, content types, currencies, pagination, timestamps, country codes, demo data markers, and other conventions that apply across every Haszno API endpoint.

## Base URL

All endpoints are served from one base URL:

```http
https://haszno.com
```

There is no version prefix in any path. The API is an unversioned developer preview; see [Changelog](changelog.md) for the versioning policy.

## Content types

| Direction | Header | Value |
|---|---|---|
| Request body | `Content-Type` | `application/json` |
| JSON responses | `Content-Type` | `application/json` |
| `/api/blog/rss.xml` | `Content-Type` | `application/rss+xml; charset=utf-8` |
| `/api/blog/feed.json` | `Content-Type` | `application/feed+json; charset=utf-8` |
| `/api/blog/:slug.md` | `Content-Type` | `text/markdown; charset=utf-8` |
| `/llms.txt` | `Content-Type` | `text/plain` |

Send `Content-Type: application/json` on every request that includes a body. A body that is not valid JSON or fails validation returns `400`. The account endpoints (`POST /api/auth/register`, `POST /api/auth/login`, `POST /api/auth/tokens`) also return `400` when the header is missing.

## Currency and prices

All monetary values are **integers in Hungarian Forints (HUF, ISO 4217)**. There are no decimal prices.

| Suffix | Meaning | Example field |
|---|---|---|
| `Net` | Excludes VAT | `costNet`, `marginNet` |
| `Gross` | Includes VAT | `priceGross`, `rrpGross`, `floorGross` |

The default VAT rate is 27 % (`vatPct: 27`), matching the Hungarian standard rate. The `vatPct` parameter on demo endpoints lets you change this for a single request; it does not persist.

Margin and floor formulas, with `net = priceGross / (1 + vatPct / 100)`:

- `marginNet = round(net − costNet)`: the net selling price minus the net cost, in whole forints.
- `marginPct = (net − costNet) / net × 100`, rounded to one decimal place; `0` when `net` is not positive.
- `marginNet` and `marginPct` are both `null` when `costNet` is `null`.
- `floorGross = costNet / (1 − minMarginPct / 100) × (1 + vatPct / 100)`, rounded up to the next price ending in 90 Ft (for example 72 431 → 72 490).

`marginPct` is a percentage with at most one decimal place, for example `31.9`. Every other price, cost and margin field is an integer.

## Null semantics

A `null` price or cost field means the value is **unknown or unavailable**, not zero. Treat `null` and `0` differently:

| Field | `null` means |
|---|---|
| `costNet: null` | Purchase cost unknown; `marginNet` and `marginPct` are `null` and `recommended.status` is `nincs_besz_ar`. |
| `marketMinGross: null` | No market price found in the requested shipping markets. |
| `rrpGross: null` | Supplier has not set a recommended retail price. |

Do not substitute zero for `null` in price comparisons or margin calculations.

## Timestamps

All timestamps are **ISO 8601 strings in UTC**, e.g. `2026-11-10T00:00:00.000Z`.


The API does not use Unix epoch integers. Demo timestamps such as `fetchedAt` and `addedAt` are fixed synthetic values and do not reflect real events; the `from` and `to` window of `GET /api/impact` is computed from the current time.

## Country codes and market parameters

Country codes follow **ISO 3166-1 alpha-2** (two uppercase letters, e.g. `HU`, `SK`).

The `shippingMarkets` query parameter accepts one or more country codes separated by commas. Valid values:

| Code | Country |
|---|---|
| `HU` | Hungary |
| `SK` | Slovakia |
| `DE` | Germany |
| `AT` | Austria |
| `CZ` | Czech Republic |
| `PL` | Poland |
| `RO` | Romania |

Default when omitted: `HU`.

In responses, `marketByCountry` has three fixed keys: `hu` (Hungary), `sk` (Slovakia), and `eu` (all other supported markets — DE, AT, CZ, PL, RO). A slot is `null` when no market price is available for that market.

## Pagination

Pagination applies to four endpoints only:

| Endpoint | Style |
|---|---|
| `GET /api/catalogue/products` | Page-based: `?page=` (1-based, default 1) + `?pageSize=` (1–100, default 24). Returns `{ items, total, facets }`. |
| `GET /api/rule-runs` | Limit: `?limit=N` (default 30, max 200). |
| `GET /api/proposals` | Limit: `?limit=N` (default 50, max 200). |
| `GET /api/blog` | Page-based: `?page=` (1-based, default 1), fixed page size of 10. Returns `{ posts, total, page, perPage }`. |

All other list endpoints return **all matching rows in a single response**. There is no cursor, `Link` header, or `nextPage` field on these endpoints. Pagination parameters sent to them are silently ignored.

> [!NOTE]
> `GET /api/orders` returns the last 50 orders only, with no mechanism to page further.

## Demo and synthetic data

The public demo at `https://haszno.com` serves synthetic sample data on its unauthenticated GET endpoints. Only the object responses of `GET /api/settings` and `GET /api/impact` carry a top-level field:

```json
"demo": true
```

Array responses have no such field. Recognise them by these markers:

| Marker | Where | Meaning |
|---|---|---|
| `"sku": "DEMO-{id}"` | Products | Placeholder SKU, not a real supplier code. |
| `"source": "mock"` | Market price records | Not a verified competitor price. |
| `"partnerCount": 0` | Categories | Real connected-partner count; always 0 on the demo. |

> [!WARNING]
> `GET /api/orders` and `GET /api/proposals` return `[]` on the public demo. This is by design; the demo does not simulate order history. Do not interpret an empty array as evidence that the endpoint works with real data.

## Language of API messages

API error messages and status enum strings are currently in **Hungarian only**, regardless of the `Accept-Language` header or the documentation language you are reading.

Examples:

| String | Context |
|---|---|
| `"error": "Ismeretlen cikkszám: DEMO-99"` | 400 error from `POST /api/webhooks/order` |
| `recommended.status: "versenykepes"` | The recommended price undercuts the cheapest market price and stays at or above the floor |
| `recommended.status: "nem_versenykepes"` | Undercutting the market would break the minimum margin; the recommended price is the floor |
| `recommended.status: "nincs_piaci_ar"` | No market price known; the recommended price is the RRP or the floor |
| `recommended.status: "nincs_besz_ar"` | Purchase cost unknown; no pricing recommendation can be made |

This will change before general availability. The `recommended.status` values are described in [GET /api/products](api-reference.md#get-products); HTTP status codes are listed in [Errors](errors.md).

## CORS

The API enforces **strict same-origin** CORS. The allowed origin is set server-side from the `CORS_ORIGIN` environment variable, falling back to `PUBLIC_URL`, then `http://localhost:5180`. On `haszno.com` the allowed origin is the production front-end.

| CORS setting | Value |
|---|---|
| Allowed methods | `GET POST PUT PATCH DELETE OPTIONS` |
| Allowed request headers | `Content-Type`, `Authorization`, `x-api-key`, `x-support-token` |
| Credentials (cookies) | Forwarded (`credentials: true`) |
| Preflight cache | 600 seconds |

Requests from an unlisted origin are rejected. Use `https://haszno.com` as the base URL.

## Request body size limit

| Endpoint | Limit |
|---|---|
| `POST /api/feeds/import` | 10 MB |
| All other endpoints | 1 MB |

Requests exceeding the limit return `413 Content Too Large`. See [Errors](errors.md#status-codes).
