# Protected endpoints

> API endpoints that require credentials — a session cookie for the account endpoints, and the HTTP Basic password arranged with the Haszno team for everything else on haszno.com.

> [!NOTE]
> The Haszno API is in developer preview. On the hosted demo, every endpoint on this page except the [account and session endpoints](#auth-account) requires the HTTP Basic password arranged with the Haszno team. See [Authentication](authentication.md) for the current credential model.

## Overview

On the hosted demo at `https://haszno.com`, the endpoints in the [endpoint groups](#endpoint-groups) need an HTTP Basic auth header (`demo:<password>`). The [account and session endpoints](#auth-account) are the exception: they are not behind the Basic wall and use a session cookie instead. On a self-hosted instance with `HASZNO_API_KEY` set, send that key in the `x-api-key` header instead of Basic auth; without it, the routes are open.

Obtain the Basic password by contacting the Haszno team. The examples on this page use the placeholder `$HASZNO_BASIC` (Basic auth credentials in `user:pass` form), sourced from your environment. Never hardcode credentials in code.

```bash
# Set once in your shell:
export HASZNO_BASIC="demo:your-password-here"
```

> [!WARNING]
> **Write endpoints have real side effects.** Calls that create or modify data — pricing changes, order submissions, feed imports, rule executions — cannot always be undone. Always verify intent before calling a write endpoint. The automation kill switch (`GET /api/automation`, `PUT /api/automation`) must be in a known state before running bulk operations.

---

## Account and session endpoints

These endpoints do **not** need the HTTP Basic password, not even on `haszno.com`. They work with a session cookie (`haszno_session`, valid for 30 days), which `register` and `login` set.

| Method | Path | Purpose | What it needs | Mode |
|--------|------|---------|---------------|------|
| POST | `/api/auth/register` | Create an account, its shop, and a session cookie | Nothing. JSON body `{email, password, name, shopName}`; the password needs at least 10 characters. Returns `201`. | **Write** |
| POST | `/api/auth/login` | Log in and receive a session cookie | Nothing. JSON body `{email, password}`. | **Write** |
| POST | `/api/auth/logout` | Destroy the current session | Nothing. Clears the session cookie if one is sent; always returns `{"ok": true}`. | **Write** |
| GET | `/api/auth/me` | Current user and shop | A session cookie or a Bearer token; `401` without either. | **Read-only** |
| POST | `/api/auth/tokens` | Create an API Bearer token | A session cookie. JSON body `{name, scope}`; `scope` is `read` (default) or `write`. Returns `201`. | **Write** |
| GET | `/api/auth/tokens` | List active Bearer tokens | A session cookie. | **Read-only** |
| DELETE | `/api/auth/tokens/:id` | Revoke a Bearer token | A session cookie. | **Write** |

- A session cookie unlocks only these endpoints. It does not open the routes in the [endpoint groups](#endpoint-groups) on `haszno.com`.
- The token endpoints accept only the session cookie, so a Bearer token cannot create, list or revoke tokens.
- With a session cookie, `POST` and `DELETE` on `/api/auth/tokens` must come from the site's own origin (CSRF check, `403` otherwise). `register`, `login` and `logout` are exempt.
- On a self-hosted instance with `HASZNO_API_KEY` set, `register`, `login`, `logout` and `me` skip the key check, but `/api/auth/tokens` also needs `x-api-key`.
- Rate limits for `register` and `login` are listed in [Errors](errors.md#auth-limits).

---

## Endpoint groups

On `haszno.com`, every endpoint in the groups below needs the HTTP Basic password.

### Shop settings

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| PUT | `/api/settings` | Update pricing rules and shipping markets | **Write** |

### Products — own shop

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| PATCH | `/api/products/:id` | Update market price or set a manual price override | **Write** |
| POST | `/api/products/:id/listing` | List or unlist a product | **Write** |
| POST | `/api/products/:id/ai-description` | Generate a Hungarian product description via AI | **Write** |
| POST | `/api/products/apply-prices` | Apply the recommended price to all or selected products | **Write** |
| GET | `/api/products/:id/offers` | Last 24 h market offers for a product | **Read-only** |
| GET | `/api/products/:id/price-history` | Price change log for a product | **Read-only** |

### Supplier network and catalogue

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/network/suppliers` | Network suppliers with connection status | **Read-only** |
| GET | `/api/network/suppliers/:id/products` | Full catalogue for a network supplier | **Read-only** |
| POST | `/api/network/suppliers/:id/connect` | Connect and import a supplier's catalogue | **Write** |
| POST | `/api/network/suppliers/:id/disconnect` | Disconnect a supplier | **Write** |
| POST | `/api/network/suppliers/:id/sync` | Refresh cost and stock from live catalogue | **Write** |
| GET | `/api/network/supplier-view/:supplierId` | Aggregated reseller stats per product (k-anon ≥ 5 shops) | **Read-only** |
| POST | `/api/network/products/:id/import` | Add one network product to the own shop | **Write** |
| GET | `/api/catalogue/products` | Browse the partner catalogue (paginated, search, facets) | **Read-only** |
| POST | `/api/import-list` | Add network products to the import list | **Write** |
| POST | `/api/import-list/push` | Push import list items to a connected webshop | **Write** |
| POST | `/api/import-list/discard` | Discard import list items | **Write** |

### Recommendations and ideas

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/recommendations` | Trending and margin-safe product shortlist | **Read-only** |
| POST | `/api/recommendations/ai` | AI-curated shortlist via Claude (cached 24 h) | **Read-only** |

### Orders

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| POST | `/api/webhooks/order` | Receive a new order from a connected shop | **Write** |
| POST | `/api/demo/order` | Create a random demo order | **Write** |
| POST | `/api/orders/:id/ship` | Mark an order as shipped | **Write** |

### Feeds

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| POST | `/api/feeds/import` | Import a supplier product feed (CSV or XML, up to 10 MB) | **Write** |
| POST | `/api/market-prices/import` | Import market comparison prices from CSV | **Write** |
| GET | `/feed/arukereso.xml` | Árukereső XML outbound product feed | **Read-only** |
| GET | `/feed/google.xml` | Google Merchant XML outbound product feed | **Read-only** |

### Rules and automation

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/rules/templates` | Built-in rule templates | **Read-only** |
| GET | `/api/rules` | List configured rules | **Read-only** |
| POST | `/api/rules` | Create a rule | **Write** |
| PATCH | `/api/rules/:id` | Update a rule | **Write** |
| DELETE | `/api/rules/:id` | Delete a rule permanently | **Write** |
| POST | `/api/rules/:id/run` | Manually run one rule | **Write** |
| POST | `/api/rules/run-all` | Run all manual-trigger rules | **Write** |
| POST | `/api/rules/trigger` | Fire a named event trigger | **Write** |
| GET | `/api/rule-runs` | Rule execution log | **Read-only** |
| POST | `/api/rule-runs/undo` | Undo the last rule run | **Write** |
| GET | `/api/automation` | Automation kill switch state | **Read-only** |
| PUT | `/api/automation` | Set automation paused state | **Write** |

### Price proposals

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| POST | `/api/proposals/approve` | Approve proposals (all pending, or by ID list) | **Write** |
| POST | `/api/proposals/reject` | Reject proposals | **Write** |
| POST | `/api/proposals/rollback` | Roll back an approved proposal batch | **Write** |

### Peers and sharing

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| POST | `/api/peers/invites` | Create a peer invite to share a supplier | **Write** |
| GET | `/api/peers/invites/:code` | Get an invite by code | **Read-only** |
| DELETE | `/api/peers/invites/:code` | Delete an invite | **Write** |
| POST | `/api/peers/invites/:code/accept` | Accept a peer invite | **Write** |
| DELETE | `/api/peers/links/:id` | Remove a peer link | **Write** |
| PUT | `/api/peers/suppliers/:id/visibility` | Set supplier visibility (private / network) | **Write** |

### Shop connections and marketplace

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/shop-connections` | List shop connections | **Read-only** |
| POST | `/api/shop-connections` | Create a shop connection | **Write** |
| GET | `/api/shop-connections/:id` | Get a connection | **Read-only** |
| DELETE | `/api/shop-connections/:id` | Delete a connection | **Write** |
| POST | `/api/shop-connections/:id/test` | Test a connection | **Read-only** |
| POST | `/api/shop-connections/:id/sync` | Sync products to a connected shop | **Write** |
| GET | `/api/shop-connections/:id/runs` | Sync run log | **Read-only** |
| GET | `/api/shop-connections/:id/pricing` | Per-shop pricing rules | **Read-only** |
| PUT | `/api/shop-connections/:id/pricing` | Update per-shop pricing rules | **Write** |
| POST | `/api/shop-connections/:id/pricing/reprice` | Reprice all products for a shop | **Write** |
| GET | `/api/marketplace/connections` | List marketplace connections | **Read-only** |
| POST | `/api/marketplace/connections` | Create a marketplace connection | **Write** |
| GET | `/api/marketplace/connections/:id` | Get a marketplace connection | **Read-only** |
| DELETE | `/api/marketplace/connections/:id` | Delete a marketplace connection | **Write** |
| PUT | `/api/marketplace/connections/:id/credentials` | Update credentials | **Write** |
| POST | `/api/marketplace/connections/:id/test` | Test connection | **Read-only** |
| GET | `/api/marketplace/connections/:id/listings` | Listings for a connection | **Read-only** |
| POST | `/api/marketplace/listings` | Create a marketplace listing | **Write** |
| GET | `/api/marketplace/listings/:id/preview` | Preview a listing | **Read-only** |
| POST | `/api/marketplace/listings/:id/publish` | Publish a listing | **Write** |
| POST | `/api/marketplace/listings/:id/update` | Update a live listing | **Write** |
| POST | `/api/marketplace/listings/:id/unpublish` | Unpublish a listing | **Write** |
| GET | `/api/marketplace/listings/:id/price-history` | Listing price history | **Read-only** |
| POST | `/api/marketplace/connections/:id/ingest-orders` | Pull orders from marketplace | **Write** |

### Routing

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/routing/sources` | All routing sources | **Read-only** |
| GET | `/api/routing/sources/:productId` | Routing sources for a product | **Read-only** |
| PUT | `/api/routing/sources/:productId` | Set routing sources | **Write** |
| DELETE | `/api/routing/sources/:productId/:supplierId` | Remove a routing source | **Write** |
| GET | `/api/routing/decisions` | Recent routing decisions log | **Read-only** |

### Fulfilment

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/fulfilment/settings` | Fulfilment provider settings | **Read-only** |
| GET | `/api/fulfilment/invoices` | Invoice list | **Read-only** |
| GET | `/api/fulfilment/shipments` | Shipment list | **Read-only** |
| GET | `/api/fulfilment/orders/:orderId/label/download` | Download shipping label PDF | **Read-only** |
| GET | `/api/fulfilment/orders/:orderId/tracking` | Get tracking status | **Read-only** |
| GET | `/api/fulfilment/settings/notifications` | Notification settings | **Read-only** |
| PUT | `/api/fulfilment/settings/notifications` | Update notification settings | **Write** |

### Insights and reports

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/insights/network-signals` | Aggregated keyword signals (k-anon ≥ 5 shops) | **Read-only** |
| GET | `/api/insights/network-signal` | Single keyword signal | **Read-only** |
| POST | `/api/trends/keywords` | Fill missing trend keywords via AI | **Write** |
| GET | `/api/reports` | List reports | **Read-only** |
| GET | `/api/reports/preview` | Preview current report | **Read-only** |
| GET | `/api/reports/:id` | Get a report as JSON | **Read-only** |
| GET | `/api/reports/:id/html` | Get a report as HTML | **Read-only** |

### Partners and metrics

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/partners/metrics` | Per-supplier performance metrics | **Read-only** |
| GET | `/api/partners/metrics/:supplierId/history` | Metric history for a supplier | **Read-only** |
| POST | `/api/partners/metrics/compute` | Recompute metrics | **Write** |

### Billing

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/billing/summary` | Current billing plan and usage | **Read-only** |
| GET | `/api/billing/history` | Payment history | **Read-only** |
| POST | `/api/billing/plan` | Change billing plan (rate-limited: 5 / min) | **Write** |

### Group buys

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/groupbuys` | List group buys | **Read-only** |
| GET | `/api/groupbuys/:id` | Get a group buy | **Read-only** |
| POST | `/api/groupbuys` | Create a group buy | **Write** |
| POST | `/api/groupbuys/:id/commit` | Commit to a group buy | **Write** |
| POST | `/api/groupbuys/:id/close` | Close a group buy | **Write** |
| POST | `/api/groupbuys/:id/cancel` | Cancel a group buy | **Write** |

### Store connect API, search, and analysis

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| POST | `/api/connect/stores` | Register a store | **Write** |
| GET | `/api/connect/stores` | List stores | **Read-only** |
| GET | `/api/connect/stores/:id` | Get a store | **Read-only** |
| PUT | `/api/connect/stores/:id/domain` | Set the store domain | **Write** |
| POST | `/api/connect/stores/:id/verify-domain` | Verify domain ownership | **Write** |
| PUT | `/api/connect/stores/:id/credentials` | Update credentials | **Write** |
| POST | `/api/connect/stores/:id/test` | Test a store connection | **Read-only** |
| POST | `/api/connect/stores/:id/push` | Push product updates to a store | **Write** |
| GET | `/api/search/suggest` | Autocomplete suggestion for catalogue search | **Read-only** |
| POST | `/api/analysis` | Start a competitor or site analysis job | **Write** |
| GET | `/api/analysis/:id` | Get analysis job result | **Read-only** |

### Shop launcher and onboarding

| Method | Path | Purpose | Mode |
|--------|------|---------|------|
| GET | `/api/launcher/jobs` | List shop build jobs | **Read-only** |
| POST | `/api/launcher/jobs` | Create a shop build job | **Write** |
| GET | `/api/launcher/jobs/:id` | Get job status | **Read-only** |
| POST | `/api/launcher/jobs/:id/generate` | Generate shop content | **Write** |
| GET | `/api/launcher/jobs/:id/plan` | Get build plan | **Read-only** |
| GET | `/api/launcher/jobs/:id/download` | Download build artifact | **Read-only** |
| GET | `/api/launcher/themes` | Available themes | **Read-only** |
| GET | `/api/onboarding` | Current onboarding state | **Read-only** |
| PUT | `/api/onboarding` | Update onboarding step | **Write** |
| POST | `/api/onboarding/complete` | Mark onboarding complete | **Write** |

---

## Full reference — 10 most useful endpoints

### GET /api/recommendations

Returns a shortlist of products from the partner catalogue ranked by trend momentum, margin potential, and market availability. Use this to surface products worth adding to the shop.

**Protected** **Read-only**

**Parameters**

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `cats` | query | comma-separated string | No | (all) | Filter by category keys, e.g. `kert,sport`. |

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  'https://haszno.com/api/recommendations?cats=kert'
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `anchor` | string | Reference keyword used for all trend comparisons. |
| `geo` | string | Always `"HU"`. |
| `items` | array | Recommended products — see sub-fields below. |

**`items` sub-fields** (selected)

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Product ID. |
| `name` | string | Product name (Hungarian). |
| `sku` | string | Supplier SKU. |
| `supplierName` | string | Supplier display name. |
| `wholesaleNet` | integer (HUF) | Net wholesale cost. |
| `rrpGross` | integer \| null | Supplier recommended retail price. |
| `stock` | integer | Available stock. |
| `marginNet` | integer (HUF) | Net margin at the recommended price. |
| `marginPct` | number | Margin percentage at the recommended price. |
| `recommendedPriceGross` | integer (HUF) | Suggested gross selling price. |
| `keyword` | string | Primary trend keyword. |
| `interest4w` | number | Trend interest over the last 4 weeks (0–100). |
| `interest12m` | number | Trend interest over the last 12 months (0–100). |
| `trend` | string | Trend direction: `"up"`, `"down"`, or `"stable"`. |
| `momentum` | number | Composite momentum score. |
| `score100` | number | Combined rank score out of 100. |

```json
{
  "anchor": "kerti napernyő",
  "geo": "HU",
  "items": [
    {
      "id": 212,
      "name": "Kerti napernyő · Prémium",
      "sku": "DEMO-212",
      "supplierName": "Példa-partner · Kert és növény",
      "wholesaleNet": 3200,
      "rrpGross": 7990,
      "stock": 15,
      "marginNet": 2080,
      "marginPct": 32.8,
      "recommendedPriceGross": 7490,
      "keyword": "kerti napernyő",
      "interest4w": 72,
      "interest12m": 61,
      "trend": "up",
      "momentum": 1.18,
      "score100": 84
    }
  ]
}
```

---

### GET /api/catalogue/products

Searches the shared partner product catalogue. Supports free-text search, category and supplier filters, margin and price bands, and pagination. This is the primary discovery endpoint for adding new products to the shop.

**Protected** **Read-only**

**Parameters**

| Name | In | Type | Required | Default | Description |
|------|----|------|----------|---------|-------------|
| `q` | query | string | No | — | Free-text search on product name or category. |
| `cats` | query | comma-separated string | No | (all) | Category keys to include. |
| `partner` | query | integer | No | (all) | Supplier ID filter. |
| `minMarginPct` | query | number | No | — | Minimum margin percentage at the RRP. |
| `maxPrice` | query | integer (HUF) | No | — | Maximum `rrpGross`. |
| `minPrice` | query | integer (HUF) | No | — | Minimum `rrpGross`. |
| `inStock` | query | `"1"` | No | — | When set, returns only in-stock products. |
| `hasImage` | query | `"1"` | No | — | When set, returns only products with an image. |
| `country` | query | string (2-char) | No | — | Supplier country code. |
| `sort` | query | string | No | `"name_asc"` | Sort order: `"margin_desc"`, `"price_asc"`, `"price_desc"`, `"name_asc"`. |
| `page` | query | integer ≥ 1 | No | `1` | Page number. |
| `pageSize` | query | integer 1–100 | No | `24` | Items per page. |

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  'https://haszno.com/api/catalogue/products?q=napernyő&inStock=1&sort=margin_desc&pageSize=10'
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `items` | array | Products on the current page. |
| `total` | integer | Total matching products across all pages. |
| `facets` | object | Aggregated filter counts (categories, suppliers, countries). |

Each item in `items`: `id`, `name`, `sku`, `supplierName`, `supplierId`, `category`, `wholesaleNet` (HUF), `rrpGross` (HUF), `stock`, `imageUrl`, `country`, `marginNet` (HUF), `marginPct`.

```json
{
  "items": [
    {
      "id": 212,
      "name": "Kerti napernyő · Prémium",
      "sku": "SUP-212",
      "supplierName": "Minta Kft.",
      "supplierId": 7,
      "category": "kert",
      "wholesaleNet": 3200,
      "rrpGross": 7990,
      "stock": 15,
      "imageUrl": null,
      "country": "HU",
      "marginNet": 2080,
      "marginPct": 32.8
    }
  ],
  "total": 142,
  "facets": {
    "categories": [{"key": "kert", "count": 142}],
    "suppliers": [{"id": 7, "name": "Minta Kft.", "count": 142}]
  }
}
```

---

### POST /api/feeds/import

Imports a supplier product feed. Accepts CSV or XML format up to 10 MB. Creates new products and updates existing ones matched by SKU.

**Protected** **Write**

> [!WARNING]
> This endpoint performs a bulk write. Import replaces cost and stock data for matched SKUs. Verify the feed content before importing to avoid overwriting prices with incorrect wholesale costs.

**Request body** (`Content-Type: application/json`)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `supplierId` | integer | Yes | ID of the supplier this feed belongs to. |
| `format` | string | Yes | `"csv"` or `"xml"`. |
| `content` | string | Yes | Full feed content as a string (1 byte – 10 MB). |

CSV format: one header row followed by data rows. Required columns: `sku`, `name`, `costNet`, `stock`. Optional: `rrpGross`.

```csv
sku,name,costNet,stock,rrpGross
ABC-001,Kerti szék,2100,10,4990
ABC-002,Kerti asztal,8500,3,19990
```

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  -X POST https://haszno.com/api/feeds/import \
  -H 'Content-Type: application/json' \
  -d '{
    "supplierId": 3,
    "format": "csv",
    "content": "sku,name,costNet,stock,rrpGross\nABC-001,Kerti szék,2100,10,4990"
  }'
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `created` | integer | Number of new products created. |
| `updated` | integer | Number of existing products updated. |
| `total` | integer | Total rows processed. |

```json
{"created": 1, "updated": 0, "total": 1}
```

**Error responses**

| Status | When |
|--------|------|
| 400 | Invalid format, missing required columns, or malformed feed content. |
| 413 | Feed content exceeds 10 MB. |
| 429 | Rate limit exceeded (20 requests per minute). |

---

### POST /api/webhooks/order

Receives a new order from a connected webshop. The API validates the SKUs, checks stock, and creates an order record for processing.

**Protected** **Write**

> [!WARNING]
> This endpoint creates an order that immediately reserves stock. It is not a dry run. If you receive a 409 (out of stock), you must handle the error on the shop side — the order is not created.

**Request body** (`Content-Type: application/json`)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `externalId` | string (1–64) | Yes | Unique order ID from the webshop. Must be unique across all orders for this shop. |
| `customerName` | string (1–120) | Yes | Customer full name. |
| `customerEmail` | string | No | Customer email address. |
| `address` | string (1–300) | Yes | Delivery address as a single string. |
| `items` | array | Yes | Order line items — see below. |

**`items` array item**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `sku` | string | Yes | Product SKU. Must match an existing product in the shop. |
| `qty` | integer > 0 | Yes | Quantity ordered. |

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  -X POST https://haszno.com/api/webhooks/order \
  -H 'Content-Type: application/json' \
  -d '{
    "externalId": "SHOP-20261112-001",
    "customerName": "Kiss Éva",
    "address": "1111 Budapest, Példa utca 1.",
    "items": [
      {"sku": "ABC-001", "qty": 2}
    ]
  }'
```

**Response (201)**

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Internal order ID assigned by Haszno. |

```json
{"id": 1047}
```

**Error responses**

| Status | Message | When |
|--------|---------|------|
| 400 | `"Ismeretlen cikkszám: ABC-999"` | SKU not found in the shop. |
| 409 | `"Elfogyott: ABC-001"` | SKU exists but has zero stock. |

> [!NOTE]
> Error messages are returned in Hungarian. See [Errors and rate limits](errors.md) for the full error schema.

---

### PUT /api/settings

Updates the shop's global pricing rules. All fields are optional; omit a field to leave its current value unchanged.

**Protected** **Write**

> [!WARNING]
> Changing `shippingMarkets` immediately triggers a recomputation of `marketMinGross` for every product. This may take several seconds on large catalogues. Pricing recommendations update accordingly.

**Request body** (`Content-Type: application/json`)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `undercutPct` | number 0–50 | No | Percentage to undercut the market minimum when computing the recommended price. |
| `minMarginPct` | number 0–90 | No | Minimum net margin percentage enforced by the floor price calculation. |
| `vatPct` | number 0–50 | No | VAT rate used for all gross/net conversions. |
| `shippingMarkets` | string[] | No | ISO country codes to monitor for market prices. At least one value required if present. Valid codes: `HU`, `SK`, `DE`, `AT`, `CZ`, `PL`, `RO`. |

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  -X PUT https://haszno.com/api/settings \
  -H 'Content-Type: application/json' \
  -d '{
    "undercutPct": 2,
    "minMarginPct": 15,
    "shippingMarkets": ["HU", "SK"]
  }'
```

**Response (200)**

Returns the updated settings row. Same schema as `GET /api/settings` (without the `demo` field on a real instance).

```json
{
  "id": 1,
  "undercutPct": 2,
  "minMarginPct": 15,
  "vatPct": 27,
  "shippingMarkets": ["HU", "SK"],
  "automationPaused": false
}
```

---

### PATCH /api/products/:id

Updates market price data or sets a manual gross price override for a single product.

**Protected** **Write**

**Path parameters**

| Name | Type | Description |
|------|------|-------------|
| `id` | integer | Product ID. |

**Request body** (`Content-Type: application/json`)

All fields are optional. Send only the fields you want to change.

| Field | Type | Description |
|-------|------|-------------|
| `marketMinGross` | integer > 0 \| null | Manually set the cheapest known market price (gross, HUF). Set to `null` to clear. |
| `marketShop` | string (max 120) \| null | Shop name for the manually set market price. Set to `null` to clear. |
| `priceGross` | integer > 0 | Manual gross price override. Must be a positive integer. |

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  -X PATCH https://haszno.com/api/products/101 \
  -H 'Content-Type: application/json' \
  -d '{"priceGross": 2990}'
```

**Response (200)**

Returns the updated product row. Same schema as a single item from `GET /api/products`.

---

### POST /api/products/apply-prices

Applies the recommended price to listed products. By default applies to all listed products. Optionally restricted to a subset by IDs or supplier.

**Protected** **Write**

> [!WARNING]
> This is a bulk write. It updates `priceGross` for every matched product to its current `recommended.priceGross`. Run `GET /api/products` first to review the recommendations before applying.

**Request body** (`Content-Type: application/json`)

All fields are optional.

| Field | Type | Description |
|-------|------|-------------|
| `ids` | integer[] | Product IDs to apply prices to. If omitted, applies to all listed products. Send `[]` for a no-op. |
| `supplier` | integer[] | Restrict to products from these supplier IDs. |

**Request**

```bash
# Apply to all listed products:
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  -X POST https://haszno.com/api/products/apply-prices \
  -H 'Content-Type: application/json' \
  -d '{}'

# Apply to specific products only:
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  -X POST https://haszno.com/api/products/apply-prices \
  -H 'Content-Type: application/json' \
  -d '{"ids": [101, 102, 103]}'
```

**Response (200)**

| Field | Type | Description |
|-------|------|-------------|
| `changed` | integer | Number of products whose price was updated. |
| `total` | integer | Number of products evaluated. |

```json
{"changed": 847, "total": 1400}
```

---

### GET /api/rules

Returns all automation rules configured for the shop.

**Protected** **Read-only**

**Parameters**

None.

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  https://haszno.com/api/rules
```

**Response (200)**

Returns an array of rule objects.

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Rule ID. |
| `name` | string | Display name. |
| `enabled` | boolean | Whether the rule is active. |
| `trigger` | string | When the rule runs — see enum below. |
| `conditions` | array | Conditions that must all match — see sub-fields below. |
| `actions` | array | Actions taken when conditions match — see sub-fields below. |
| `template` | string \| null | Built-in template key, if the rule was created from a template. |
| `createdAt` | ISO-8601 string | Creation timestamp. |

**`trigger` enum**

| Value | Meaning |
|-------|---------|
| `"feed_import"` | Runs after each feed import. |
| `"market_price"` | Runs after market prices are refreshed. |
| `"manual"` | Runs only when triggered manually or via `POST /api/rules/:id/run`. |

**`conditions` item sub-fields**

| Field | Type | Description |
|-------|------|-------------|
| `field` | string | Product field to evaluate, e.g. `"current.marginPct"`. |
| `op` | string | Comparison operator: `"lt"`, `"lte"`, `"gt"`, `"gte"`, `"eq"`, `"neq"`, `"changed"`. |
| `value` | number \| string | Threshold value. |

**`actions` item sub-fields**

| Field | Type | Description |
|-------|------|-------------|
| `type` | string | Action type: `"unlist"`, `"propose_price"`, `"notify"`. |

```json
[
  {
    "id": 5,
    "name": "Veszteséges termékek javaslata",
    "enabled": true,
    "trigger": "market_price",
    "conditions": [
      {"field": "current.marginPct", "op": "lt", "value": 0}
    ],
    "actions": [
      {"type": "propose_price"}
    ],
    "template": "loss-guard",
    "createdAt": "2026-10-01T10:00:00.000Z"
  }
]
```

---

### GET /api/network/suppliers

Returns all suppliers available in the Haszno network, together with the shop's connection status for each.

**Protected** **Read-only**

**Parameters**

None.

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  https://haszno.com/api/network/suppliers
```

**Response (200)**

Returns an array of network supplier objects.

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Supplier ID. |
| `name` | string | Supplier display name. |
| `category` | string | Primary category key. |
| `country` | string | ISO country code. |
| `productCount` | integer | Total products offered. |
| `connected` | boolean | Whether the shop is already connected to this supplier. |
| `avgResellerMarginPct` | number \| null | Aggregated average reseller margin across all connected shops (k-anon ≥ 5 shops; `null` when fewer than 5 shops have connected). |

```json
[
  {
    "id": 7,
    "name": "Minta Kft.",
    "category": "kert",
    "country": "HU",
    "productCount": 340,
    "connected": false,
    "avgResellerMarginPct": 28.4
  }
]
```

> [!NOTE]
> `avgResellerMarginPct` is an aggregated figure derived from at least 5 shops. Individual shop margins are never exposed through this endpoint.

---

### POST /api/products/:id/listing

Lists or unlists a single product. When listing a product that was previously unlisted, `priceGross` is automatically set to the current `recommended.priceGross`.

**Protected** **Write**

**Path parameters**

| Name | Type | Description |
|------|------|-------------|
| `id` | integer | Product ID. |

**Request body** (`Content-Type: application/json`)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `listed` | boolean | Yes | `true` to list; `false` to unlist. |

**Request**

```bash
curl --fail-with-body \
  -u "$HASZNO_BASIC" \
  -X POST https://haszno.com/api/products/101/listing \
  -H 'Content-Type: application/json' \
  -d '{"listed": true}'
```

**Response (200)**

Returns the updated product row. Same schema as a single item from `GET /api/products`. The `listed` field reflects the new state and `priceGross` reflects the auto-applied recommended price when listing.

```json
{
  "id": 101,
  "listed": true,
  "priceGross": 2090,
  "recommended": {
    "priceGross": 2090,
    "status": "versenykepes"
  }
}
```
