# Public API endpoints

> Complete reference for every endpoint the Haszno hosted demo serves without credentials.

> [!NOTE]
> The Haszno API is in developer preview. All data returned by the hosted demo at `https://haszno.com` is **synthetic sample data** unless noted otherwise. See [Conventions](conventions.md) for null semantics, currency, and timestamp formats.

## Summary

Every endpoint in this table is callable against `https://haszno.com` without an `Authorization` header or any other credential. For endpoints that require credentials, see [Protected endpoints](protected-endpoints.md).

| Method | Path | Returns |
|--------|------|---------|
| GET | `/api/health` | Liveness check |
| GET | `/api/geo` | Visitor country and suggested UI language |
| GET | `/api/categories` | Category taxonomy with product counts |
| GET | `/api/products` | Product catalogue with pricing and market data |
| GET | `/api/products/supplier-counts` | Product count per supplier |
| GET | `/api/suppliers` | Configured supplier list |
| GET | `/api/stats` | Dashboard summary numbers |
| GET | `/api/settings` | Active pricing rules |
| GET | `/api/impact` | Profit impact attributed to Haszno |
| GET | `/api/orders` | Last 50 orders (empty array on demo) |
| GET | `/api/proposals` | Price proposals (empty array on demo) |
| GET | `/api/peers/invites` | Peer invite codes (empty array on demo) |
| GET | `/api/peers/links` | Established peer links (empty array on demo) |
| GET | `/api/import-list` | Import list staging items |
| GET | `/api/blog` | Published market insight posts (real data) |
| GET | `/api/blog/:slug` | Single published post (real data) |
| GET | `/api/blog/rss.xml` | RSS feed of posts |
| GET | `/api/blog/feed.json` | JSON Feed of posts |
| GET | `/llms.txt` | LLM-readable site summary |

---

## System

### GET /api/health

Returns a liveness indicator. Use this to verify that the API is reachable before making other calls.

**Public** **Read-only**

**Parameters**

None.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/health
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `ok` | boolean | Always `true` when the server is up. |

```json
{"ok":true}
```

---

### GET /api/geo

Returns the visitor's country code and the suggested UI language. The country is derived from request headers set by the edge network (`x-vercel-ip-country` or `cf-ipcountry`). The API server never stores or logs IP addresses.

**Public** **Read-only**

**Parameters**

None.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/geo
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `country` | string \| null | Two-letter ISO 3166-1 country code, or `null` if the country cannot be determined. |
| `lang` | `"hu"` \| `"en"` | Suggested UI language: `"hu"` for Hungary or unknown, `"en"` for all other countries. |

```json
{"country":"HU","lang":"hu"}
```

> [!NOTE]
> `Cache-Control: private, no-store` is set on this response; do not cache it on a shared layer.

---

## Demo catalogue data

The following endpoints return **synthetic sample data**. The response body includes `"demo": true` where applicable. `partnerCount` is always `0` in the demo — no real suppliers are connected.

---

### GET /api/categories

Returns the full category taxonomy used to organise products. Each entry includes counts for the demo data set.

**Public** **Read-only**

**Parameters**

None.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/categories
```

**Response (200)**

Returns an array of category objects. All 14 categories are always returned; there is no filtering or pagination.

| Field | Type | Description |
|-------|------|-------------|
| `key` | string | Internal category key used in filter parameters, e.g. `"kert"`. |
| `label` | string | Hungarian display name, e.g. `"Kert és növény"`. |
| `emoji` | string | Emoji icon for the category. |
| `partnerCount` | integer | Number of real connected suppliers. Always `0` on the demo. |
| `exampleCount` | integer | Number of demo/example suppliers. |
| `productCount` | integer | Total products in the category across all suppliers. |

```json
[
  {
    "key": "auto",
    "label": "Autó, gumi és motor",
    "emoji": "🛞",
    "partnerCount": 0,
    "exampleCount": 1,
    "productCount": 100
  },
  {
    "key": "kert",
    "label": "Kert és növény",
    "emoji": "🌿",
    "partnerCount": 0,
    "exampleCount": 1,
    "productCount": 100
  }
]
```

**Notes**

- `partnerCount: 0` is expected on the demo — no real suppliers are connected.
- Use the `key` value as the `cats` parameter when filtering products or stats.

---

### GET /api/products

Returns the product catalogue with gross prices, net costs, market price comparisons, and pricing recommendations. This is the primary read endpoint for integration work.

**Public** **Read-only**

> [!NOTE]
> The demo returns synthetic products only. `sku` values follow the pattern `DEMO-{id}`. Market prices have `"source": "mock"`. Pricing parameters (`undercutPct`, `minMarginPct`, `vatPct`) are accepted but do not persist — they affect only the current response.

**Parameters**

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `cats` | query | comma-separated string | No | (all) | Category keys to include, e.g. `kert,sport`. |
| `supplier` | query | comma-separated integers | No | (all) | Supplier IDs to include. |
| `all` | query | `"1"` | No | omit | Real instances only; the demo ignores it. When set to `"1"`, includes catalogue-only (unlisted) products. |
| `shippingMarkets` | query | comma-separated ISO 3166-1 alpha-2 | No | `HU` | Demo only. Countries for which to return market price comparisons. Valid values: `HU SK DE AT CZ PL RO`. A real instance uses the shop's saved `shippingMarkets` setting. |
| `undercutPct` | query | number | No | `1` | Demo only. Percentage to undercut the market minimum when computing the recommended price. Does not persist. |
| `minMarginPct` | query | number | No | `10` | Demo only. Minimum net margin percentage enforced by the pricing engine. Does not persist. |
| `vatPct` | query | number | No | `27` | Demo only. VAT rate used for gross/net conversion in `recommended`. Does not persist. |

**Request**

```bash
curl --fail-with-body \
  'https://haszno.com/api/products?cats=kert&shippingMarkets=HU,SK'
```

**Response (200)**

Returns an array. Each item represents one product.

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Internal product ID. |
| `sku` | string | Supplier SKU. `DEMO-{id}` on the demo. |
| `name` | string | Product name (Hungarian). |
| `supplierId` | integer | Supplier ID. |
| `supplierName` | string | Supplier display name. |
| `category` | string | Category key (matches `GET /api/categories`). |
| `costNet` | integer (HUF) | Net wholesale cost excluding VAT. |
| `priceGross` | integer (HUF) | Current gross selling price including VAT. |
| `rrpGross` | integer \| null | Supplier's recommended retail price, gross. `null` if not provided. |
| `marketMinGross` | integer \| null | Cheapest gross price found across monitored markets. `null` if no data. |
| `marketShop` | string \| null | Shop name that holds the cheapest offer. |
| `stock` | integer | Stock units available. |
| `listed` | boolean | Whether the product is actively listed. |
| `example` | boolean | `true` for demo/example supplier products. Present on real instances only; absent from the synthetic demo response. |
| `current` | object | `{marginNet, marginPct}` — margin at the current price; both `null` when `costNet` is unknown. |
| `recommended` | object | Pricing suggestion — see sub-fields below. |
| `marketByCountry` | object | Fixed keys `hu`, `sk` and `eu`. Each slot: market offer object or `null`. |
| `bestMarket` | object \| null | `{market, ratio}`: the market (`"hu"`, `"sk"` or `"eu"`) with the highest `minGross / priceGross` ratio; a `ratio` above 1 means the shop is already the cheapest there. Always `null` on the demo. |
| `markets` | array | Market offer records: on the demo one per requested country, on a real instance one per market (`hu`, `sk`, `eu`) that has price data. |
| `trendKeyword` | string \| null | Hungarian trend search keyword for the product. Present on real instances only; absent from the synthetic demo response. |
| `trendKeywordEn` | string \| null | English trend keyword. Present on real instances only; absent from the synthetic demo response. |
| `updatedAt` | ISO-8601 string \| null | Timestamp of last update. Present on real instances only; absent from the synthetic demo response. |

**`recommended` sub-fields**

| Field | Type | Description |
|-------|------|-------------|
| `priceGross` | integer (HUF) | Suggested gross selling price. |
| `floorGross` | integer (HUF) | Lowest gross price that still keeps `minMarginPct`; `0` when `costNet` is unknown. Formula in [Conventions](conventions.md#currency). |
| `marginNet` | integer (HUF) \| null | Net margin at the recommended price. `null` when `costNet` is unknown. |
| `marginPct` | number \| null | Margin percentage at the recommended price. `null` when `costNet` is unknown. |
| `status` | string | Pricing status — see enum below. |

**`recommended.status` enum**

| Value | Meaning |
|-------|---------|
| `"versenykepes"` | Competitive — a market price is known, and the recommended price undercuts it by `undercutPct` (rounded to a price ending in 90 Ft) while staying at or above `floorGross`. |
| `"nem_versenykepes"` | Not competitive — beating the market price would break the minimum margin, so the recommended price stays at `floorGross`. |
| `"nincs_piaci_ar"` | No market price is known. The recommended price is `rrpGross` when it is at or above `floorGross`, otherwise `floorGross`. |
| `"nincs_besz_ar"` | The purchase cost (`costNet`) is unknown, so no pricing recommendation can be made: `priceGross` keeps the current price, `floorGross` is `0`, and `marginNet` and `marginPct` are `null`. |

**Market offer object (each slot in `marketByCountry` and each item in `markets`)**

| Field | Type | Description |
|-------|------|-------------|
| `market` | string | Market key: `"hu"`, `"sk"` or `"eu"`. |
| `country` | string \| null | Two-letter ISO country code of the cheapest offer, e.g. `"DE"` for the `eu` market. |
| `minGross` | integer (HUF) | Cheapest gross price found in this market. |
| `shop` | string | Shop name for the cheapest offer. |
| `url` | string \| null | URL of the cheapest listing. `null` on the demo. |
| `source` | string | Data source: `"dataforseo"` or `"mock"`. Always `"mock"` on the demo. |
| `fetchedAt` | ISO-8601 string | When the market price was fetched. |
| `isCheapest` | boolean | Whether this market holds the overall cheapest offer. |

The demo returns this full object in both places. On a real instance, `marketByCountry` slots have no `market` or `isCheapest` field, and `markets` items have no `fetchedAt`.

```json
{
  "id": 101,
  "category": "kert",
  "supplierId": 2,
  "sku": "DEMO-101",
  "name": "Kerti napernyő · 1. változat",
  "supplierName": "Példa-partner · Kert és növény",
  "costNet": 1121,
  "priceGross": 2190,
  "rrpGross": 2190,
  "marketMinGross": 2124,
  "marketShop": "Példa-bolt · HU",
  "stock": 0,
  "listed": true,
  "current": {"marginNet": 603, "marginPct": 35},
  "recommended": {
    "priceGross": 2090,
    "floorGross": 1590,
    "status": "versenykepes",
    "marginNet": 525,
    "marginPct": 31.9
  },
  "marketByCountry": {
    "hu": {
      "market": "hu",
      "country": "HU",
      "minGross": 2124,
      "shop": "Példa-bolt · HU",
      "url": null,
      "source": "mock",
      "fetchedAt": "2026-11-10T00:00:00.000Z",
      "isCheapest": true
    },
    "sk": {
      "market": "sk",
      "country": "SK",
      "minGross": 2650,
      "shop": "Példa-bolt · SK",
      "url": null,
      "source": "mock",
      "fetchedAt": "2026-11-10T00:00:00.000Z",
      "isCheapest": false
    },
    "eu": null
  },
  "bestMarket": null,
  "markets": [
    {"market":"hu","country":"HU","minGross":2124,"shop":"Példa-bolt · HU",
     "url":null,"source":"mock","fetchedAt":"2026-11-10T00:00:00.000Z","isCheapest":true},
    {"market":"sk","country":"SK","minGross":2650,"shop":"Példa-bolt · SK",
     "url":null,"source":"mock","fetchedAt":"2026-11-10T00:00:00.000Z","isCheapest":false}
  ]
}
```

**Notes**

- `null` on a price field means unknown or unavailable, not zero.
- `source: "mock"` on market prices means the price is not a verified live market result.
- On the public demo, `current.marginPct` is a fixed placeholder (`35`) that does not follow from the prices, while `current.marginNet` is computed as `round(priceGross / 1.27 − costNet)`, always at 27 % VAT. Only `recommended` reflects the `undercutPct`, `minMarginPct` and `vatPct` parameters.
- Returns all matching products in a single array. There is no pagination on this endpoint.

---

### GET /api/products/supplier-counts

Returns the number of products per supplier. Useful for displaying a supplier breakdown without loading the full catalogue.

**Public** **Read-only**

**Parameters**

On the demo, the same filtering parameters as `GET /api/products`: `cats`, `supplier`. A real instance ignores them and counts the listed products of every supplier.

**Request**

```bash
curl --fail-with-body 'https://haszno.com/api/products/supplier-counts?cats=kert'
```

**Response (200)**

Returns an array. Only suppliers with at least one matching product are included.

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Supplier ID. |
| `name` | string | Supplier display name. |
| `count` | integer | Number of products matching the filter. |

```json
[
  {"id": 2, "name": "Példa-partner · Kert és növény", "count": 100}
]
```

---

### GET /api/suppliers

Returns the list of suppliers configured in the shop. On the demo, returns synthetic example suppliers.

**Public** **Read-only**

**Parameters**

None.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/suppliers
```

**Response (200)**

Returns an array of supplier objects.

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Supplier ID. |
| `name` | string | Supplier display name. |
| `category` | string | Primary category key. |
| `visibility` | string | `"private"` or `"network"`. |
| `network` | boolean | Whether the supplier is part of the shared network. |
| `orderMode` | string | How orders are forwarded (e.g. `"email"`). |
| `email` | string \| null | Supplier contact email, or `null`. |

```json
[
  {
    "id": 1,
    "name": "Példa-partner · Autó, gumi és motor",
    "category": "auto",
    "visibility": "private",
    "network": false,
    "orderMode": "email",
    "email": null
  }
]
```

---

### GET /api/stats

Returns dashboard summary numbers. These are computed across all products matching the optional filters.

**Public** **Read-only**

**Parameters**

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `cats` | query | comma-separated string | No | (all) | Filter by category keys. |
| `supplier` | query | comma-separated integers | No | (all) | Filter by supplier IDs. |

**Request**

```bash
curl --fail-with-body https://haszno.com/api/stats
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `products` | integer | Number of actively listed products. |
| `losing` | integer | Products with a negative net margin at the current price. |
| `overMarket` | integer | Products priced above the cheapest known competitor. |
| `toFix` | integer | Products where the current price differs from the recommended price. |
| `orders` | integer | Total order count (not window-scoped). |

```json
{"products":1400,"losing":0,"overMarket":1400,"toFix":1400,"orders":0}
```

**Notes**

- `orders: 0` is expected on the demo — no real orders exist.
- `overMarket: 1400` on the demo reflects that all 1 400 synthetic products are above their (synthetic) market minimum.

---

### GET /api/settings

Returns the current pricing rule configuration for the shop.

**Public** **Read-only**

> [!NOTE]
> The demo response contains `"demo": true` and `"automationPaused": true`. The `undercutPct`, `minMarginPct`, and `vatPct` fields shown here are read-only on the demo; write them via `PUT /api/settings` on a real instance.

**Parameters**

None.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/settings
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Always `1`. Settings are a singleton per shop. Absent in the hosted demo response. |
| `undercutPct` | number | Percentage to undercut the market minimum when computing the recommended price. |
| `minMarginPct` | number | Minimum net margin percentage enforced by the floor price. |
| `vatPct` | number | VAT rate used for all gross/net conversions. |
| `shippingMarkets` | string[] | ISO country codes for which market prices are monitored. |
| `automationPaused` | boolean | Whether the automation engine is paused. |
| `shopId` | integer \| null | Owning shop. Present on real instances only. |
| `demo` | boolean | Present and `true` on the hosted demo. |

```json
{
  "undercutPct": 1,
  "minMarginPct": 10,
  "vatPct": 27,
  "shippingMarkets": ["HU"],
  "automationPaused": true,
  "demo": true
}
```

---

### GET /api/impact

Returns the extra profit attributed to Haszno's repricing and new-listing engine over a configurable time window.

**Public** **Read-only**

> [!NOTE]
> Demo values are computed from synthetic order history. `arfutarImpact` may exceed `profitNet` on the demo — this is expected when the attribution formula compares against a hypothetical baseline using synthetic data.

**Parameters**

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `days` | query | integer 1–365 | No | `30` | Attribution window in days, counting back from now. |
| `cats` | query | comma-separated string | No | (all) | Restrict to these category keys. |

**Request**

```bash
curl --fail-with-body 'https://haszno.com/api/impact?days=30'
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `days` | integer | Requested window length. |
| `from` | ISO-8601 string | Window start. |
| `to` | ISO-8601 string | Window end. |
| `orders` | integer | Order count within the window. |
| `revenueGross` | integer (HUF) | Total gross revenue in the window. |
| `profitNet` | integer (HUF) | Total net profit across all orders in the window. |
| `arfutarImpact` | integer (HUF) | Extra profit attributed to Haszno (sum of `fromRepricing` and `fromNewProducts`). |
| `fromRepricing` | integer (HUF) | Impact from repriced products. |
| `fromNewProducts` | integer (HUF) | Impact from newly listed products. |
| `lossProductsFixed` | integer | Loss-making products corrected by repricing. |
| `autoForwardedOrders` | integer | Orders forwarded or fulfilled automatically. |
| `minutesPerOrder` | number | Estimated manual minutes saved per order. |
| `items` | array | Per-product attribution breakdown — see sub-fields below. |
| `demo` | boolean | Present and `true` on the hosted demo. |

**`items` sub-fields**

| Field | Type | Description |
|-------|------|-------------|
| `productId` | integer | Product ID. |
| `name` | string | Product name. |
| `kind` | string | `"arazas"` (repricing) or `"felvetel"` (new listing). |
| `oldPriceGross` | integer (HUF) \| null | Gross price before the change. `null` for new listings. |
| `newPriceGross` | integer (HUF) | Gross price after the change. |
| `unitsBeforePerDay` | number | Average units sold per day at the old price, before the change. |
| `unitsAfter` | integer | Total units sold after the change within the attribution window. |
| `actualProfit` | integer (HUF) | Actual net profit from units sold after the change. |
| `expectedProfit` | integer (HUF) | Baseline profit at the old price and old sales pace. `0` for new listings. |
| `impact` | integer (HUF) | Profit impact attributed to this product (`actualProfit − expectedProfit`). `0` when `insufficientHistory` is `true`. |
| `insufficientHistory` | boolean | `true` when there is not enough order history for a reliable calculation. Such items are excluded from the totals. |

```json
{
  "days": 30,
  "from": "2026-10-13T14:08:40.424Z",
  "to": "2026-11-12T14:08:40.424Z",
  "orders": 80,
  "revenueGross": 383700,
  "profitNet": 92996,
  "arfutarImpact": 110647,
  "fromRepricing": 66658,
  "fromNewProducts": 43989,
  "lossProductsFixed": 2,
  "autoForwardedOrders": 64,
  "minutesPerOrder": 6,
  "items": [
    {
      "productId": 3,
      "name": "Ablaktörlő · 1. változat",
      "kind": "felvetel",
      "oldPriceGross": null,
      "newPriceGross": 3990,
      "unitsBeforePerDay": 0,
      "unitsAfter": 40,
      "actualProfit": 43989,
      "expectedProfit": 0,
      "impact": 43989,
      "insufficientHistory": false
    }
  ],
  "demo": true
}
```

---

### GET /api/orders

Returns the last 50 orders, most recent first. The public demo returns an empty array.

**Public** **Read-only**

> [!NOTE]
> Returns `[]` on the hosted demo. No synthetic orders are generated for public access. A real instance returns order objects with `items` and `events` arrays.

**Parameters**

None on the demo. A real instance accepts `cats` (comma-separated category keys) to return only orders with at least one item from those categories.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/orders
```

**Response (200)**

Returns an array of order objects (empty on the demo), newest first. Each real order contains: `id`, `externalId`, `customerName`, `customerEmail`, `address`, `status`, `supplierOrderId`, `trackingNumber`, `totalGross` (HUF), `events` (array of `{at, text}`), `createdAt`, `forwardingStartedAt`, `shopId`, and `items` (array of `{orderId, qty, priceGross, name, sku}`).

```json
[]
```

---

### GET /api/proposals

Returns price proposals generated by the pricing engine. The public demo returns an empty array.

**Public** **Read-only**

> [!NOTE]
> Returns `[]` on the hosted demo.

**Parameters**

None on the demo. A real instance accepts `status` (default `pending`), `limit` (default 50, max 200) and `cats`.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/proposals
```

**Response (200)**

Returns an array of proposal objects (empty on the demo). Each item: `id`, `productId`, `productName`, `productSku`, `productImage`, `supplierName`, `oldPriceGross`, `proposedPriceGross`, `reason`, `source`, `status` (`"pending"`, `"approved"`, `"rejected"`, `"rolled_back"`, `"stale"`), `batchId`, `createdAt`, `decidedAt`.

```json
[]
```

---

### GET /api/peers/invites

Returns the shop's outbound peer invite codes. The public demo returns an empty array.

**Public** **Read-only**

**Parameters**

None.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/peers/invites
```

**Response (200)**

Returns an array of invite objects (empty on the demo). Each item: `id`, `code`, `createdBy`, `supplierId`, `supplierName`, `expiresAt`, `acceptedBy`, `acceptedAt`, `revokedAt`, `createdAt`.

```json
[]
```

---

### GET /api/peers/links

Returns established peer links between shops. The public demo returns an empty array.

**Public** **Read-only**

**Parameters**

None.

**Request**

```bash
curl --fail-with-body https://haszno.com/api/peers/links
```

**Response (200)**

Returns an array of link objects (empty on the demo). Each item: `id`, `supplierId`, `partner`, `createdAt`, `supplierName`, `supplierCategory`.

```json
[]
```

---

### GET /api/import-list

Returns the import list — products staged from the partner catalogue before they are pushed to a connected webshop.

**Public** **Read-only**

**Parameters**

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `status` | query | string | No | (all) | Filter by status: `varakozik` (waiting), `feltoltve` (pushed to shop), `elvetve` (discarded). |
| `q` | query | string | No | — | Case-insensitive substring search on the product name (a real instance also matches the category). |

**Request**

```bash
curl --fail-with-body 'https://haszno.com/api/import-list?status=varakozik'
```

**Response (200)**

Returns an array of import list items.

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Import list entry ID. |
| `addedAt` | ISO-8601 string | When the item was added to the list. |
| `status` | string | `"varakozik"`, `"feltoltve"`, or `"elvetve"`. |
| `note` | string \| null | Optional note added during import. |
| `pushedProductId` | integer \| null | ID of the shop product created when this item was pushed. |
| `networkProduct` | object | The source catalogue product — see sub-fields below. |

**`networkProduct` sub-fields**

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Catalogue product ID. |
| `name` | string | Product name. |
| `imageUrl` | string \| null | Product image URL. |
| `sku` | string | Supplier SKU. |
| `wholesaleNet` | integer (HUF) \| null | Net wholesale cost. |
| `rrpGross` | integer (HUF) \| null | Supplier's recommended retail price. |
| `stock` | integer | Available stock. |
| `category` | string | Category display name (Hungarian label, e.g. `"Kert és növény"`). |
| `supplierName` | string | Supplier display name. |
| `example` | boolean | `true` for demo/example items. |
| `marginNet` | integer (HUF) \| null | Net margin at the RRP: `round(rrpGross / (1 + vatPct / 100) − wholesaleNet)`. `null` when either price is unknown. |
| `marginPct` | number \| null | Margin percentage at the RRP. On the public demo this is the same fixed placeholder (`35`) as `current.marginPct` in `GET /api/products`. |

```json
[
  {
    "id": 1,
    "addedAt": "2026-11-10T08:00:00.000Z",
    "status": "varakozik",
    "note": null,
    "pushedProductId": null,
    "networkProduct": {
      "id": 1,
      "name": "Autós telefontartó · 1. változat",
      "imageUrl": null,
      "sku": "DEMO-1",
      "wholesaleNet": 1019,
      "rrpGross": 1990,
      "stock": 0,
      "category": "Autó, gumi és motor",
      "supplierName": "Példa-partner · Autó, gumi és motor",
      "example": true,
      "marginNet": 548,
      "marginPct": 35
    }
  }
]
```

---

## Blog and market insights

The blog endpoints return **real data** — published market insight posts generated from aggregated (anonymised, ≥ 5 shops) trend data. No `"demo": true` flag is present in these responses.

---

### GET /api/blog

Returns all published market insight posts, most recent first.

**Public** **Read-only**

**Parameters**

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `page` | query | integer ≥ 1 | No | `1` | Page number. |
| `period` | query | `"het"` \| `"honap"` | No | (all) | Filter to weekly (`"het"`) or monthly (`"honap"`) posts only. |

**Request**

```bash
curl --fail-with-body https://haszno.com/api/blog
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `posts` | array | Published post summaries — see item fields below. |
| `total` | integer | Total number of published posts matching the filter. |
| `page` | integer | Current page number. |
| `perPage` | integer | Fixed page size (always `10`). |

**Post item fields**

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Post ID. |
| `slug` | string | URL slug used in `GET /api/blog/:slug`. |
| `period` | `"het"` \| `"honap"` | Aggregation period type: `"het"` = weekly, `"honap"` = monthly. |
| `periodStart` | ISO-8601 date string | Start date of the aggregation window (e.g. `"2026-11-02"`). |
| `periodEnd` | ISO-8601 date string | End date of the aggregation window (e.g. `"2026-11-08"`). |
| `title` | string | Post title (Hungarian). |
| `summary` | string | Short summary paragraph. |
| `publishedAt` | ISO-8601 string | Publication timestamp. |
| `coverImageUrl` | string \| null | Cover image URL, or `null`. |

```json
{
  "posts": [
    {
      "id": 2,
      "slug": "2026-het45-toplista",
      "period": "het",
      "periodStart": "2026-11-02",
      "periodEnd": "2026-11-08",
      "title": "2026/45. hét — Legjobban fogyó termékek",
      "summary": "A 45. héten párásítók és LED-es világítástechnikai termékek forgalma emelkedett...",
      "publishedAt": "2026-11-09T08:00:00.000Z",
      "coverImageUrl": null
    }
  ],
  "total": 3,
  "page": 1,
  "perPage": 10
}
```

---

### GET /api/blog/:slug

Returns a single published post by its slug.

**Public** **Read-only**

**Path parameters**

| Name | Type | Description |
|------|------|-------------|
| `slug` | string | The post slug from the `slug` field in `GET /api/blog`. |

**Request**

```bash
curl --fail-with-body https://haszno.com/api/blog/2026-het45-toplista
```

**Response (200)**

Returns a single post object. It has no `coverImageUrl`, unlike the list items.

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Post ID. |
| `slug` | string | URL slug. |
| `period` | `"het"` \| `"honap"` | Aggregation period type: `"het"` = weekly, `"honap"` = monthly. |
| `periodStart` | ISO-8601 date string | Start date of the aggregation window. |
| `periodEnd` | ISO-8601 date string | End date of the aggregation window. |
| `title` | string | Post title (Hungarian). |
| `summary` | string | Short summary paragraph. |
| `bodyMd` | string | Full post content in Markdown. |
| `data` | object | Aggregated figures behind the post: `shopsCount`, `rankedProducts` and `rankedCategories`. Each ranked item has `name`, `units`, `shopsCount` and an optional `trend` (`"emelkedo"`, `"stabil"` or `"csokkeno"`); ranked products may also have `imageUrl`. |
| `ogTitle` | string \| null | Open Graph title; `null` means the `title` is used. |
| `publishedAt` | ISO-8601 string \| null | Publication timestamp. |
| `status` | string | Always `"publikalt"` (published) on this endpoint. |
| `createdAt` | ISO-8601 string | When the post was created. |
| `updatedAt` | ISO-8601 string | When the post was last updated. |

Append `.md` to the slug (`/api/blog/2026-het45-toplista.md`) to get only `bodyMd`, as `text/markdown; charset=utf-8`.

**Error responses**

| Status | When |
|--------|------|
| 404 | Slug not found or post is not published. |

---

### GET /api/blog/rss.xml

Returns an RSS 2.0 feed of published posts. The response content type is `application/rss+xml; charset=utf-8`.

**Public** **Read-only**

**Request**

```bash
curl --fail-with-body https://haszno.com/api/blog/rss.xml
```

---

### GET /api/blog/feed.json

Returns a JSON Feed (version 1.1) of published posts. The response content type is `application/feed+json; charset=utf-8`.

**Public** **Read-only**

**Request**

```bash
curl --fail-with-body https://haszno.com/api/blog/feed.json
```

---

## Utility endpoints

### GET /llms.txt

Returns the LLM-readable site summary following the [llms.txt](https://llmstxt.org/) convention. Content type: `text/plain`.

**Public** **Read-only**

**Request**

```bash
curl --fail-with-body https://haszno.com/llms.txt
```
