{"openapi":"3.1.0","info":{"title":"B2B Spare Parts Portal — Integration API","description":"## General rules\n\n- **Authentication**: via the `Authorization: Bearer <api_key>` header.\n  API keys are created in the admin panel and carry `import` / `export` scopes.\n- **Content type**: `application/json`. All field names are `snake_case`.\n- **Dates**: import payload dates are date-only `YYYY-MM-DD` (no timezone, no\n  conversion). Response timestamps (order export: `created_at`, `exported_at`,\n  `shipped_at`) are RFC 3339 **UTC** (`2025-01-15T10:30:00Z`). The order export\n  `since` parameter accepts either format; a date-only value means the start of\n  that day in the site display timezone.\n- **Site localization settings do not affect this API**: the contract language is\n  English, dates follow the rules above and amounts are always base-currency\n  equivalents. `errors[].code` is a stable machine code; `errors[].message` is\n  always English.\n- **Money amounts**: sent as strings (`1500.50`), up to 4 decimal places.\n- **Batch size**: at most 1000 records per request. More → `413 Payload Too Large`.\n- **Partial success**: `errors` may be non-empty even with `200 OK`. Always check it.\n- **Idempotent**: re-sending a record with the same `erp_ref` updates it — no duplicates.\n- **Rate limit**: 60 requests/minute by default. Exceeding → `429 Too Many Requests`.\n- **All amounts are base currency**: invoice and account-transaction amounts must be\n  sent as equivalents of the **base currency** selected in admin Settings. The system\n  does not convert currencies.\n\n## Import order\n\n| Step | Endpoint | Requires |\n|------|----------|------------|\n| 1 | `POST /brands` | — |\n| 2 | `POST /currencies` | — |\n| 3 | `POST /exchange-rates` | currencies |\n| 3 | `POST /customers` | — |\n| 3 | `POST /customer-groups` | — |\n| 4 | `POST /customer-group-members` | customers, customer-groups |\n| 5 | `POST /products` | brands, currencies |\n| 6 | `POST /prices` | products, currencies |\n| 6 | `POST /stocks` | products |\n| 7 | `POST /discounts` | customers/groups, brands |\n| 8 | `POST /invoices` | customers |\n| 9 | `POST /account-transactions` | customers |\n","license":{"name":""},"version":"1.0"},"paths":{"/integration/v1/account-transactions":{"post":{"tags":["account_transactions"],"summary":"Batch current account transaction import.","description":"Imports customer current account transactions from the ERP into the B2B\nportal. Customers can see all their transactions and their current\nbalance on the \"My Account > Statement\" page.\n\n## Currency (CRITICAL)\n\nAs with invoices, all amounts must be sent as **base-currency\nequivalents**. The base currency is selected in the admin Settings page\n(default TRY). The system does not convert currencies.\n\n## Document types\n\n`doc_type` is free text, but standard types are recommended:\n`\"FAT\"` (invoice), `\"TAH\"` (collection), `\"VIR\"` (transfer),\n`\"MUT\"` (reconciliation), `\"IAD\"` (return), `\"DEK\"` (voucher).\n\n## Error cases\n\n- Empty `erp_ref` is a record error.\n- Empty `doc_type` is a record error.\n- `customer_erp_ref` not found in the system is a record error.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountTransactionBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/brands":{"post":{"tags":["brands"],"summary":"Batch brand import.","description":"Imports the brand list from the ERP into the B2B portal. Brands are the\nbuilding blocks of the product catalog — they must be defined **before**\nimporting products.\n\n## Upsert behavior\n\n- A record with an existing `erp_ref` is updated.\n- A new `erp_ref` creates a brand.\n- `name` is always replaced with the latest value.\n- Names are unique case-insensitively. A brand that has no `erp_ref` yet\n  (e.g. created in the admin panel) and the same name is taken over: the\n  `erp_ref` is written onto it instead of creating a second brand.\n\n## Error cases\n\n- Empty `erp_ref` or `name` is a record error.\n- A `name` already used by a brand with a different `erp_ref` is a record\n  error (`brand_name_taken`).\n- On a database error the affected record is skipped; the rest are processed.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/currencies":{"post":{"tags":["currencies"],"summary":"Batch currency definition import.","description":"Imports the currency list from the ERP into the B2B portal. Currencies\nmust be imported after brands and **before** products.\n\n## Upsert behavior\n\n- Matching is done on the `code` field (not `erp_ref`!).\n- Sending the same `code` again updates `name` and `symbol`.\n- `erp_ref` is always replaced with the latest value.\n\n## Error cases\n\n- Empty `erp_ref` or `code` is a record error.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurrencyBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/customer-group-members":{"post":{"tags":["customer_group_members"],"summary":"Manage customer group memberships.","description":"Adds customers to groups or removes them from groups.\n**This is not a full-list endpoint** — every record you send is an add or\nremove operation; existing memberships are preserved.\n\n## Add / remove\n\n- `remove: false` (default) → adds the customer to the group.\n  No change if already a member (idempotent).\n- `remove: true` → removes the customer from the group.\n  No change if not a member.\n\n## Relationship checks\n\n- `customer_erp_ref`: must be imported via `/integration/v1/customers`.\n- `group_erp_ref`: must be imported via `/integration/v1/customer-groups`.\n- Missing references return an error.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/customer-groups":{"post":{"tags":["customer_groups"],"summary":"Batch customer group import.","description":"Imports customer groups from the ERP into the B2B portal. Groups are used\nfor discount rules; use the `/integration/v1/discounts` endpoint to define\nper-group or per-customer discounts.\n\n## Upsert behavior\n\n- A record with an existing `erp_ref` is updated.\n- `name` is always replaced with the latest value.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerGroupBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/customers":{"post":{"tags":["customers"],"summary":"Batch customer import.","description":"Imports the customer (current account) list from the ERP into the B2B\nportal. Most other imports reference customers, so customers must be\nimported **early**.\n\n## Addresses\n\nWhen the `addresses` array is provided, each address is upserted by its\nown `erp_ref`. This is not a full-list replacement — omitted addresses\nare not deleted.\n\n## User accounts\n\nThis endpoint only creates the customer **company**. User accounts\nattached to the company are created manually from the admin panel.\n\n## Error cases\n\n- Empty `erp_ref`, `code` or `title` is a record error.\n- Empty `addresses[].erp_ref` or `addresses[].address` is a record error.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/discounts":{"post":{"tags":["discounts"],"summary":"Batch discount rule import.","description":"Defines customer- or group-specific discount rates. Discounts are applied\nin cascade on the products' gross list price.\n\n## Targeting rules\n\n- Exactly ONE of `customer_erp_ref` / `group_erp_ref` must be given.\n- Neither or both → record error.\n- With `brand`, the discount applies only to that brand's products;\n  without it, to all brands.\n\n## Precedence (on conflict)\n\nWhen a customer has both an individual discount and a group discount,\nthe **individual discount** wins; the group discount is ignored.\n\n## Error cases\n\n- Empty `erp_ref` is a record error.\n- Both or neither of `customer_erp_ref`/`group_erp_ref` given → error.\n- Referenced customer/group/brand missing in the system → error.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscountBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/exchange-rates":{"post":{"tags":["exchange_rates"],"summary":"Batch exchange rate update.","description":"Imports rate definitions (from the ERP or another source) into the B2B\nportal. Rates can also be entered manually in the admin panel\n(`/manage/currencies`); both paths write the same `exchange_rates` table.\n\n## Upsert behavior\n\n- Matching is done on `currency_code + rate_date`.\n- Re-sending a rate for the same day updates the existing record\n  (`source = 'api'`).\n\n## Error cases\n\n- Record error if `currency_code` is not found or is the base currency.\n- Record error if `rate_date` is invalid or in the future.\n- Record error if `rate` is zero or negative.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExchangeRateBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/invoices":{"post":{"tags":["invoices"],"summary":"Batch invoice import.","description":"Imports ERP invoice records into the B2B portal. The customer can view\ntheir invoices on the \"My Account > Invoices\" page.\n\n## Currency conversion (CRITICAL)\n\nThe B2B portal works with a **single currency (the base currency)**.\nThe base currency is selected in the admin Settings page (default TRY).\nEven if ERP invoices are in different currencies, all amounts\n(`net_total`, `vat_total`, `grand_total`, line `unit_price` and\n`line_total`) must be sent as **base-currency equivalents**.\n\nUse the rate on the invoice date for conversion. `currency_code` is\ninformational only; the system does not convert currencies based on it.\n\n## Lines\n\nWhen `lines` is provided, a **full-list replacement** happens.\nTo keep existing lines, omit this field.\n\n## Error cases\n\n- Empty `erp_ref` is a record error.\n- `customer_erp_ref` not found in the system is a record error.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/orders":{"get":{"tags":["orders_export"],"summary":"List orders.","description":"Returns a paginated list for exporting B2B portal orders to the ERP.\n**At-least-once delivery model**: after pulling orders and transferring\nthem to the ERP, mark them via `POST /orders/mark-exported`. Unmarked\norders can be pulled again with `only_new=true`.\n\n## Usage flow\n\n1. `GET /orders?only_new=true` → pull orders never exported before\n2. Transfer the orders to the ERP\n3. `POST /orders/mark-exported` → mark the successfully transferred ones\n\nBecause marking and pulling are separate, orders can be pulled again if\nan error occurs in the ERP.\n\n## Currency\n\nAll amount fields of the order (`total_gross`, `line_total`, etc.) are\nin the currency given by the order's `currency_code` field — the base\ncurrency at the time the order was placed (selected in admin Settings).\nEven if the product is priced in its original currency, the system\nconverts it with the current rate to the order's currency and writes\nthese fields.","operationId":"list","parameters":[{"name":"status","in":"query","description":"Filter by order status.\n\nValues: `\"new\"`, `\"processing\"`, `\"shipped\"`, `\"cancelled\"`, `\"completed\"`.\nOmitted → all statuses.","required":false,"schema":{"type":["string","null"]}},{"name":"since","in":"query","description":"Return orders created at or after this instant.\n\nAccepts either an RFC 3339 timestamp (`2025-01-15T10:30:00Z`, with or\nwithout an explicit UTC offset) or a date-only value (`2025-01-15`).\nA date-only value is interpreted as the start of that day in the site\ndisplay timezone. All timestamps in the response are UTC (RFC 3339).","required":false,"schema":{"type":["string","null"]}},{"name":"only_new","in":"query","description":"If `true`, returns only orders never exported before\n(`exported_at IS NULL`). Defaults to `false`.","required":false,"schema":{"type":"boolean"}},{"name":"page","in":"query","description":"Page number (starts at 1). Default 1.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"per_page","in":"query","description":"Records per page (1-200). Default 50.","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"Paginated order list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResponse"}}}},"401":{"description":"Invalid API key or missing `export` scope"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["export"]}]}},"/integration/v1/orders/mark-exported":{"post":{"tags":["orders_export"],"summary":"Mark orders as exported.","description":"Marks the orders in the given `order_nos` list as transferred to the\nERP. Only orders not previously marked are updated.\n\n## Why a separate endpoint?\n\nPulling (`GET /orders`) and marking (`POST /orders/mark-exported`) are\nseparate. This way:\n- Orders can be pulled again if the ERP transfer fails.\n- On partial success, only the transferred ones are marked.\n- The `only_new=true` filter returns unmarked orders.\n\n## Idempotent\n\nAlready-marked orders are not marked again. `marked` only counts the\nnewly marked ones.","operationId":"mark_exported","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkExportedRequest"}}},"required":true},"responses":{"200":{"description":"Mark-exported result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkExportedResponse"}}}},"401":{"description":"Invalid API key or missing `export` scope"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["export"]}]}},"/integration/v1/orders/{order_no}":{"get":{"tags":["orders_export"],"summary":"Single order detail.","description":"Returns all details of a specific order (lines, prices, shipments).\nCalled with the `order_no` obtained from the list endpoint.\n\n## Usage\n\n- List → obtain `order_no`\n- Detail → obtain all data of the order\n- Mark → mark as exported via `POST /orders/mark-exported`","operationId":"detail","parameters":[{"name":"order_no","in":"path","description":"Order number","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"Order detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportOrder"}}}},"401":{"description":"Invalid API key or missing `export` scope"},"404":{"description":"Order not found"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["export"]}]}},"/integration/v1/prices":{"post":{"tags":["prices"],"summary":"Batch product price update.","description":"Fast sync of product prices only. Use `/integration/v1/products` for\nfull catalog updates.\n\n## Use cases\n\n- Price updates after daily exchange rate changes\n- Starting/ending campaigns\n- Bulk price increases\n\n## Error cases\n\n- Record error if `product_erp_ref` or `currency_code` is not found —\n  make sure the related imports have completed first.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/products":{"post":{"tags":["products"],"summary":"Batch product catalog import.","description":"Imports ERP product data into the B2B portal. This is the most\ncomprehensive import endpoint — you can send all product-related data\n(basic fields, price, reference codes, images) at once.\n\n## Relationship to other endpoints\n\n- Run `/integration/v1/brands` and `/integration/v1/currencies` first.\n- Price-only updates: `/integration/v1/prices` (faster).\n- Stock-only updates: `/integration/v1/stocks` (faster).\n\n## Reference codes and images\n\nThe `references` and `images` fields do a **full-list replacement**.\nTo preserve existing data, omit these fields entirely. To only add\nitems, read the current list first, append the new items and send the\nfull list.\n\n## Image downloading\n\n`images` URLs are downloaded by the system in the background. If a\ndownload fails, the product record is unaffected — only the image is\nmissing. Download status can be followed in the admin panel.\n\n## Error cases\n\n- Required fields (`erp_ref`, `code`, `name`) empty → record error.\n- `brand` missing in the system → record error (run the brand import first).\n- `currency_code` missing in the system → record error.\n- `references[].kind` not `\"oem\"` or `\"competitor\"` → record error.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}},"/integration/v1/stocks":{"post":{"tags":["stocks"],"summary":"Batch stock quantity update.","description":"Fast sync of stock quantities only. Does not change product details\n(name, price, etc.). Faster than `/integration/v1/products` because it\nupdates a single field without a transaction.\n\n## Use case\n\nIdeal for sending only the products whose stock changed in the ERP\n(sales, returns, counts). Use `/integration/v1/products` for full\ncatalog updates.\n\n## Error cases\n\n- Record error if `product_erp_ref` is not found in the system —\n  run the product import first.","operationId":"import","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StockBatch"}}},"required":true},"responses":{"200":{"description":"Import result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResponse"}}}},"401":{"description":"Invalid API key or missing `import` scope"},"413":{"description":"1000 record limit exceeded"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearer_auth":["import"]}]}}},"components":{"schemas":{"AccountTransactionBatch":{"type":"object","description":"Batch account transaction import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AccountTransactionItem"},"description":"List of account transactions to import (max 1000)"}}},"AccountTransactionItem":{"type":"object","description":"Account transaction record.\n\nRepresents the customer's current account movements (debit/credit).\nCustomers can see their movements and current balance on the\n\"My Account > Statement\" page.\n\n## IMPORTANT: Currency\n\nAs with invoices, **all amounts must be sent as base-currency\nequivalents**. The base currency is selected in the admin Settings page\n(default TRY). Even if ERP movements are in different currencies, convert\n`debit` and `credit` to the base currency using the rate on the relevant\ndate before sending.\n\n`currency_code` is informational and not used in calculations.\nWhen omitted, the system's base currency is used.\n\n## Debit / Credit\n\n- `debit`: the amount the customer owes (base currency).\n- `credit`: the amount the customer is credited (base currency).\n- Both can be greater than zero in one record.\n- When both are omitted, `0` is assigned.\n\n## Import order\n\nAccount transactions must be imported after customers (step 9).","required":["erp_ref","customer_erp_ref","tx_date","doc_type"],"properties":{"credit":{"type":["string","null"],"description":"Credit amount (base currency).\n\nThe amount to be refunded/offset to the customer.\nOmitted → `0`.","example":0.0},"currency_code":{"type":["string","null"],"description":"Original currency of the transaction (informational).\n\nWhen omitted, the system's base currency is used.\nAmounts must always be in the base currency.","example":"TRY"},"customer_erp_ref":{"type":"string","description":"ERP code of the customer the transaction belongs to.\n\nMust be imported via `/integration/v1/customers`.","example":"CUST-001"},"debit":{"type":["string","null"],"description":"Debit amount (base currency).\n\nThe amount to be collected from the customer.\nOmitted → `0`.","example":3000.0},"description":{"type":"string","description":"Transaction description.\n\nShown to the customer on the statement page."},"doc_no":{"type":["string","null"],"description":"Document number (human readable).","example":"FAT-2024-001234"},"doc_type":{"type":"string","description":"Document type.\n\n**Required.** E.g. `\"FAT\"` (invoice), `\"TAH\"` (collection),\n`\"VIR\"` (transfer), `\"MUT\"` (reconciliation).\nUsed for filtering on the statement page.","example":"FAT"},"due_date":{"type":["string","null"],"description":"Due date (ISO 8601, YYYY-MM-DD).\n\nShown to the customer as due-date information when present.\nValidated per record — a malformed value fails only this record.","example":"2024-04-15"},"erp_ref":{"type":"string","description":"Unique code of the transaction in the ERP.\n\nSending the same `erp_ref` again updates the transaction.","example":"TXN-2024-0001"},"tx_date":{"type":"string","description":"Transaction date (ISO 8601, YYYY-MM-DD).\n\nValidated per record — a malformed value fails only this record\n(partial success contract, docs/architecture.md §14.1).","example":"2024-03-15"}}},"AddressItem":{"type":"object","description":"Customer address record.\n\nA delivery or billing address attached to a customer. Addresses are\nimported together with the main record; they have no separate endpoint.\nEach address has its own `erp_ref` used for upserting.\n\n## Upsert, not a full list\n\nAddresses are processed as **upserts per `erp_ref`, not as a full list**.\nTo add a new address for a customer, just send that address — other\naddresses are never deleted.","required":["erp_ref","address"],"properties":{"address":{"type":"string","description":"Full address text.\n\n**Required.** Cannot be empty.","example":"42 Technology St, Industrial District"},"city":{"type":["string","null"],"description":"City","example":"Istanbul"},"district":{"type":["string","null"],"description":"District","example":"Kadikoy"},"erp_ref":{"type":"string","description":"Unique code of the address in the ERP.\n\nSending the same `erp_ref` again updates the address.","example":"ADR-001"},"is_default":{"type":["boolean","null"],"description":"Is this the default address?\n\nMultiple addresses may have `is_default: true`.\nThe default address is preselected at checkout.","example":true},"title":{"type":["string","null"],"description":"Address title (e.g. \"HQ\", \"Branch\", \"Warehouse\").\n\nOmitted → defaults to \"HQ\".","example":"HQ"}}},"BrandBatch":{"type":"object","description":"Batch brand import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/BrandItem"},"description":"List of brands to import (max 1000)"}}},"BrandItem":{"type":"object","description":"Brand definition.\n\nDefines the brands products belong to. Once a brand exists, product imports\nmatch it via the `brand` field.\n\n## Import order\n\nBrands must be imported **first**. Since products reference brands, a product\nwhose brand is not defined comes back as an error in the `errors` array.","required":["erp_ref","name"],"properties":{"erp_ref":{"type":"string","description":"Unique brand code in the ERP system.\n\nSending the same `erp_ref` again **updates** the brand (upsert).","example":"BRD001","maxLength":100},"name":{"type":"string","description":"Display name of the brand (e.g. \"Bosch\", \"Valeo\").\n\nProduct matching compares names case-insensitively.","example":"Bosch"}}},"CurrencyBatch":{"type":"object","description":"Batch currency import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CurrencyItem"},"description":"List of currencies to import (max 1000)"}}},"CurrencyItem":{"type":"object","description":"Currency definition.\n\nEvery price in the system is tied to a currency. In the import order,\ncurrencies come **second** (after brands, before products). Product and\nprice imports match against `currency_code`.\n\n## Base currency\n\nThe system must have **exactly one** currency with `is_base = true`.\nWhen a product import omits `currency_code`, this base currency is used.\nThe base currency starts as `TRY` at seed time and can be changed from\nthe admin panel (Settings → Base Currency) — the system re-scales\nexchange rates on that change (docs/architecture.md §7).\n\n## Exchange rates\n\nThis endpoint imports **currency definitions only**, not rates.\nExchange rates live in the `exchange_rates` table and are entered\nmanually in the admin panel (`/manage/currencies`, source `manual`) or\nvia the `POST /integration/v1/exchange-rates` endpoint (source `api`).","required":["erp_ref","code"],"properties":{"code":{"type":"string","description":"ISO 4217 currency code (e.g. \"TRY\", \"USD\", \"EUR\").\n\n**Required.** Stored uppercased. Sending the same `code` again\nupdates the existing record.","example":"TRY","maxLength":3,"minLength":3},"erp_ref":{"type":"string","description":"Unique currency code in the ERP system.\n\nProduct and price imports match `currency_code` against this code\n(case-insensitive, stored uppercased).","example":"CUR001"},"name":{"type":["string","null"],"description":"Full name of the currency.\n\nDefaults to `code` when omitted.","example":"Turkish Lira"},"symbol":{"type":["string","null"],"description":"Currency symbol (e.g. \"₺\", \"$\", \"€\").\n\nDefaults to `code` when omitted.","example":"₺"}}},"CustomerBatch":{"type":"object","description":"Batch customer import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CustomerItem"},"description":"List of customers to import (max 1000)"}}},"CustomerGroupBatch":{"type":"object","description":"Batch customer group import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CustomerGroupItem"},"description":"List of groups to import (max 1000)"}}},"CustomerGroupItem":{"type":"object","description":"Customer group definition.\n\nUsed to group customers. Groups are the target of **discount** and\n**special pricing** rules. To add members to a group, use the\n`/integration/v1/customer-group-members` endpoint.\n\n## Import order\n\nCustomer groups must be imported after customers (step 3).","required":["erp_ref","name"],"properties":{"erp_ref":{"type":"string","description":"Unique group code in the ERP system","example":"GRP-BAYI"},"name":{"type":"string","description":"Display name of the group (e.g. \"Dealers\", \"Main Distributors\")","example":"Dealers"}}},"CustomerItem":{"type":"object","description":"Customer record.\n\nBasic information of the customer company that will log into the B2B\nportal. After customers are imported, user accounts must be created from\nthe admin panel (customers cannot self-register).\n\n## Import order\n\nCustomers must be imported after brands and currencies (step 3).\nCustomer groups and discounts reference customers, so customers must be\ndefined first.","required":["erp_ref","code","title"],"properties":{"addresses":{"type":["array","null"],"items":{"$ref":"#/components/schemas/AddressItem"},"description":"List of addresses belonging to the customer.\n\nWhen provided, each address is upserted by its `erp_ref`.\nNot a full-list replacement — omitted addresses are preserved."},"code":{"type":"string","description":"Customer code (short human-readable code).\n\n**Required.** Visible in the portal and in lists.","example":"BCK-001"},"email":{"type":["string","null"],"description":"Company e-mail address.\n\nOrder confirmations, invoices and password resets are sent here.","example":"info@bcs-kadikoy.com"},"erp_ref":{"type":"string","description":"Unique customer code in the ERP.\n\nAll other imports (`invoices`, `discounts`, `account_transactions`)\nreference the customer by this code.","example":"CUST-001"},"is_active":{"type":["boolean","null"],"description":"Is the customer active?\n\nWhen `false`, the customer cannot log in or place orders.\nOmitted → defaults to `true`.","example":true},"phone":{"type":["string","null"],"description":"Company phone number","example":"+902161234567"},"tax_number":{"type":["string","null"],"description":"Tax number (10 or 11 digits)","example":"1234567890"},"tax_office":{"type":["string","null"],"description":"Tax office name","example":"Kadikoy Tax Office"},"title":{"type":"string","description":"Company title.\n\n**Required.** Used on invoices and orders.","example":"Bosch Car Service Kadikoy"}}},"DiscountBatch":{"type":"object","description":"Batch discount import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/DiscountItem"},"description":"List of discounts to import (max 1000)"}}},"DiscountItem":{"type":"object","description":"Discount rule.\n\nDefines a customer- or group-specific price reduction. Works with three\ncascading discount rates applied on the product's `gross_price`\n(gross list price).\n\n## Discount tiers\n\nThe system supports three tiers:\n- `rate1`: first discount rate (%) — general customer discount\n- `rate2`: second discount rate (%) — volume/performance discount\n- `rate3`: third discount rate (%) — campaign/term discount\n\nTiers are applied in cascade:\n`net = gross * (1 - rate1/100) * (1 - rate2/100) * (1 - rate3/100)`\n\n## Targeting\n\nEvery discount rule applies to a **customer** OR a **customer group**\n(never both at once). Optionally a `brand` filter limits it to that\nbrand's products.\n\n## Import order\n\nDiscounts must be imported after customers, groups and brands (step 7).","required":["erp_ref"],"properties":{"brand":{"type":["string","null"],"description":"Brand name to restrict the discount to that brand's products.\n\nEmpty → applies to all brands.","example":"Bosch"},"customer_erp_ref":{"type":["string","null"],"description":"ERP code of the customer the discount applies to.\n\nCannot be combined with `group_erp_ref`.\nMust be imported via `/integration/v1/customers`.","example":"CUST-001"},"erp_ref":{"type":"string","description":"Unique code of the discount rule in the ERP.\n\nSending the same `erp_ref` again updates the rule.","example":"DSC-001"},"group_erp_ref":{"type":["string","null"],"description":"ERP code of the customer group the discount applies to.\n\nCannot be combined with `customer_erp_ref`.\nMust be imported via `/integration/v1/customer-groups`.","example":"GRP-DEALER"},"is_active":{"type":["boolean","null"],"description":"Is the discount active?\n\nOmitted → defaults to `true`.","example":true},"rate1":{"type":["string","null"],"description":"1st discount rate (percent, 0-100).\n\nOmitted → `0` (no discount).","example":10.0},"rate2":{"type":["string","null"],"description":"2nd discount rate (percent, 0-100).","example":5.0},"rate3":{"type":["string","null"],"description":"3rd discount rate (percent, 0-100).","example":2.0}}},"ExchangeRateBatch":{"type":"object","description":"Batch exchange rate entry request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExchangeRateItem"},"description":"List of rates to import (max 1000)"}}},"ExchangeRateItem":{"type":"object","description":"A single daily exchange rate record.\n\nRates are stored in `exchange_rates` with a `(currency_id, rate_date)`\nunique key. Sending a rate for the same day again updates the existing\nrecord — no duplicates.","required":["currency_code","rate_date","rate"],"properties":{"currency_code":{"type":"string","description":"ISO 4217 currency code (e.g. \"USD\").\n\nMust be a currency imported via `/integration/v1/currencies` and must\nNOT be the base currency (`is_base = true`).","example":"USD"},"rate":{"type":"string","description":"Value of 1 unit of the currency in the **base currency**.\n\nThe base currency is selected in the admin Settings page (default TRY).\nMust be greater than zero.","example":41.86},"rate_date":{"type":"string","description":"Day the rate applies to, ISO 8601 (`YYYY-MM-DD`).\n\nFuture dates are rejected; back-dated entries (corrections) are allowed.","example":"2026-07-15"}}},"ExportOrder":{"type":"object","description":"Order record (export).\n\nContains all details of an order placed in the B2B portal.\nExported for transfer to the ERP.\nAll amount fields are in the currency given by `currency_code`\n(the base currency at order time).","required":["id","order_no","customer_id","customer_code","customer_title","user_id","placed_by_agent","delivery_method","ship_address","currency_code","total_gross","total_discount","total_net","total_vat","total_with_vat","status","created_at","items","shipments"],"properties":{"created_at":{"type":"string","format":"date-time","description":"Order creation time"},"currency_code":{"type":"string","description":"Currency code of the order (the base currency at order time).\n\nAll amount fields are in this currency."},"customer_code":{"type":"string","description":"Customer code"},"customer_erp_ref":{"type":["string","null"],"description":"Customer's ERP code"},"customer_id":{"type":"string","format":"uuid","description":"Customer ID"},"customer_title":{"type":"string","description":"Customer title"},"delivery_method":{"type":"string","description":"Delivery method (e.g. \"Cargo\", \"Pickup\")"},"exported_at":{"type":["string","null"],"format":"date-time","description":"Time exported to the ERP (null = not yet)"},"id":{"type":"string","format":"uuid","description":"Order ID"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ExportOrderItem"},"description":"Order lines"},"note":{"type":["string","null"],"description":"Order note"},"order_no":{"type":"integer","format":"int64","description":"Order number (human readable, sequential)"},"placed_by_agent":{"type":"boolean","description":"Was it placed by an agent (customer representative)?"},"ship_address":{"type":"string","description":"Delivery address (single line)"},"shipments":{"type":"array","items":{"$ref":"#/components/schemas/ExportShipment"},"description":"Shipments"},"status":{"type":"string","description":"Order status (\"new\", \"processing\", \"shipped\", \"cancelled\", \"completed\")"},"total_discount":{"type":"string","description":"Total discount (order currency)"},"total_gross":{"type":"string","description":"Gross total (order currency, before discounts)"},"total_net":{"type":"string","description":"Net total (order currency, excl. VAT)"},"total_vat":{"type":"string","description":"Total VAT (order currency)"},"total_with_vat":{"type":"string","description":"Grand total (order currency, incl. VAT)"},"user_id":{"type":"string","format":"uuid","description":"ID of the user who placed the order"}}},"ExportOrderItem":{"type":"object","description":"Order line (export).\n\nA product line inside an order. Contains all price and discount\ninformation at order time (a snapshot).\nAll amount fields are in the order's currency\n(`ExportOrder.currency_code` — the base currency at order time).","required":["id","product_id","product_code","product_name","brand_name","quantity","unit","currency_code","exchange_rate","gross_price","discount_rate1","discount_rate2","discount_rate3","is_campaign","unit_net","vat_rate","line_gross","line_discount","line_net","line_vat","line_total","stock_state_at_order","status_name","shipped_qty","cancelled_qty"],"properties":{"brand_name":{"type":"string","description":"Brand name (at order time)"},"cancelled_qty":{"type":"string","description":"Cancelled quantity"},"currency_code":{"type":"string","description":"Original currency code of the product"},"discount_rate1":{"type":"string","description":"1st discount rate (%)"},"discount_rate2":{"type":"string","description":"2nd discount rate (%)"},"discount_rate3":{"type":"string","description":"3rd discount rate (%)"},"exchange_rate":{"type":"string","description":"Exchange rate at order time (original currency → order currency)"},"gross_price":{"type":"string","description":"Gross unit price (in the original currency)"},"id":{"type":"string","format":"uuid","description":"Line ID"},"is_campaign":{"type":"boolean","description":"Was it bought at the campaign price?"},"line_discount":{"type":"string","description":"Line discount total (order currency)"},"line_gross":{"type":"string","description":"Line gross total (order currency)"},"line_net":{"type":"string","description":"Line net total (order currency, excl. VAT)"},"line_total":{"type":"string","description":"Line grand total (order currency, incl. VAT)"},"line_vat":{"type":"string","description":"Line VAT total (order currency)"},"product_code":{"type":"string","description":"Product code (at order time)"},"product_id":{"type":"string","format":"uuid","description":"Product ID"},"product_name":{"type":"string","description":"Product name (at order time)"},"quantity":{"type":"string","description":"Ordered quantity"},"shipped_qty":{"type":"string","description":"Shipped quantity"},"status_name":{"type":"string","description":"Current line status (e.g. \"Pending\", \"Shipped\")"},"stock_state_at_order":{"type":"string","description":"Stock state at order time (\"in_stock\" / \"out_of_stock\")"},"unit":{"type":"string","description":"Unit"},"unit_net":{"type":"string","description":"Unit net price after discount (order currency)"},"vat_rate":{"type":"string","description":"VAT rate (%)"}}},"ExportShipment":{"type":"object","description":"Shipment record (export).\n\nA partial or full shipment of an order.\nAn order can have multiple shipments.","required":["shipment_no","shipped_at","items"],"properties":{"carrier":{"type":["string","null"],"description":"Carrier name"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ExportShipmentItem"},"description":"Shipped lines"},"note":{"type":["string","null"],"description":"Shipment note"},"shipment_no":{"type":"integer","format":"int32","description":"Shipment number (sequential per order)"},"shipped_at":{"type":"string","format":"date-time","description":"Shipment date"},"tracking_no":{"type":["string","null"],"description":"Carrier tracking number"}}},"ExportShipmentItem":{"type":"object","description":"Shipment line (export).","required":["order_item_id","quantity"],"properties":{"order_item_id":{"type":"string","format":"uuid","description":"ID of the order line"},"quantity":{"type":"string","description":"Shipped quantity"}}},"ImportError":{"type":"object","description":"Error detail of a failed record.","required":["index","erp_ref","code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable error code (e.g. `invoice_date_invalid`).\n\nThe admin UI maps known codes to localized messages; unknown codes\nfall back to the English `message`."},"erp_ref":{"type":"string","description":"ERP reference code identifying the record the error belongs to"},"index":{"type":"integer","description":"Zero-based position in the submitted `items` array","minimum":0},"message":{"type":"string","description":"Human-readable error description (English)"}}},"ImportResponse":{"type":"object","description":"Result summary of an import operation.\n\nAll import endpoints return this structure. The operation works with a\n**partial success** model: even a 200 response may contain failed\nrecords. Always check the `errors` array.\n\nFailed records can be fixed and re-sent — the operation is idempotent\nover the upsert key (`erp_ref`).","required":["received","upserted","failed","errors"],"properties":{"errors":{"type":"array","items":{"$ref":"#/components/schemas/ImportError"},"description":"Details of failed records (index, ERP reference, error message)"},"failed":{"type":"integer","description":"Number of failed records","minimum":0},"received":{"type":"integer","description":"Total number of records submitted","minimum":0},"upserted":{"type":"integer","description":"Number of records successfully created or updated","minimum":0}}},"InvoiceBatch":{"type":"object","description":"Batch invoice import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceItem"},"description":"List of invoices to import (max 1000)"}}},"InvoiceItem":{"type":"object","description":"Invoice record.\n\nAn ERP invoice imported into the B2B portal. The customer can view their\ninvoices and download the PDF from the \"My Account\" page.\n\n## IMPORTANT: Currency\n\n**The system works with a single currency.** ERP invoices may exist in\ndifferent currencies (TRY, USD, EUR, etc.), but when importing into the\nB2B portal all amounts must be sent as **base-currency equivalents**.\nThe base currency is selected in the admin Settings page (default TRY).\n\nExample: for a USD invoice in the ERP, convert it using the rate on the\ninvoice date and send the `currency_code` field with that currency's\ncode. USD amounts sent without conversion are interpreted as base\ncurrency and lead to wrong balance calculations.\n\n`currency_code` is informational: it may show the real currency of the\namounts, but it is ignored in calculations.\n\n## Import order\n\nInvoices must be imported after customers (step 8).","required":["erp_ref","customer_erp_ref","invoice_no","invoice_date","net_total","vat_total","grand_total"],"properties":{"currency_code":{"type":["string","null"],"description":"Original currency code of the invoice (informational).\n\nWhen omitted, the system's **base currency** is used.\n**Amounts must always be sent as base-currency equivalents.**","example":"TRY"},"customer_erp_ref":{"type":"string","description":"ERP code of the customer the invoice belongs to.\n\nMust be imported via `/integration/v1/customers`.","example":"CUST-001"},"erp_ref":{"type":"string","description":"Unique code of the invoice in the ERP.\n\nSending the same `erp_ref` again updates the invoice.","example":"INV-2024-0001"},"file_url":{"type":["string","null"],"description":"URL of the invoice PDF/HTML file.\n\nThe customer can view/download the invoice from this URL.\nThe system does not download or store the file — it only presents\nthe link.","example":"https://erp.example.com/invoices/INV-2024-0001.pdf"},"grand_total":{"type":"string","description":"Grand total (base currency).\n\n**Required.** Should normally equal `net_total + vat_total`.","example":3000.0},"invoice_date":{"type":"string","description":"Invoice date (ISO 8601, YYYY-MM-DD).\n\nValidated per record — a malformed value fails only this record\n(partial success contract, docs/architecture.md §14.1).","example":"2024-03-15"},"invoice_no":{"type":"string","description":"Invoice number (human readable).","example":"FAT-2024-001234"},"lines":{"type":["array","null"],"items":{"$ref":"#/components/schemas/InvoiceLineItem"},"description":"Invoice lines.\n\n**When provided, a FULL-LIST replacement happens** — existing lines\nare deleted and the new ones written. To keep existing lines, omit\nthis field entirely."},"net_total":{"type":"string","description":"Total excluding VAT (base currency).\n\n**Required.** If the ERP invoice is not in the base currency, send\nthe amount converted to the base currency using the rate on the\nrelevant date.","example":2500.0},"order_no":{"type":["integer","null"],"format":"int64","description":"The `order_no` of the order this invoice belongs to.\n\nThe invoice counterpart of orders placed in the B2B portal.\nLeave empty for invoices independent of the ERP.","example":1001},"vat_total":{"type":"string","description":"Total VAT amount (base currency).\n\n**Required.**","example":500.0}}},"InvoiceLineItem":{"type":"object","description":"Invoice line item.\n\nA product line inside an invoice. Invoice lines are imported together\nwith the main record. **When provided, a FULL-LIST replacement happens** —\nexisting lines are deleted and the new ones written. To keep existing\nlines, omit this field entirely.","required":["product_code","quantity","unit_price","line_total"],"properties":{"brand_name":{"type":"string","description":"Brand name (at invoice time)"},"line_total":{"type":"string","description":"Line total (in the submitted currency).\n\nShould normally equal `quantity * unit_price`.","example":3001.0},"product_code":{"type":"string","description":"Product code (in the ERP or the B2B portal).\n\nUsed to match against order lines.","example":"BCK-001"},"product_name":{"type":"string","description":"Product name (at invoice time)","example":"Front Brake Pad Set"},"quantity":{"type":"string","description":"Quantity","example":2.0},"unit_price":{"type":"string","description":"Unit price (in the submitted currency)","example":1500.5}}},"ListResponse":{"type":"object","description":"Paginated order list response.","required":["items","total","page","per_page"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExportOrder"},"description":"Order list"},"page":{"type":"integer","format":"int64","description":"Current page number"},"per_page":{"type":"integer","format":"int64","description":"Records per page"},"total":{"type":"integer","format":"int64","description":"Total order count (matching the filters)"}}},"MarkExportedRequest":{"type":"object","description":"Mark-as-exported request.","required":["order_nos"],"properties":{"order_nos":{"type":"array","items":{"type":"integer","format":"int64"},"description":"Order numbers to mark as exported.\n\nThe `exported_at` field of the orders in this list is set to `now()`.\nAlready-marked orders are not marked again.","example":"[1001, 1002, 1003]"}}},"MarkExportedResponse":{"type":"object","description":"Mark-as-exported response.","required":["marked"],"properties":{"marked":{"type":"integer","format":"int64","description":"Number of orders successfully marked.\n\nAlready-marked orders are not counted."}}},"MemberBatch":{"type":"object","description":"Batch membership import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MemberItem"},"description":"List of memberships to import (max 1000)"}}},"MemberItem":{"type":"object","description":"Group membership record.\n\nAdds a customer to a group or removes them from it.\n**This endpoint does NOT do full-list replacement** — each record is a\nsingle add or remove operation. Existing memberships are never deleted.\n\n## Import order\n\nGroup members must be imported after customers and groups (step 4).\nBoth the customer (`customer_erp_ref`) and the group (`group_erp_ref`)\nmust already be imported.","required":["customer_erp_ref","group_erp_ref"],"properties":{"customer_erp_ref":{"type":"string","description":"ERP code of the customer to add or remove.\n\nMust point to a customer imported via `/integration/v1/customers`.","example":"CUST-001"},"group_erp_ref":{"type":"string","description":"ERP code of the group the customer is added to or removed from.\n\nMust point to a group imported via `/integration/v1/customer-groups`.","example":"GRP-DEALER"},"remove":{"type":"boolean","description":"If `true`, **removes** the customer from the group.\n\nIf `false` (default), **adds** the customer to the group.\nAdding an existing member is a no-op (idempotent).","example":false}}},"PriceBatch":{"type":"object","description":"Batch price update request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PriceItem"},"description":"List of prices to import (max 1000)"}}},"PriceItem":{"type":"object","description":"Price update record.\n\nUpdates only the price fields of an existing product.\n**Does not create products.** Products must already be imported via\n`/integration/v1/products`.\n\n## Difference from `/integration/v1/products`\n\n- `products`: full product data (name, brand, description, references, images, price)\n- `prices`: **price fields only** (fast, single transaction)\n\nUse `prices` for frequent intraday price changes, and `products` for the\ninitial load and catalog updates.\n\n## Campaign price\n\nWhen `campaign_price` is omitted (null/absent), the current value is\n**preserved**. To remove a campaign, send `campaign_price: null` via the\n`products` endpoint instead.\n\n## Import order\n\nPrices must be imported after products (step 6).","required":["product_erp_ref","gross_price","currency_code"],"properties":{"campaign_price":{"type":["string","null"],"description":"Campaign price (in the given currency).\n\nWhen greater than 0, customers see this price instead of the regular\none. Omitted → the current campaign price is preserved.","example":1299.9},"currency_code":{"type":"string","description":"Currency code of this price (e.g. \"TRY\", \"USD\", \"EUR\").\n\nMust be imported via `/integration/v1/currencies`.","example":"TRY"},"gross_price":{"type":"string","description":"Gross list price excluding VAT (in the given currency).\n\nThis is the price shown to the customer and the base of the discount\ncalculation.","example":1500.5},"product_erp_ref":{"type":"string","description":"ERP code of the product to update","example":"PRD-001"},"vat_rate":{"type":["string","null"],"description":"VAT rate (percent, e.g. 20.0 = 20%).\n\nOmitted → the current VAT rate is preserved. Default for new products\nis 20%.","example":20.0}}},"ProductBatch":{"type":"object","description":"Batch product import request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ProductItem"},"description":"List of products to import (max 1000)"}}},"ProductItem":{"type":"object","description":"Product record.\n\nA spare part product listed and orderable in the B2B portal — the exact\ncounterpart of the ERP product catalog.\n\n## Import order\n\nProducts must be imported after brands and currencies (step 5).\n`brand` and `currency_code` must point to previously imported records.\n\n## Upsert behavior\n\n- Matching `erp_ref` → **update**.\n- No match → **new product** is created.\n- When the `references` and `images` arrays are provided, a **full-list\n  replacement** happens (existing references/images are deleted and the\n  new ones written).\n- `campaign_price`: omitted → current value preserved,\n  `null` → campaign price removed.\n\n## Images\n\nURLs in the `images` array are downloaded by the system and stored under\n`uploads/products/`. Downloading runs asynchronously in the background;\nit does not complete immediately.","required":["erp_ref","code","name"],"properties":{"brand":{"type":["string","null"],"description":"Brand name.\n\nMust exactly match a brand imported via `/integration/v1/brands`\n(case-insensitive). The record fails if the brand is missing.","example":"Bosch"},"campaign_price":{"type":["string","null"],"description":"Campaign price.\n\n- Omitted: current value preserved (no change).\n- `null`: campaign price removed.\n- A value: campaign price updated."},"code":{"type":"string","description":"Product code (short code shown in the catalog).\n\n**Required.** The primary code customers search with.","example":"BCK-001"},"currency_code":{"type":["string","null"],"description":"Currency code of this price (e.g. \"TRY\", \"USD\", \"EUR\").\n\nMust be imported via `/integration/v1/currencies`.\nOmitted → the system's base currency is used.","example":"TRY"},"description":{"type":["string","null"],"description":"Product description.\n\nShown on the product detail page. Plain text, not HTML.","example":"High-performance ceramic brake pads"},"erp_ref":{"type":"string","description":"Unique product code in the ERP.\n\nAll product references (price, stock, order) use this code.\n**Required.**","example":"PRD-001"},"gross_price":{"type":["string","null"],"description":"Gross list price excluding VAT (in the currency given by `currency_code`).\n\nOmitted → assigned `0`. Can be updated later via `/integration/v1/prices`.","example":1500.5},"images":{"type":["array","null"],"items":{"type":"string"},"description":"List of product image URLs.\n\n**When provided, a full-list replacement happens.** Images are\ndownloaded in the background and stored under `uploads/products/`."},"is_active":{"type":["boolean","null"],"description":"Is the product active?\n\nWhen `false`, the product is hidden from the catalog and cannot be\nadded to the cart. Omitted → defaults to `true`.","example":true},"manufacturer_code":{"type":["string","null"],"description":"Manufacturer part code (the code printed on the product).","example":"BP1234"},"name":{"type":"string","description":"Product name.\n\n**Required.** The full product name shown to the customer.","example":"Front Brake Pad Set"},"references":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ProductReferenceItem"},"description":"Product reference codes (OEM/competitor).\n\n**When provided, a FULL-LIST replacement happens** — all existing\nreferences are deleted and only the ones in this list are added.\nTo keep references, omit this field entirely."},"unit":{"type":["string","null"],"description":"Unit (e.g. \"AD\", \"TK\", \"LT\").\n\nOmitted → defaults to `\"AD\"` (piece).","example":"AD"},"vat_rate":{"type":["string","null"],"description":"VAT rate (percent).\n\nOmitted → current value preserved. Default for new products is 20%.","example":20.0}}},"ProductReferenceItem":{"type":"object","description":"Product reference code (OEM or competitor code).\n\nHolds the product's equivalents in other systems. Lets customers find\nthe right product when searching by OEM or competitor codes.","required":["kind","code"],"properties":{"brand_name":{"type":"string","description":"Brand name the reference code belongs to","example":"Bosch"},"code":{"type":"string","description":"Reference code","example":"F00E123456"},"kind":{"type":"string","description":"Reference kind: `\"oem\"` or `\"competitor\"`.\n\n- `oem`: Original manufacturer code (e.g. the vehicle's original part number)\n- `competitor`: The code a competitor brand gives to the same product","example":"oem"}}},"StockBatch":{"type":"object","description":"Batch stock update request.","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/StockItem"},"description":"List of stock updates to import (max 1000)"}}},"StockItem":{"type":"object","description":"Stock quantity update record.\n\nUpdates only the stock quantity of an existing product.\n**Does not create products.** Products must already be imported via\n`/integration/v1/products`.\n\n## Import order\n\nStocks must be imported after products (step 6), optionally together\nwith the product and price imports.\n\n## Stock semantics\n\n- `stock_qty` is in units (not boxes/packs).\n- Negative stock is technically accepted but not recommended.\n- `stock_qty = 0` means out of stock; the product is not deactivated.\n- Out-of-stock products can still be added to the cart, but a warning is\n  shown before the order is completed.","required":["product_erp_ref","stock_qty"],"properties":{"product_erp_ref":{"type":"string","description":"ERP code of the product to update.\n\nMust point to a product imported via `/integration/v1/products`.\nReturns an error if not found.","example":"PRD-001"},"stock_qty":{"type":"string","description":"Current stock quantity (units).","example":42.0}}}},"securitySchemes":{"bearer_auth":{"type":"http","scheme":"bearer","bearerFormat":"API key (created in the admin panel)","description":"Send the API key created in the admin panel in `Bearer <key>` format."}}},"security":[{"bearer_auth":[]}]}