package httpapi // OpenAPI 3.0 for the public /api/v1 surface (API-key auth). // Served at GET /api/v1/openapi.yaml; rendered by the marketing /docs page. var v1OpenAPIYAML = []byte(`openapi: 3.0.3 info: title: Descrybe Public API version: '1.0' description: | Public catalog, feed, and product-processing API. ## Base URL The public API is hosted by Descrybe (customers do not self-host this surface). - Production: https://descrybe.io/api/v1 OpenAPI document: GET https://descrybe.io/api/v1/openapi.yaml ## Authentication All /api/v1 operations require a company API key except: Document-level security is BearerAuth OR ApiKeyAuth (same key value). Do not send dashboard session cookies or CSRF tokens to /api/v1. ### Security schemes (components.securitySchemes) - BearerAuth - HTTP bearer. Authorization: Bearer dk_your_key (preferred) - ApiKeyAuth - header X-API-Key: dk_your_key When both headers are set, Bearer wins. Full key values are never embedded in this YAML. RapiDoc Try-it: paste a key, or when logged into the docs page use "Use my API key" (coordinates with the in-app authorize helper). ### Create a key in the app 1. Sign in at https://descrybe.io 2. Open Settings -> API keys (/settings?tab=api-keys) 3. Company admins create a key via dashboard POST /api/api-keys (session cookie + CSRF; not this public OpenAPI surface). The secret is shown once and starts with dk_. 4. Store it securely. Later list/revoke shows only key_prefix (first 10 characters). Revoked keys fail auth immediately. ### Cutover / migration (reissue) API keys from the previous Descrybe platform were not migrated. After cutover, integrations must create a new dk_ key in Settings -> API keys (or Use my API key on /docs). Pre-cutover secrets return the same HTTP 401 Unauthorized as unknown keys — there is no separate “legacy key” error. ### 401 Unauthorized Missing, empty, unknown, revoked, or non-migrated (pre-cutover) keys return HTTP 401 from RequireAPIKey with the legacy coded envelope: { "error": { "code": "unauthorized", "message": "Unauthorized" } } See components.responses.Unauthorized (schema LegacyAPIError). Reissue via Settings -> API keys (/settings?tab=api-keys). ### 403 Forbidden Bad API keys on /api/v1 never return 403 (always 401). Tenant scope comes from the key; cross-company resources typically 404. HTTP 403 appears on dashboard /api/* session routes (admin required, CSRF mismatch) under Team/Admin tags (SessionCookie + CSRFHeader) - not this public key surface. ### Rate limits Heavy mutations are limited to 30 requests per minute per company (in-process per API replica; not shared across replicas). Counts HTTP requests, not products inside a bulk body. Limited POST paths: - /products/process, /process, /process/{id}/retry - /feeds/{id}/sync, /feeds/{id}/extract-schema, /feeds/{id}/sync-process-sample - /export-feeds/{id}/generate, /export-feeds/{id}/export-products, /export-feeds/{id}/rotate-token (admin) Over limit: HTTP 429, header Retry-After: 60, body { "error": "rate limit exceeded" } (see TooManyRequests). Ordinary GETs and other mutations are outside this HTTP budget (process starts may still return 402 for plan/credits). ## Quick curl curl -s -H "Authorization: Bearer dk_your_key" \ "https://descrybe.io/api/v1/products?page=1&limit=1" Local Go API (default listen from README): curl -s -H "Authorization: Bearer dk_your_key" \ "http://localhost:28471/api/v1/products?page=1&limit=1" Health: GET /api/v1/health (also /healthz and /readyz on the API host). ## Processing contracts (dual-mode) Two separate surfaces — do not mix bodies or response envelopes: 1. **Legacy public process (source of truth for integrations)** - POST /products/process with body items[].ean - GET /products/process/{id} - Envelope: HTTP 200 { data: { process_id, … } } (and completed items[]) - Matches legacy Descrybe /api/v1/products/process - Handler also accepts raw_product_ids as an alternate body on this path - Plan gates (credits / product limit / AI / EPREL / feature flags) run before EnsureRaw catalog writes on the items[].ean path; blocked starts return HTTP 402 2. **Internal / dashboard-style jobs** - POST /process with raw_product_ids (same body as POST /api/processing/jobs) - GET /process, GET /process/{id}, cancel/terminate/retry - Flat JSON (no data wrapper); 202 Accepted on start/retry - Plan gates return HTTP 402 with PlanGateError (error + code + upgrade_url) ### Dual IDs (do not confuse) - GET /products data[].id = processed_products.id (enriched row) - GET /products data[].raw_product_id = raw_products.id (use this for raw_product_ids) - POST ... raw_product_ids[] must be raw_products.id — never PresentProduct.id - GET /products/process/{id} COMPLETED items[].id = processed_products.id (legacy); additive processed_product_id (same as id) and raw_product_id (raw_products.id) Note: Dashboard JSON under /api/* uses session cookies + CSRF and is separate from this public API-key surface. Other legacy path aliases (/categories/create, /attributes/create, /campaigns) appear next to canonical paths. servers: - url: https://descrybe.io/api/v1 description: Production (Descrybe-hosted) - url: http://localhost:28472/api/v1 description: Local web (Vite proxy to Go API) - url: http://localhost:28471/api/v1 description: Local API (default Go listen address) security: - BearerAuth: [] - ApiKeyAuth: [] tags: - name: Health - name: Products - name: Categories - name: Attributes - name: Feeds - name: Export feeds - name: Campaigns description: Seasonal content calendar (legacy /campaigns aliases) - name: Processing - name: Team description: Dashboard session routes under /api (not API-key) - name: Admin description: Platform-admin dashboard routes under /api (not API-key) paths: /health: get: tags: - Health security: [] summary: Liveness (same payload as /healthz) description: | Returns process liveness plus cutover flags (maintenance, read_only, hypercare). Host-level probes /healthz and /readyz share the same flag fields; /readyz additionally reports database checks. responses: '200': description: Liveness OK. No API key required. Same flag fields as host /healthz (status, service, maintenance, read_only, hypercare). content: application/json: schema: $ref: "#/components/schemas/HealthStatus" example: status: ok service: api maintenance: false read_only: false hypercare: false '500': description: Unexpected failure building the health payload (rare process fault). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: health check failed /openapi.yaml: get: tags: - Health security: [] summary: This OpenAPI document responses: '200': description: This OpenAPI document. Served with Cache-Control and ETag; gzip when Accept-Encoding allows. content: application/yaml: schema: type: string example: | openapi: 3.0.3 info: title: Descrybe Public API version: "1.0" '304': description: Not Modified — request If-None-Match matched the document ETag. Empty body. '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error /products: get: tags: - Products summary: List products description: | Legacy-compatible public list. Envelope is { data, meta } (not flat products/offset). Query params match legacy: page, limit (default 25), status, search, sortBy, sortOrder, feedId. Each row matches PresentProduct (id = processed_products.id, raw_product_id = raw_products.id, product_id, name, category, status, feed_id, quality_score, quality_grade, created_at, updated_at). For POST /products/process or POST /process dual-mode bodies, pass raw_product_id — not id. parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/LegacyLimit" - in: query name: status schema: type: string enum: - all - needs_review - processed - completed - error - processing - unprocessed default: all description: | Filter by product status. all (default) returns every status. needs_review also matches legacy status processed (pre-P0-8 AI review queue). Use completed after Accept enrichment. - in: query name: search schema: type: string maxLength: 200 description: | Free-text search over product name / product_id. Alias q is also accepted. Omit or empty to skip text filtering. Max ~200 characters. - in: query name: sortBy schema: type: string enum: - updatedAt - createdAt - name default: updatedAt description: | Sort column for the product list. One of updatedAt (default), createdAt, or name. - in: query name: sortOrder schema: type: string enum: - asc - desc default: desc description: | Sort direction. asc or desc (default desc). Combined with sortBy. - in: query name: feedId schema: type: string format: uuid description: | Restrict results to products from this input feed. UUID format. Alias feed_id is also accepted. Omit to include all feeds. responses: '200': description: "Paged processed products for the API-key company. Returned after ListProcessedProductsDetailed\ \ succeeds. Envelope is data[] + meta (page, limit, total, totalPages)." content: application/json: schema: $ref: "#/components/schemas/ProductListResponse" example: data: - id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 product_id: SKU-1001 name: Wireless earbuds category: electronics/audio status: completed feed_id: 22222222-2222-2222-2222-222222222222 quality_score: 72 quality_grade: C created_at: '2026-08-01T10:15:00Z' updated_at: '2026-08-03T14:22:11Z' meta: page: 1 limit: 25 total: 1284 totalPages: 52 '400': description: Client-facing catalog validation on filters (for example an invalid feed id). handleV1ListProducts uses v1Err validation_error. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: invalid feed id '500': description: Unexpected database/list failure in handleV1ListProducts. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_error message: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized /products/quality: get: tags: - Products summary: List product quality scores description: | Legacy-compatible quality listing with { data, meta }. Defaults status=completed. Optional min_score filters rows after scoring. parameters: - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/LegacyLimit" - in: query name: status schema: type: string enum: - all - needs_review - processed - completed - error - processing - unprocessed default: completed description: | Filter by product status. Default completed (quality scores are most useful after enrichment). all returns every status. Same values as GET /products status. - in: query name: search schema: type: string maxLength: 200 description: | Free-text search over product name / product_id. Alias q also accepted. Omit to skip. - in: query name: min_score schema: type: integer minimum: 0 maximum: 100 description: | Minimum quality score (0-100 inclusive). Rows below this value are excluded after scoring. Omit for no score floor. - in: query name: feedId schema: type: string format: uuid description: | Restrict to products from this input feed UUID. Alias feed_id also accepted. responses: '200': description: Quality rows for processed products (default status=completed). Returned after list+score. min_score filters in-process after scoring. Envelope data + meta (page, limit, total). content: application/json: schema: $ref: "#/components/schemas/ProductQualityListResponse" example: data: - id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 product_id: SKU-1001 name: Wireless earbuds quality_score: 72 quality_grade: C quality_checks: title: true description: true attributes: false meta: page: 1 limit: 25 total: 410 '400': description: Query min_score is present but not an integer. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: invalid min_score '500': description: Unexpected list failure in handleV1ListProductQuality. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_error message: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized /products/reset: post: tags: - Products summary: Reset products to unprocessed requestBody: required: true content: application/json: schema: type: object required: - product_ids properties: product_ids: type: array minItems: 1 items: type: string format: uuid description: | Processed (or raw, when kind=raw) product UUIDs to return to unprocessed. Non-empty array of UUID strings. kind: type: string enum: - processed - raw default: processed description: | Which table the ids refer to. processed (default) resets enriched products; raw targets raw_products rows instead. example: product_ids: - a1b2c3d4-e5f6-7890-abcd-ef1234567890 - b2c3d4e5-f6a7-8901-bcde-f12345678901 kind: processed responses: '200': description: Selected products returned to unprocessed. Returned when ResetProductsToUnprocessed commits. Requires company-admin capability (API keys use role api and pass). content: application/json: schema: type: object required: - success - reset_count - message properties: success: type: boolean reset_count: type: integer message: type: string example: success: true reset_count: 12 message: 12 product(s) returned to unprocessed state '400': description: Invalid JSON, invalid product_ids UUID, empty product_ids, over max batch, or catalog.ClientError from reset. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: product_ids is required '403': description: Caller role is neither admin nor api (requireCompanyAdmin). Valid company API keys use role api and do not hit this. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '500': description: Unexpected reset failure after validation. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not reset products '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /products/{id}: parameters: - $ref: "#/components/parameters/ID" get: tags: - Products summary: Get processed product description: | Shared dashboard handler — flat ProcessedProduct JSON (not a legacy { data } envelope). List endpoints use { data, meta }. responses: '200': description: Single processed product as a flat object (no data wrapper). Returned when GetProcessedProduct finds the id for this company. content: application/json: schema: $ref: "#/components/schemas/ProcessedProduct" example: id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 product_id: SKU-1001 name: Wireless earbuds status: completed '400': description: Path id is not a UUID. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '404': description: Product not found for this company (or wrong tenant). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json patch: tags: - Products summary: Update processed product description: "Flat ProcessedProduct JSON (not a legacy { data } envelope)." requestBody: content: application/json: schema: type: object description: Partial update — only send fields to change properties: processed_name: type: string description: Enriched display title after AI/manual edit processed_description: type: string description: Enriched long description text status: type: string enum: - needs_review - processed - completed - error - processing - unprocessed description: Product workflow status. Use completed after accepting enrichment. category: type: string description: Category path or unique_id string stored on the product attributes: type: object additionalProperties: true description: Source/raw attribute map (string keys to scalar or list values) processed_attributes: type: object additionalProperties: true description: Enriched attribute map after processing example: processed_name: Sony WH-1000XM5 Wireless Noise Cancelling Headphones Black status: completed attributes: color: Black brand: Sony battery_life_hours: '30' responses: '200': description: Product updated. Flat processed product JSON when UpdateProcessedProduct succeeds. content: application/json: schema: $ref: "#/components/schemas/ProcessedProduct" example: id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 product_id: SKU-1001 name: Wireless earbuds Pro status: completed '400': description: Invalid JSON, invalid id, or catalog.ClientError / ClientOrLog on update. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not update product '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '404': description: Resource id not found for this API-key company (or wrong tenant). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /products/process: post: tags: - Processing summary: Start processing by EAN (legacy public contract) description: | Legacy Descrybe public API (handleV1StartProcess). Upserts raw products from items[].ean (GTIN), enqueues one processing job, returns HTTP 200 with a data envelope. Primary body: items[].ean. Alternate body on the same handler: raw_product_ids (raw_products.id UUID list — not GET /products data[].id). Prefer items for public integrations. On items[].ean, assertV1ProcessGates runs before EnsureRaw so plan/credit failures cannot spam catalog writes (HTTP 402 legacy coded envelope). Not an alias of POST /process (flat ProcessingJob / 202). Do not mix envelopes. requestBody: $ref: "#/components/requestBodies/LegacyStartProcessByEAN" responses: '200': description: "Legacy process job accepted. Always HTTP 200 (not 202) with data.process_id when\ \ enqueue succeeds. Prefer items[].ean; raw_product_ids alternate body is accepted on the\ \ same path." content: application/json: schema: $ref: "#/components/schemas/LegacyProcessStartEnvelope" example: data: process_id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa status: PENDING processing_type: full '400': description: Missing body, invalid processing_type, items without ean, invalid raw_product_id, empty items, or other validation_error from handleV1StartProcess / v1ErrFromProcessing. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: '''items'' array is required' '402': description: | Plan gate blocked starting processing (billing credits/limits/AI/EPREL/feature flags). v1ErrFromProcessing maps these to HTTP 402 with a coded legacy envelope. Codes: insufficient_credits, product_limit, ai_requires_upgrade, eprel_requires_upgrade, plan_gate (message feature_disabled when the platform feature flag is off). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" examples: insufficient_credits: summary: Credits value: error: code: insufficient_credits message: Insufficient credits feature_disabled: summary: Feature flag value: error: code: plan_gate message: feature_disabled "429": { $ref: "#/components/responses/TooManyRequests" } '500': description: Enqueue or unexpected internal failure starting the legacy job. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_server_error message: Internal server error '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /products/process/{id}: get: tags: - Processing summary: Processing job status by process_id (legacy public contract) description: | Poll legacy job status. Path param is process_id from POST /products/process. Completed jobs return items[] (EAN-keyed enrichment). In-progress and failed jobs omit items. Not the same shape as GET /process/{id} (flat ProcessingJob). On COMPLETED items, id is the processed_products UUID (legacy). Additive aliases: processed_product_id (same value as id), raw_product_id (raw_products.id), and name (same value as title) for dual-mode clients / scorecards. parameters: - name: id in: path required: true schema: type: string format: uuid description: | process_id returned by POST /products/process. UUID format. Required. responses: '200': description: "Legacy job poll. Returned when the job exists for this company. status is uppercase;\ \ items[] appear when status is COMPLETED." content: application/json: schema: $ref: "#/components/schemas/LegacyProcessStatusEnvelope" examples: completed: summary: Completed A1-shaped item value: data: status: COMPLETED process_id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa processing_type: full items: - ean: '8606019604493' id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb processed_product_id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb raw_product_id: cccccccc-cccc-cccc-cccc-cccccccccccc status: processed category: '50' category_name: Cookers title: VOX electric cooker EHT 6020 WG name: VOX electric cooker EHT 6020 WG meta_title: VOX electric cooker EHT 6020 WG | 50 meta_description: Affordable electric cooker with four plates and a 65 L fan oven. description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven." attributes: brand: Vox product_model: EHT6020WG main_image: https://images.example.com/products/vox-eht6020wg.jpg more_images: null eprel: id: "1234567" label: https://eprel.ec.europa.eu/label/Example pdf: https://eprel.ec.europa.eu/fiches/Example.pdf energy_class: A energy_scale: A-G total_items: 1 processed_at: '2026-08-04T10:04:12Z' processing: summary: In progress value: data: status: PROCESSING process_id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa processing_type: full '400': description: Path id is not a UUID. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: invalid id '404': description: No processing job with this id for the API-key company. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: not_found message: Processing job not found '500': description: Unexpected failure loading job status. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_server_error message: Internal server error '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /categories: get: tags: - Categories summary: List categories description: "Legacy public contract — { data, meta } with page/limit pagination." parameters: - name: page in: query schema: type: integer minimum: 1 default: 1 description: | 1-based page index. Integer, default 1, minimum 1. - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 25 description: | Page size. Integer, default 25, minimum 1, maximum 100. - in: query name: search schema: type: string maxLength: 200 description: | Free-text search over category name / unique_id. Alias of q; either may be sent. - in: query name: q schema: type: string maxLength: 200 description: | Canonical search query (same as search). Omit both to return the full page. responses: '200': description: "Paged categories as data[] + meta. Returned after ListCategories succeeds." content: application/json: schema: $ref: "#/components/schemas/LegacyCategoriesResponse" example: data: - id: 33333333-3333-3333-3333-333333333333 unique_id: electronics name: Electronics created_at: '2026-07-01T08:00:00Z' updated_at: '2026-07-15T12:00:00Z' meta: page: 1 limit: 25 total: 42 totalPages: 2 '500': description: Unexpected list failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized post: tags: - Categories summary: Create category description: Requires name and unique_id. parent_id is accepted as an alias of parent_unique_id. requestBody: $ref: "#/components/requestBodies/CreateCategory" responses: '201': description: Category created. HTTP 201 with data containing id, unique_id, name. content: application/json: schema: $ref: "#/components/schemas/LegacyCategoryCreateResponse" example: data: id: 33333333-3333-3333-3333-333333333333 unique_id: electronics name: Electronics '400': description: Invalid JSON, missing name/unique_id, duplicate unique_id, bad parent, or other catalog.ClientError from CreateCategory. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not create category '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /categories/create: post: tags: - Categories summary: Create category (legacy alias) description: "Alias of POST /categories. Same body and { data } response." requestBody: $ref: "#/components/requestBodies/CreateCategory" responses: '201': description: Category created. HTTP 201 with data containing id, unique_id, name. content: application/json: schema: $ref: "#/components/schemas/LegacyCategoryCreateResponse" example: data: id: 33333333-3333-3333-3333-333333333333 unique_id: electronics name: Electronics '400': description: Invalid JSON, missing name/unique_id, duplicate unique_id, bad parent, or other catalog.ClientError from CreateCategory. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not create category '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /categories/{id}: parameters: - name: id in: path required: true schema: type: string description: | For GET/PATCH — category UUID. For DELETE — category unique_id slug (e.g. electronics). Required. Format depends on the method. get: tags: - Categories summary: Get category by UUID description: "Flat category JSON (not a legacy { data } envelope). Path id must be the category\ \ UUID." responses: '200': description: Category detail (flat CategoryDetail) when found. Path id is typically unique_id for v1 delete; shared get handler accepts the mounted id param. content: application/json: schema: $ref: "#/components/schemas/CategoryDetail" example: id: 33333333-3333-3333-3333-333333333333 unique_id: electronics name: Electronics '400': description: Invalid category id. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '404': description: Category not found for this company. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected get failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: get failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json patch: tags: - Categories summary: Update category by UUID description: "Flat updated category JSON (not a legacy { data } envelope)." requestBody: content: application/json: schema: type: object description: Partial update — only send fields to change properties: name: type: string description: Display name shown in the catalog UI unique_id: type: string description: | Stable slug identifier (lowercase letters, digits, underscores/hyphens). Changing it may break feed mappings that reference the old id. parent_unique_id: type: string nullable: true description: Parent category unique_id, or null for a root category description: type: string nullable: true description: Optional human-readable category description is_active: type: boolean description: When false, category is hidden from active catalog selection position: type: integer description: Sort order among siblings (lower first). Non-negative integer. example: name: Consumer Electronics description: Updated root for consumer devices is_active: true position: 0 responses: '200': description: Category updated (flat object) when PATCH succeeds. content: application/json: schema: $ref: "#/components/schemas/CategoryDetail" example: id: 33333333-3333-3333-3333-333333333333 unique_id: electronics name: Consumer Electronics '400': description: Invalid JSON or catalog client error. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '404': description: Category not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json delete: tags: - Categories summary: Delete category by unique_id description: Path id is the category unique_id (legacy public contract). responses: '200': description: Category deleted by unique_id path param. Returned when DeleteCategoryByUniqueID succeeds. Envelope data.message. content: application/json: schema: $ref: "#/components/schemas/LegacySuccessMessage" example: data: message: Category deleted successfully '400': description: Empty unique_id or catalog.ClientError blocking delete. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid category id '404': description: No category with this unique_id for the company. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected delete failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: delete failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /attributes: get: tags: - Attributes summary: List attributes description: "Legacy public contract — { data, meta }." parameters: - name: page in: query schema: type: integer minimum: 1 default: 1 description: | 1-based page index. Integer, default 1, minimum 1. - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 25 description: | Page size. Integer, default 25, minimum 1, maximum 100. - in: query name: search schema: type: string maxLength: 200 description: | Free-text search over attribute name / key. Alias of q. - in: query name: q schema: type: string maxLength: 200 description: | Canonical search query (same as search). Omit both for an unfiltered page. - in: query name: categoryId schema: type: string description: | Filter by category unique_id slug (e.g. electronics), not UUID. - in: query name: sortBy schema: type: string enum: - name - attributeKey - updatedAt default: updatedAt description: | Sort column. name, attributeKey, or updatedAt (default). - in: query name: sortOrder schema: type: string enum: - asc - desc default: desc description: | Sort direction. asc or desc (default desc). responses: '200': description: "Paged attributes as data[] + meta (presentV1Attribute)." content: application/json: schema: $ref: "#/components/schemas/LegacyAttributesResponse" example: data: - id: 44444444-4444-4444-4444-444444444444 key: color name: Color type: text unit: "" required: false category_unique_id: electronics created_at: '2026-07-01T08:00:00Z' updated_at: '2026-07-15T12:00:00Z' meta: page: 1 limit: 25 total: 18 totalPages: 1 '500': description: Unexpected list failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized post: tags: - Attributes summary: Create attribute description: Requires name, attribute_key, value_type, and category_unique_id; links the attribute to that category. requestBody: $ref: "#/components/requestBodies/CreateAttribute" responses: '201': description: Attribute created (and linked when category_unique_id resolves). HTTP 201 data envelope. content: application/json: schema: $ref: "#/components/schemas/LegacyAttributeCreateResponse" example: data: id: 44444444-4444-4444-4444-444444444444 key: color name: Color type: text category_unique_id: electronics '400': description: Invalid JSON, missing required fields, or catalog.ClientError on create/link. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: "Missing required fields: name, attribute_key, value_type, category_unique_id" '404': description: Link target category was not found (when link step maps to not found). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected create failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not create attribute '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /attributes/create: post: tags: - Attributes summary: Create attribute (legacy alias) description: Alias of POST /attributes. requestBody: $ref: "#/components/requestBodies/CreateAttribute" responses: '201': description: Attribute created (and linked when category_unique_id resolves). HTTP 201 data envelope. content: application/json: schema: $ref: "#/components/schemas/LegacyAttributeCreateResponse" example: data: id: 44444444-4444-4444-4444-444444444444 key: color name: Color type: text category_unique_id: electronics '400': description: Invalid JSON, missing required fields, or catalog.ClientError on create/link. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: "Missing required fields: name, attribute_key, value_type, category_unique_id" '404': description: Link target category was not found (when link step maps to not found). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected create failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not create attribute '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /attributes/{id}: parameters: - $ref: "#/components/parameters/ID" patch: tags: - Attributes summary: Update attribute description: Flat attribute JSON (attribute_key / value_type fields — not legacy key/type aliases). requestBody: content: application/json: schema: type: object description: Partial update — only send fields to change properties: name: type: string description: Human-readable attribute label shown in the UI value_type: type: string enum: [string, number, list, multiselect] description: | Value shape for this attribute. string, number, list, or multiselect. unit: type: string nullable: true description: Optional unit label (e.g. W, cm). Null clears the unit. example: type: string nullable: true description: Sample value for docs/UI hints (e.g. Black) example: name: Color value_type: string example: Black responses: '200': description: Attribute updated when PATCH succeeds (flat AttributeDetail / shared handler). content: application/json: schema: $ref: "#/components/schemas/AttributeDetail" example: id: 44444444-4444-4444-4444-444444444444 attribute_key: color name: Colour '400': description: Invalid JSON or catalog client error. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '404': description: Attribute not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json delete: tags: - Attributes summary: Delete attribute by UUID responses: '200': description: Attribute deleted by UUID. Returned when DeleteAttribute succeeds. data.message envelope. content: application/json: schema: $ref: "#/components/schemas/LegacySuccessMessage" example: data: message: Attribute deleted successfully '400': description: Path id is not a UUID. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid attribute id '404': description: Attribute not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected delete failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: delete failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /feeds: get: tags: - Feeds summary: List input feeds description: | Legacy public contract — { data: Feed[], meta: { page, limit, total } }. Accepts page+limit (legacy defaults page=1, limit=25, max 100) or limit+offset. Each feed includes presentFeed fields plus dual-support v2 keys (feed_type, sync_interval_minutes, options). parameters: - name: page in: query schema: type: integer minimum: 1 default: 1 description: | 1-based page index for legacy page+limit mode. Integer, default 1, minimum 1. - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 25 description: | Page size. Integer, default 25, minimum 1, maximum 100. Also used with offset. - $ref: "#/components/parameters/Offset" - in: query name: q schema: type: string maxLength: 200 description: | Free-text search over feed name / URL. Canonical search param. - in: query name: search schema: type: string maxLength: 200 description: | Alias of q. Same free-text search over feed name / URL. responses: '200': description: Paged feeds with meta (including active_total, mapped_total). Returned after Feeds.List. content: application/json: schema: $ref: "#/components/schemas/FeedListResponse" example: data: - id: 22222222-2222-2222-2222-222222222222 name: Main XML feed url: https://supplier.example/feed.xml item_path: products/product is_active: true meta: page: 1 limit: 25 total: 3 totalPages: 1 offset: 0 active_total: 2 mapped_total: 1 '500': description: Unexpected list failure (v1Err). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_error message: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized post: tags: - Feeds summary: Create feed description: | Legacy body requires name + item_path (url optional). Dual-support also accepts feed_type, sync_interval_minutes, sync_frequency (hours), and multipart file uploads. requestBody: content: application/json: schema: type: object required: [name, item_path] properties: name: type: string description: Display name for this input feed (required) item_path: type: string description: | XML item xpath / path stored in options.item_path (e.g. channel/item). Required. url: type: string format: uri nullable: true description: | HTTPS (or HTTP) URL of the remote feed. Nullable for upload-only feeds. feed_type: type: string enum: - xml - csv description: Source format. xml (default) or csv. sync_interval_minutes: type: integer minimum: 1 default: 60 description: | How often automatic sync should run, in minutes. Default 60. Minimum 1. sync_frequency: type: integer minimum: 1 description: | Legacy interval in hours. Converted to minutes when sync_interval_minutes is unset. is_active: type: boolean description: | When true, status is set to active after create; when false/omitted, typically unmapped. example: name: Nordic Webshop Google Merchant XML item_path: channel/item url: https://feeds.example.com/nordic/google-merchant.xml feed_type: xml sync_interval_minutes: 60 is_active: true responses: '201': description: Feed created from JSON or multipart. HTTP 201 data envelope when Create succeeds. is_active is honored for UUID ids. content: application/json: schema: $ref: "#/components/schemas/FeedCreateResponse" example: data: id: 22222222-2222-2222-2222-222222222222 name: Main XML feed url: https://supplier.example/feed.xml item_path: products/product is_active: true '400': description: Invalid JSON/multipart, missing name/item_path (or name+url), upload failure, or feeds.ClientError. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: "Missing required fields: name, item_path" '500': description: Unexpected create failure. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_error message: could not create feed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /feeds/{id}: parameters: - $ref: "#/components/parameters/ID" get: tags: - Feeds summary: Get feed description: "Legacy envelope — { data: Feed }." responses: '200': description: Single feed in data envelope when Feeds.Get succeeds for this company. content: application/json: schema: $ref: "#/components/schemas/FeedGetResponse" example: data: id: 22222222-2222-2222-2222-222222222222 name: Main XML feed url: https://supplier.example/feed.xml item_path: products/product is_active: true '400': description: Path id is not a UUID. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: Invalid id '404': description: Feed not found for this company. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: not_found message: Not found '500': description: Unexpected get failure. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_error message: get failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json patch: tags: - Feeds summary: Update feed description: "Returns flat PresentFeed JSON (same shape as data in GET /feeds/{id}, without the\ \ data wrapper)." requestBody: content: application/json: schema: type: object description: Partial update — only send fields to change properties: name: type: string description: Display name for this input feed url: type: string format: uri nullable: true description: Remote feed URL, or null to clear item_path: type: string description: XML item xpath / path (stored in options.item_path) feed_type: type: string enum: - xml - csv description: Source format. xml or csv. status: type: string description: | Feed lifecycle status (e.g. active, unmapped, error). Handler validates allowed values. sync_interval_minutes: type: integer minimum: 1 description: Automatic sync interval in minutes. Minimum 1. sync_frequency: type: integer minimum: 1 description: | Legacy interval in hours. Converted to minutes when sync_interval_minutes is unset. example: name: Nordic Webshop Google Merchant XML (EU) sync_interval_minutes: 120 status: active responses: '200': description: Feed updated. Flat PresentFeed from shared handleUpdateFeed. content: application/json: schema: $ref: "#/components/schemas/PresentFeed" example: id: 22222222-2222-2222-2222-222222222222 name: Main XML feed is_active: false '400': description: Invalid id/JSON or feeds.ClientError. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '404': description: Feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json delete: tags: - Feeds summary: Delete feed description: "Company-admin only. Flat { id, deleted: true } (not a legacy message envelope)." responses: '200': description: Feed deleted. Flat FeedDeleted when handleDeleteFeed succeeds. Requires company-admin capability (API keys pass as role api). content: application/json: schema: $ref: "#/components/schemas/FeedDeleted" example: id: 22222222-2222-2222-2222-222222222222 deleted: true '400': description: Invalid feed UUID. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '403': description: requireCompanyAdmin rejected the caller (session non-admin). API keys pass. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '404': description: Feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected delete failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: delete failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /feeds/{id}/sync: post: tags: - Feeds summary: Trigger feed sync description: | Legacy contract — HTTP 200 { data: { jobId } } (not 202). Enqueues a durable feed_sync_jobs row and wakes the worker via NOTIFY; the API process does not run sync work. Dual-support also returns job_id. Dashboard POST /api/feeds/{id}/sync uses HTTP 202 with a flat job object — do not mix envelopes. parameters: - $ref: "#/components/parameters/ID" responses: "200": description: Sync job durably enqueued. Returned when EnqueueSync succeeds. data.jobId (+ job_id alias). content: application/json: schema: $ref: "#/components/schemas/FeedSyncResponse" example: data: jobId: 55555555-5555-5555-5555-555555555555 job_id: 55555555-5555-5555-5555-555555555555 '400': description: Invalid id or feeds.ClientError (inactive feed, bad source, …). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: Invalid id '404': description: Feed not found. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: not_found message: Feed not found '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid id legacy: summary: v1Err coded value: error: code: validation_error message: Invalid id '500': description: Failed to create sync job. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_error message: Failed to create sync job '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '429': description: Rate limit exceeded for heavy mutations (30/min/company). Retry-After 60. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: rate limit exceeded /feeds/{id}/mappings: parameters: - $ref: "#/components/parameters/ID" get: tags: - Feeds summary: Get feed mappings description: "Active feed_mappings row, or { mappings: [] } when none exist." responses: '200': description: Active feed_mappings document for the feed, or an empty mappings payload when none exist. content: application/json: schema: $ref: "#/components/schemas/FeedMappings" example: mappings: title: name ean: gtin '400': description: Invalid feed UUID. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '404': description: Feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected mappings read failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json put: tags: - Feeds summary: Put feed mappings description: Replace active mappings (bumps version). requestBody: required: true content: application/json: schema: type: object required: - mappings properties: mappings: description: Mapping document (object or array) example: mappings: item_path: channel/item fields: - source: g:id target: product_id - source: title target: name - source: g:gtin target: gtin - source: g:brand target: brand - source: g:image_link target: main_image responses: '200': description: Mappings replaced when PUT body validates and save succeeds. content: application/json: schema: $ref: "#/components/schemas/FeedMappings" example: mappings: title: name ean: gtin '400': description: Invalid id/JSON or mapping validation error. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '404': description: Feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /feeds/{id}/extract-schema: post: tags: - Feeds summary: Extract feed schema sample paths and preview description: Samples the feed source and returns discovered field paths plus a short preview. parameters: - $ref: "#/components/parameters/ID" requestBody: required: false content: application/json: schema: type: object properties: item_path: type: string description: Optional XML item path hint; inferred when omitted example: item_path: channel/item responses: '200': description: Sample schema/fields extracted from the feed source. content: application/json: schema: $ref: "#/components/schemas/SchemaExtractResult" example: fields: - name - gtin - price item_path: products/product '400': description: Invalid id or extract client error. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '404': description: Feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected extract failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: extract failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json "429": { $ref: "#/components/responses/TooManyRequests" } /feeds/{id}/sync-process-sample: post: tags: - Feeds summary: Sync feed then process a sample of raw products description: | Optionally syncs the feed, then starts dashboard-style processing for up to N raw products (default 10, max 100). Flat JSON; HTTP 202 when a job is queued, 200 when sync finishes with no products. parameters: - $ref: "#/components/parameters/ID" requestBody: required: false content: application/json: schema: type: object properties: limit: type: integer minimum: 1 maximum: 100 default: 10 description: | Max raw products to queue after sync. Integer, default 10, minimum 1, maximum 100. skip_sync: type: boolean default: false description: | When true, skip the feed sync step and process existing raw products only. processing_type: type: string default: full description: | Pipeline mode — full (default) or a single step name (category, title, description, attributes). example: limit: 10 skip_sync: false processing_type: full responses: '200': description: Sample sync finished but produced no raw products to process (sync completed empty). content: application/json: schema: type: object example: ok: true imported: 0 message: no products '202': description: Sample sync imported products and queued processing (async accept). content: application/json: schema: type: object example: accepted: true job_id: 55555555-5555-5555-5555-555555555555 imported: 5 '400': description: Invalid id or sample validation error. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '404': description: Feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected sample failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: sample failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json "429": { $ref: "#/components/responses/TooManyRequests" } /export-feeds: get: tags: - Export feeds summary: List export feeds description: | Legacy envelope — { data: ExportFeed[], meta: { page, limit, total } }. Accepts page+limit (legacy defaults page=1, limit=25, max 100) or limit+offset. parameters: - name: page in: query schema: type: integer default: 1 minimum: 1 description: | 1-based page index for legacy page+limit mode. Integer, default 1, minimum 1. - $ref: "#/components/parameters/LegacyLimit" - $ref: "#/components/parameters/Offset" responses: '200': description: "Paged export feeds as data[] + meta (presentV1ExportFeed rows)." content: application/json: schema: type: object required: - data - meta properties: data: type: array items: $ref: "#/components/schemas/ExportFeedDetail" meta: type: object example: data: - id: 66666666-6666-6666-6666-666666666666 name: Google Shopping format: xml is_active: true meta: page: 1 limit: 25 total: 1 '500': description: Unexpected list failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized post: tags: - Export feeds summary: Create export feed description: "Legacy envelope — { data: ExportFeed }." requestBody: content: application/json: schema: type: object required: - name - format properties: name: type: string description: Display name for the export feed (required) format: type: string enum: - xml - csv description: Output format. xml or csv (required). source_feed_id: type: string format: uuid description: Optional input feed UUID this export is derived from template: type: object description: | Export template document (root/item/mappings). Structure depends on format. structure: type: object description: Legacy alias for template — same object shape mappings: type: object description: Field mapping object (source → target). May also live under template. filters: type: object description: | Product filters applied at generate time (e.g. statuses list). Object map. root_xpath: type: string description: Optional XML root xpath hint for template builders item_xpath: type: string description: Optional XML item xpath hint for template builders example: name: Warehouse Inventory CSV format: csv source_feed_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6 filters: statuses: - completed mappings: title: processed_name gtin: gtin responses: '201': description: Export feed created when name+format validate. HTTP 201 data envelope. content: application/json: schema: type: object required: - data properties: data: $ref: "#/components/schemas/ExportFeedDetail" example: data: id: 66666666-6666-6666-6666-666666666666 name: Google Shopping format: xml '400': description: "Invalid JSON, missing name/format, or feeds.ClientError. Note: ClientOrLog fallback\ \ also uses flat Error on some paths." content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: "Missing required fields: name, format" '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /export-feeds/{id}: parameters: - $ref: "#/components/parameters/ID" get: tags: - Export feeds summary: Get export feed description: Flat export feed row including template/filters (not the list presentV1ExportFeed / data envelope). responses: '200': description: Export feed detail when found. content: application/json: schema: $ref: "#/components/schemas/ExportFeedDetail" example: id: 66666666-6666-6666-6666-666666666666 name: Google Shopping format: xml '400': description: Invalid UUID. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '404': description: Export feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected get failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: get failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json patch: tags: - Export feeds summary: Update export feed description: Flat updated export feed row. requestBody: content: application/json: schema: type: object description: Partial update — only send fields to change properties: name: type: string description: Display name for the export feed is_active: type: boolean description: When false, public token URLs may still exist but feed is inactive template: type: object description: Export template document (root/item/mappings) filters: type: object description: Product filters applied at generate time example: name: Google Shopping XML EU is_active: true filters: statuses: - completed responses: '200': description: Export feed updated. content: application/json: schema: $ref: "#/components/schemas/ExportFeedDetail" example: id: 66666666-6666-6666-6666-666666666666 name: Google Shopping EU format: xml '400': description: Invalid JSON or client error. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '404': description: Export feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json delete: tags: - Export feeds summary: Delete export feed description: "Company-admin only. Flat { status: ok } (not LegacySuccessMessage)." responses: '200': description: Export feed deleted (admin capability; API keys pass). Flat deleted marker. content: application/json: schema: $ref: "#/components/schemas/FeedDeleted" example: id: 66666666-6666-6666-6666-666666666666 deleted: true '400': description: Invalid UUID. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '403': description: requireCompanyAdmin rejected session non-admin. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '404': description: Export feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected delete failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: delete failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /export-feeds/{id}/template: put: tags: - Export feeds summary: Update export feed template description: Updates template and/or filters; returns flat ExportFeedDetail. parameters: - $ref: "#/components/parameters/ID" requestBody: content: application/json: schema: type: object properties: template: type: object description: | Full export template document (root, item path, field mappings). Replaces prior template when set. filters: type: object description: | Product filters for generation (e.g. statuses). Replaces prior filters when set. example: template: root: rss item: channel/item mappings: title: processed_name gtin: gtin filters: statuses: - completed responses: '200': description: Template saved for the export feed. content: application/json: schema: $ref: "#/components/schemas/ExportFeedDetail" example: id: 66666666-6666-6666-6666-666666666666 name: Google Shopping format: xml '400': description: Invalid id/body or template validation. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '404': description: Export feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /export-feeds/{id}/rotate-token: post: tags: - Export feeds summary: Rotate public export URL token description: | Replaces public_token so the previous /api/public/export-feeds/{token}.{xml|csv} URL stops working immediately. Requires company admin (or platform staff). Response is a flat ExportFeedDetail including the new public_token. parameters: - $ref: "#/components/parameters/ID" responses: '200': description: Token rotated; body includes the new public_token and URLs. content: application/json: schema: $ref: "#/components/schemas/ExportFeedDetail" '404': description: Export feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" '403': description: Admin required. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required "400": { $ref: "#/components/responses/BadRequest" } "422": { $ref: "#/components/responses/ValidationError" } "500": { $ref: "#/components/responses/InternalServerError" } "429": { $ref: "#/components/responses/TooManyRequests" } /export-feeds/{id}/generate: post: tags: - Export feeds summary: Generate export feed file description: | Legacy envelope — { data: { generated, format, filePath, downloadUrl, ... } }. Content is streamed live via public download URL (not persisted to disk). parameters: - $ref: "#/components/parameters/ID" responses: '200': description: Generation finished. data includes generated, format, downloadUrl, products_exported, status. filePath may be null — documented under examples.value for RapiDoc safety. content: application/json: schema: type: object required: - data properties: data: type: object examples: ok: summary: Generated value: data: generated: true format: xml filePath: null downloadUrl: /api/public/export-feeds/tok_abc.xml products_exported: 1284 last_generated_at: '2026-08-04T12:00:00Z' status: ready '400': description: Invalid id or generation_failed client error (empty template, …). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: generation_failed message: template is empty '404': description: Export feed not found. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: not_found message: Export feed not found '500': description: Generation failed internally. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: generation_failed message: Failed to generate export feed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json "429": { $ref: "#/components/responses/TooManyRequests" } /export-feeds/{id}/export-products: post: tags: - Export feeds summary: Export selected processed products description: | Streams the rendered export for the given processed product UUIDs. Response body is CSV or XML bytes (not JSON). Headers include Content-Disposition and X-Products-Exported. parameters: - $ref: "#/components/parameters/ID" requestBody: required: true content: application/json: schema: type: object required: - product_ids properties: product_ids: type: array minItems: 1 items: type: string format: uuid description: | Processed product UUIDs to include in the streamed export. Non-empty array. example: product_ids: - 2c5ea4c0-4067-4e44-8c5a-9a8b7c6d5e4f - 550e8400-e29b-41d4-a716-446655440001 responses: '200': description: Selected products exported / file bytes produced for this export feed. content: application/json: schema: type: object example: exported: 25 skipped: 2 '400': description: Invalid id/body or selection validation. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '404': description: Export feed not found. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '500': description: Unexpected export failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: export failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json "429": { $ref: "#/components/responses/TooManyRequests" } /campaigns: get: tags: - Campaigns summary: List seasonal campaign presets (legacy alias) description: | Alias of GET /marketing/calendar. Returns legacy envelope { data: { year, presets, prepared } }. Not email campaigns. parameters: - name: year in: query schema: type: integer example: 2026 minimum: 2000 maximum: 2100 description: | Calendar year for seasonal presets (e.g. 2026). Integer. When omitted, server uses the current year. responses: '200': description: Legacy alias of marketing calendar. data envelope around calendar payload (handleV1ListCampaigns → v1OK). content: application/json: schema: $ref: "#/components/schemas/MarketingCalendar" example: data: year: 2026 presets: - id: back_to_school '400': description: Calendar query validation failed (v1MarketingCalendar → v1Err). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: invalid year '500': description: Unexpected calendar failure. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: internal_error message: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized /campaigns/prepare: post: tags: - Campaigns summary: Prepare seasonal campaign export (legacy alias) description: | Alias of POST /marketing/calendar/prepare. Legacy envelope { data: { preset_id, name, export_feed_id, created, ... } }. requestBody: content: application/json: schema: type: object required: - preset_id properties: preset_id: type: string enum: - black_friday - christmas description: | Seasonal preset id. black_friday or christmas (required). year: type: integer minimum: 2000 maximum: 2100 description: | Target calendar year for date windows. Integer. Defaults to current year when omitted. format: type: string enum: - csv - xml default: csv description: Export feed format for the prepared campaign. csv (default) or xml. force_new: type: boolean description: | When true, create a new export feed even if one already exists for this preset/year. example: preset_id: black_friday year: 2026 format: csv force_new: false responses: '200': description: Prepare reused existing campaign. data envelope (PreparedCampaignEnvelope). content: application/json: schema: $ref: "#/components/schemas/PreparedCampaignEnvelope" example: data: preset_id: back_to_school year: 2026 created: false '201': description: Prepare created a new campaign. data envelope. content: application/json: schema: $ref: "#/components/schemas/PreparedCampaignEnvelope" example: data: preset_id: back_to_school year: 2026 created: true '400': description: Invalid JSON or marketing.ClientError (v1Err). ClientOrLog may emit flat Error. content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: validation_error message: invalid json '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /marketing/calendar: get: tags: - Campaigns summary: List seasonal campaign presets description: Canonical path (flat JSON). Prefer /campaigns for legacy clients. parameters: - name: year in: query schema: type: integer example: 2026 minimum: 2000 maximum: 2100 description: | Calendar year for seasonal presets (e.g. 2026). Integer. When omitted, server uses the current year. responses: '200': description: Flat marketing calendar payload (year, presets, prepared) when query validates. content: application/json: schema: $ref: "#/components/schemas/MarketingCalendar" example: year: 2026 presets: - id: back_to_school label: Back to school '400': description: Invalid query (year/preset). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid date '500': description: Unexpected calendar failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized /marketing/calendar/prepare: post: tags: - Campaigns summary: Prepare seasonal campaign export description: Canonical path (flat JSON). Prefer /campaigns/prepare for legacy clients. requestBody: content: application/json: schema: type: object required: - preset_id properties: preset_id: type: string enum: - black_friday - christmas description: | Seasonal preset id. black_friday or christmas (required). year: type: integer minimum: 2000 maximum: 2100 description: | Target calendar year for date windows. Integer. Defaults to current year when omitted. format: type: string enum: - csv - xml description: Export feed format for the prepared campaign. csv or xml. force_new: type: boolean description: | When true, create a new export feed even if one already exists for this preset/year. example: preset_id: black_friday year: 2026 format: csv force_new: false responses: '200': description: Prepare reused an existing campaign (Created=false). Flat PreparedCampaign JSON. content: application/json: schema: $ref: "#/components/schemas/PreparedCampaign" example: preset_id: back_to_school year: 2026 created: false '201': description: Prepare created a new campaign (Created=true). Flat PreparedCampaign JSON. content: application/json: schema: $ref: "#/components/schemas/PreparedCampaign" example: preset_id: back_to_school year: 2026 created: true '400': description: Invalid JSON or marketing.ClientError. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /process: get: tags: - Processing summary: List processing jobs (raw_product_ids surface) description: | Flat job list for the /process + raw_product_ids contract. Separate from legacy GET /products/process/{id}. parameters: - $ref: "#/components/parameters/Limit" responses: '200': description: "Recent dashboard-style jobs. Flat object with jobs[] and limit (handleV1ListProcessJobs\ \ — not legacy data envelope)." content: application/json: schema: type: object required: - jobs - limit properties: jobs: type: array items: $ref: "#/components/schemas/ProcessingJob" limit: type: integer example: jobs: - id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa status: processing total_products: 25 processed_products: 8 limit: 50 '500': description: Unexpected job list failure. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: list failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized post: tags: - Processing summary: Start processing job by raw_product_ids description: | Enqueues AI processing for existing raw product UUIDs. Same pipeline as dashboard POST /api/processing/jobs. Flat JSON body/response (no data wrapper); HTTP 202. Pass raw_products.id only (GET /products data[].raw_product_id), never PresentProduct.id. Separate from legacy POST /products/process (items[].ean → 200 { data }). Plan gates return HTTP 402 PlanGateError (not the legacy coded envelope). requestBody: $ref: "#/components/requestBodies/StartProcessByRawIDs" responses: '202': description: "Job accepted (HTTP 202). Flat ProcessingJobStartResponse; may include jobs[] when\ \ auto-split." content: application/json: schema: $ref: "#/components/schemas/ProcessingJobStartResponse" example: id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa status: pending total_products: 2 processing_type: full '400': description: Invalid JSON, invalid raw_product_id, empty ids, or processing.ClientError / LogAndError. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not start processing job '402': description: Plan gate blocked start. Special shape from handleStartProcessingJob (error string + code + upgrade_url) — not the legacy coded envelope. content: application/json: schema: $ref: "#/components/schemas/PlanGateError" example: error: Insufficient credits code: insufficient_credits upgrade_url: /pricing "429": { $ref: "#/components/responses/TooManyRequests" } '500': description: River enqueue failed after job row create. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: enqueue failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /process/{id}: get: tags: - Processing summary: Processing job status (raw_product_ids surface) description: | Flat ProcessingJob JSON. Not the legacy { data: { process_id, items } } envelope. When status is completed (finished), response is additively enriched with items[] and total_items (same processed product projection as GET /products/process/{id}). parameters: - $ref: "#/components/parameters/ID" responses: '200': description: "Flat ProcessingJob when GetJob succeeds for this company. Completed jobs include items[]." content: application/json: schema: $ref: "#/components/schemas/ProcessingJob" examples: processing: summary: In progress value: id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa status: processing total_products: 25 processed_products: 8 processing_type: full current_step: title completed: summary: Finished with processed products value: id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa status: completed total_products: 1 processed_products: 1 processing_type: full items: - ean: '0123456789012' id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb status: processed title: Acme Wireless Earbuds ANC Black total_items: 1 '400': description: Path id is not a UUID. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid id '404': description: Job not found for this company. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /process/{id}/cancel: post: tags: - Processing summary: Cancel processing job parameters: - $ref: "#/components/parameters/ID" responses: '200': description: Cancel/terminate acknowledged. Returns the updated job object when CancelJob succeeds. content: application/json: schema: $ref: "#/components/schemas/ProcessingJob" example: id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa status: cancelled total_products: 25 '400': description: Invalid id, or processing.ClientError / LogAndError (job not cancellable). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not cancel job '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '404': description: Resource id not found for this API-key company (or wrong tenant). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /process/{id}/terminate: post: tags: - Processing summary: Terminate processing job (alias of cancel) description: "Pixel/legacy naming alias of POST /process/{id}/cancel." parameters: - $ref: "#/components/parameters/ID" responses: '200': description: Cancel/terminate acknowledged. Returns the updated job object when CancelJob succeeds. content: application/json: schema: $ref: "#/components/schemas/ProcessingJob" example: id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa status: cancelled total_products: 25 '400': description: Invalid id, or processing.ClientError / LogAndError (job not cancellable). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not cancel job '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '500': description: Unexpected server error for this operation (handler internal_error / flat Error). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: internal error '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '404': description: Resource id not found for this API-key company (or wrong tenant). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /process/{id}/retry: post: tags: - Processing summary: Retry a failed or cancelled processing job parameters: - $ref: "#/components/parameters/ID" responses: '202': description: Retry accepted (HTTP 202). Flat job after RetryJob + enqueue. content: application/json: schema: $ref: "#/components/schemas/ProcessingJob" example: id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa status: pending total_products: 25 '400': description: Invalid id or job not retryable (ClientError / LogAndError). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: could not retry job "429": { $ref: "#/components/responses/TooManyRequests" } '500': description: Enqueue failed after retry. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: enqueue failed '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '403': description: Insufficient role or wrong company. On API-key routes, cross-tenant ids usually return 404 instead. requireCompanyAdmin returns flat 'admin required' for session non-admin callers (API keys use role api and pass). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '404': description: Resource id not found for this API-key company (or wrong tenant). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '422': description: Validation failed. Current public v1 handlers emit these cases as HTTP 400 (flat or coded). 422 is documented for clients that expect an explicit validation status; body matches FlatAPIError or LegacyAPIError. content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error() value: error: invalid json legacy: summary: v1Err coded value: error: code: validation_error message: invalid json /team/{userID}: parameters: - name: userID in: path required: true schema: type: string format: uuid description: Membership user id within the selected company patch: tags: - Team summary: Update team member role description: | Company admin or platform admin. Demoting the last active admin returns 409. Requires session cookie + CSRF double-submit (X-CSRF-Token). Dashboard surface under /api (not the public /api/v1 API-key base). security: - SessionCookie: [] CSRFHeader: [] requestBody: required: true content: application/json: schema: type: object required: - role properties: role: type: string enum: - admin - member description: Normalized to admin|member (case-insensitive input accepted) example: role: member responses: '200': description: Role updated (or unchanged) content: application/json: schema: type: object required: - status - role - user_id properties: status: type: string example: ok role: type: string enum: - admin - member user_id: type: string format: uuid example: status: ok role: member user_id: 4f3c2b1a-0e9d-4c8b-7a6f-5e4d3c2b1a09 '400': description: Invalid request for this route (HTTP 400). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '403': description: Forbidden (not company/platform admin) '404': description: Resource not found (HTTP 404). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: not found '409': description: Cannot demote the last admin content: application/json: schema: type: object properties: error: type: string example: cannot demote the last admin '401': description: Missing or invalid API key. RequireAPIKey returns this when Authorization Bearer and X-API-Key are both absent/empty or AuthenticateAPIKey returns ErrInvalidAPIKey. Legacy coded envelope (not a flat string). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized '422': description: Validation failed. Dashboard routes may use this status; prefer reading the message. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: validation failed '500': description: Unexpected server error (HTTP 500). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: list failed /admin/emails/set-password: post: tags: - Admin summary: Re-issue set-password invites description: | Platform admin only. Prefers durable invite reissue (ReissueSetPasswordInvite); falls back to HMAC set-password tokens when the user has no active membership. Skips synthetic …@legacy.local emails. Rate-limited per admin. When SMTP is disabled and a single user_id is provided, the response may include a one-time token for local/staging link copy (never logs email/token). Dashboard surface under /api (not the public /api/v1 API-key base). security: - SessionCookie: [] CSRFHeader: [] requestBody: required: false content: application/json: schema: type: object properties: user_id: type: string format: uuid description: | Target user UUID. When omitted, bulk-targets users needing a password (capped). example: user_id: 4f3c2b1a-0e9d-4c8b-7a6f-5e4d3c2b1a09 responses: '200': description: Issue/send summary content: application/json: schema: type: object required: - sent - issued - skipped - smtp_enabled - mode properties: sent: type: integer issued: type: integer skipped: type: integer skipped_synthetic: type: integer skipped_ineligible: type: integer skipped_rate_limited: type: integer skipped_send: type: integer smtp_enabled: type: boolean mode: type: string example: invite token: type: string description: Present only for single-user reissue when SMTP is off example: sent: 1 issued: 1 skipped: 0 skipped_synthetic: 0 skipped_ineligible: 0 skipped_rate_limited: 0 skipped_send: 0 smtp_enabled: true mode: invite '401': description: Unauthorized — missing session or privilege for this dashboard route. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: unauthorized '429': description: Rate limit exceeded for this admin action. May include Retry-After. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: Too many requests '503': description: Dependency unavailable (for example mailer down). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: mailer unavailable '400': description: Invalid request for this route (HTTP 400). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: invalid json '403': description: Forbidden for this route (HTTP 403). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: admin required '409': description: Conflict with current resource state. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: conflict '422': description: Validation failed. Dashboard routes may use this status; prefer reading the message. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: validation failed '500': description: Unexpected server error (HTTP 500). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: list failed components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: API key (dk_...) description: | Preferred scheme for /api/v1. Value is the raw company API key (prefix dk_...), not a JWT. Example header: Authorization: Bearer dk_your_key Create keys in Settings -> API keys. RapiDoc Try-it: paste the key, or use "Use my API key" when logged into the docs page. Full keys are never published in this YAML. ApiKeyAuth: type: apiKey in: header name: X-API-Key description: | Alternate scheme for /api/v1. Same company API key as BearerAuth, sent as header X-API-Key: dk_your_key. When both Authorization Bearer and X-API-Key are present, Bearer wins. SessionCookie: type: apiKey in: cookie name: descrybe_session description: Dashboard session cookie only (SESSION_COOKIE_NAME; default descrybe_session). Not valid for /api/v1 public routes. CSRFHeader: type: apiKey in: header name: X-CSRF-Token description: Dashboard CSRF double-submit header (must match descrybe_csrf cookie). Not used by /api/v1 API-key routes. parameters: ID: in: path name: id required: true schema: type: string format: uuid description: | Resource UUID for this path (product, attribute, feed, export feed, or processing job). Format: UUID string (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). Required. Limit: in: query name: limit schema: type: integer default: 50 maximum: 200 minimum: 1 description: | Page size for non-legacy list endpoints. Integer, default 50, minimum 1, maximum 200. LegacyLimit: in: query name: limit schema: type: integer default: 25 maximum: 100 minimum: 1 description: | Page size for legacy public list endpoints. Integer, default 25, minimum 1, maximum 100. Page: in: query name: page schema: type: integer default: 1 minimum: 1 description: | 1-based page index for offset/page pagination. Integer, default 1, minimum 1. Offset: in: query name: offset schema: type: integer default: 0 description: Offset pagination (ignored when cursor or after_id is set) Cursor: in: query name: cursor schema: type: string description: Opaque keyset cursor from next_cursor (preferred for deep pages) AfterID: in: query name: after_id schema: type: string format: uuid description: Keyset bookmark by product id; cursor wins when both are set requestBodies: LegacyStartProcessByEAN: required: true description: | Prefer items[].ean (legacy public). Same handler also accepts raw_product_ids when items is omitted. Sending neither returns validation_error. raw_product_ids must be raw_products.id values (see GET /products data[].raw_product_id). Do not pass PresentProduct.id / processed_products.id. content: application/json: schema: type: object properties: items: type: array minItems: 1 description: Primary legacy body — required unless raw_product_ids is set items: type: object required: - ean properties: ean: type: string description: GTIN / EAN barcode digits (required). Typically 8-14 characters. category_unique_id: type: string description: Optional category unique_id; forces categorization when set title: type: string description: Optional product title seed for enrichment description: type: string description: Optional long description seed for enrichment specifications: type: array description: Optional key/value specification pairs for attribute hints items: type: object properties: key: type: string description: Specification attribute key value: type: string description: Specification attribute value search: type: string description: Optional search keywords passed into enrichment context main_image: type: string format: uri description: Primary product image URL more_images: description: Additional image URLs as a single string or string array oneOf: - type: string - type: array items: type: string image_url: type: string format: uri description: Alternate primary image URL field (alias of main_image) additional_image_urls: description: Extra image URLs as a single string or string array oneOf: - type: string - type: array items: type: string image_link: type: string format: uri description: Google-style primary image_link URL additional_image_link: description: Google-style additional image links (string or array) oneOf: - type: string - type: array items: type: string raw_product_ids: type: array items: type: string format: uuid minItems: 1 description: | Alternate body — existing raw_products.id UUIDs (items takes precedence). Not PresentProduct.id. Use GET /products data[].raw_product_id. processing_type: description: | full (default); legacy steps category|title|description|attributes (string or array of those steps); dual-mode also accepts v2 dashboard types such as normalize_only, enhance_only, attributes_only, eprel_only, categorize_only (ParseV1ProcessingType). Alias processingType accepted when values match. oneOf: - type: string enum: - full - category - title - description - attributes - normalize_only - enhance - enhance_only - enhance-only - attributes_only - specs - specifications - eprel - eprel_only - categorize - categorize_only - categorize_enhance - type: array items: type: string enum: - category - title - description - attributes processingType: description: CamelCase alias of processing_type (must match if both set) oneOf: - type: string - type: array items: type: string processing_types: type: array items: type: string description: Dashboard fine-grained steps; used when processing_type omitted examples: by_ean: summary: "Legacy items[].ean (preferred)" value: items: - ean: '4548736132174' title: WH-1000XM5 Sony WH-1000XM5 Black category_unique_id: electronics main_image: https://images.example.com/products/wh1000xm5-black.jpg more_images: - https://images.example.com/products/wh1000xm5-black-side.jpg - ean: 0194252092942 title: Anker PowerLine III USB-C to USB-C 2m processing_type: full by_raw_ids: summary: Alternate raw_product_ids on same path value: raw_product_ids: - 2c5ea4c0-4067-4e44-8c5a-9a8b7c6d5e4f processing_type: normalize_only StartProcessByRawIDs: required: true description: | Dashboard-style start body — existing raw product UUIDs only (no items[].ean). Used by POST /process. Returns flat ProcessingJob JSON with HTTP 202. IDs must be raw_products.id (GET /products data[].raw_product_id), never PresentProduct.id. content: application/json: schema: type: object required: - raw_product_ids properties: raw_product_ids: type: array items: type: string format: uuid minItems: 1 description: | raw_products.id UUIDs to enqueue. Non-empty array of UUID strings (required). Do not pass processed product list id values. processing_type: type: string description: | full (default) or a legacy/dashboard step (category, title, description, attributes, normalize_only, enhance_only, …). default: full example: full processing_types: type: array items: type: string description: Optional fine-grained steps (dashboard); StartJob uses processing_type example: raw_product_ids: - 2c5ea4c0-4067-4e44-8c5a-9a8b7c6d5e4f - 550e8400-e29b-41d4-a716-446655440001 processing_type: full CreateCategory: required: true description: | Create a catalog category. Requires name and unique_id. parent_id is accepted as an alias of parent_unique_id. content: application/json: schema: type: object required: - name - unique_id properties: name: type: string description: Display name shown in the catalog UI (required) unique_id: type: string description: | Stable slug identifier (e.g. headphones). Lowercase letters, digits, underscores/hyphens. Required and unique within the company. parent_unique_id: type: string nullable: true description: Parent category unique_id, or null/omit for a root category parent_id: type: string nullable: true description: Legacy alias of parent_unique_id description: type: string nullable: true description: Optional human-readable category description example: name: Headphones unique_id: electronics_audio_headphones parent_id: electronics_audio description: Over-ear and in-ear headphones CreateAttribute: required: true description: | Create a catalog attribute and link it to a category. Requires name, attribute_key, value_type, and category_unique_id. content: application/json: schema: type: object required: - name - attribute_key - value_type - category_unique_id properties: name: type: string description: Human-readable attribute label (required) attribute_key: type: string description: | Stable machine key (e.g. battery_life_hours). Snake_case preferred. Required. value_type: type: string enum: - string - number - list - multiselect description: | Value shape. string, number, list, or multiselect (required). category_unique_id: type: string description: Category unique_id slug to attach this attribute to (required) unit: type: string nullable: true description: Optional unit label (e.g. W, cm) example: type: string nullable: true description: Sample value for docs/UI hints required: type: boolean default: false description: When true, products in this category should supply the attribute parent_key: type: string nullable: true description: Optional parent attribute_key for nested/grouped attributes example: name: Battery Life attribute_key: battery_life_hours value_type: number category_unique_id: electronics_audio_headphones unit: h required: false example: '65' schemas: HealthStatus: type: object required: - status - service - maintenance - read_only properties: status: type: string example: ok description: ok for liveness; ready/not_ready on /readyz service: type: string example: api maintenance: type: boolean description: When true API is in maintenance mode: null read_only: type: boolean description: When true mutating writes are rejected: null hypercare: type: boolean description: When true, tenant hypercare report-missing CTA is shown (P1-17) checks: type: object additionalProperties: type: string description: Present on /readyz (e.g. database ok|fail|unavailable) error: type: string description: Present on /readyz when not ready (safe public message) ProductListResponse: type: object required: [data, meta] properties: data: type: array items: $ref: "#/components/schemas/PresentProduct" meta: type: object required: - page - limit - total properties: page: type: integer limit: type: integer total: type: integer totalPages: type: integer ProductQualityListResponse: type: object required: - data - meta properties: data: type: array items: type: object properties: id: type: string format: uuid product_id: type: string name: type: string quality_score: type: integer quality_grade: type: string quality_checks: type: object additionalProperties: true meta: type: object required: - page - limit - total properties: page: type: integer limit: type: integer total: type: integer PresentProduct: type: object properties: id: type: string format: uuid description: | processed_products.id for this list row. Do not send as raw_product_ids — use raw_product_id instead. product_id: type: string example: SONY-WH1000XM5-B name: type: string nullable: true category: type: string nullable: true status: type: string example: completed raw_product_id: type: string format: uuid nullable: true description: | raw_products.id for dual-mode POST /products/process and POST /process bodies. Prefer this over id when starting jobs by UUID. feed_id: type: string format: uuid nullable: true quality_score: type: integer quality_grade: type: string example: C created_at: type: string format: date-time nullable: true updated_at: type: string format: date-time nullable: true ProcessedProduct: type: object properties: id: type: string format: uuid product_id: type: string example: SONY-WH1000XM5-B name: type: string processed_name: type: string category: type: string description: type: string processed_description: type: string status: type: string example: completed raw_product_id: type: string format: uuid feed_id: type: string format: uuid gtin: type: string attributes: type: object additionalProperties: true processed_attributes: type: object additionalProperties: true created_at: type: string format: date-time updated_at: type: string format: date-time CategoryListResponse: type: object required: - categories - total - limit - offset properties: categories: type: array items: type: object total: type: integer limit: type: integer offset: type: integer LegacyPaginationMeta: type: object required: - page - limit - total properties: page: type: integer limit: type: integer total: type: integer totalPages: type: integer LegacyCategoriesResponse: type: object required: - data - meta properties: data: type: array items: $ref: "#/components/schemas/LegacyCategory" meta: $ref: "#/components/schemas/LegacyPaginationMeta" LegacyCategory: type: object properties: id: type: string format: uuid unique_id: type: string name: type: string created_at: type: string format: date-time updated_at: type: string format: date-time CategoryDetail: type: object description: "Flat category row from GET/PATCH /categories/{id} (not a legacy data envelope)." properties: id: type: string format: uuid name: type: string unique_id: type: string parent_unique_id: type: string nullable: true path: type: string nullable: true level: type: integer position: type: integer is_active: type: boolean description: type: string nullable: true title_template: nullable: true description_template: nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time LegacyCategoryCreateResponse: type: object required: - data properties: data: type: object required: - id - unique_id - name properties: id: type: string format: uuid unique_id: type: string name: type: string LegacyAttributesResponse: type: object required: - data - meta properties: data: type: array items: $ref: "#/components/schemas/LegacyAttribute" meta: $ref: "#/components/schemas/LegacyPaginationMeta" LegacyAttribute: type: object properties: id: type: string format: uuid key: type: string name: type: string type: type: string unit: type: string nullable: true required: type: boolean category_unique_id: type: string created_at: type: string format: date-time updated_at: type: string format: date-time AttributeDetail: type: object description: "Flat attribute row from PATCH /attributes/{id} (not a legacy data envelope)." properties: id: type: string format: uuid attribute_key: type: string name: type: string value_type: type: string enum: [string, number, list, multiselect] unit: type: string nullable: true example: type: string nullable: true parent_key: type: string nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time LegacyAttributeCreateResponse: type: object required: - data properties: data: type: object properties: id: type: string format: uuid key: type: string name: type: string type: type: string unit: type: string nullable: true category_unique_id: type: string required: type: boolean LegacySuccessMessage: type: object required: - data properties: data: type: object required: - message properties: message: type: string FeedListResponse: type: object required: - data - meta properties: data: type: array items: $ref: "#/components/schemas/PresentFeed" meta: type: object required: - page - limit - total properties: page: type: integer limit: type: integer total: type: integer description: All matching feeds offset: type: integer active_total: type: integer description: Feeds with status active (truly syncing) mapped_total: type: integer description: Feeds with status mapped (fields saved, not activated) FeedGetResponse: type: object required: - data properties: data: $ref: "#/components/schemas/PresentFeed" FeedCreateResponse: type: object required: - data properties: data: $ref: "#/components/schemas/PresentFeed" FeedSyncResponse: type: object required: - data properties: data: type: object required: - jobId properties: jobId: type: string format: uuid job_id: type: string format: uuid description: Dual-support snake_case alias PresentFeed: type: object properties: id: type: string format: uuid name: type: string url: type: string nullable: true item_path: type: string is_active: type: boolean product_count: type: integer status: type: string last_synced: type: string format: date-time nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time feed_type: type: string description: Dual-support v2 field sync_interval_minutes: type: integer description: Dual-support v2 field last_synced_at: type: string format: date-time nullable: true options: type: object additionalProperties: true FeedMappings: type: object properties: id: type: string format: uuid description: Absent when no mappings row exists yet version: type: integer mappings: description: Field mapping document (object or array). Empty array when none saved. oneOf: - type: object additionalProperties: true - type: array items: type: object additionalProperties: true SchemaExtractResult: type: object required: - feed_id - format - fields - sample_rows properties: feed_id: type: string format: uuid format: type: string enum: - xml - csv suggested_item_path: type: string item_path: type: string fields: type: array items: type: object properties: path: type: string field_name: type: string data_type: type: string sample_values: type: array items: type: string unique_values_count: type: integer suggested_target: type: string sample_rows: type: integer preview: type: string preview_truncated: type: boolean ExportFeedDetail: type: object description: Flat export_feeds row from GET/PATCH/PUT template handlers (not presentV1ExportFeed / data envelope). properties: id: type: string format: uuid name: type: string source_feed_id: type: string format: uuid nullable: true format: type: string enum: - xml - csv public_token: type: string template: type: object additionalProperties: true nullable: true filters: type: object additionalProperties: true nullable: true is_active: type: boolean last_generated_at: type: string format: date-time nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time StatusOK: type: object required: - status properties: status: type: string example: ok FeedDeleted: type: object required: - id - deleted properties: id: type: string format: uuid deleted: type: boolean PreparedCampaign: type: object properties: preset_id: type: string enum: - black_friday - christmas name: type: string start_date: type: string format: date end_date: type: string format: date year: type: integer export_feed_id: type: string format: uuid export_feed_name: type: string created: type: boolean description: True only on newly prepared campaigns PreparedCampaignEnvelope: type: object required: - data properties: data: $ref: "#/components/schemas/PreparedCampaign" MarketingCalendar: type: object required: - year - presets - prepared properties: year: type: integer presets: type: array items: type: object properties: id: type: string name: type: string description: type: string start_date: type: string format: date end_date: type: string format: date year: type: integer prepared: type: array items: $ref: "#/components/schemas/PreparedCampaign" ProcessingJob: type: object properties: id: type: string format: uuid company_id: type: string format: uuid status: type: string example: pending total_products: type: integer processed_products: type: integer processing_type: type: string current_step: type: string step_progress: type: array items: type: object properties: step: type: string status: type: string note: type: string error: type: string nullable: true started_at: type: string format: date-time nullable: true completed_at: type: string format: date-time nullable: true created_at: type: string format: date-time items: type: array description: Present when status is completed — processed product payload (additive) items: $ref: "#/components/schemas/LegacyProcessItem" total_items: type: integer description: Present with items when status is completed jobs: type: array items: $ref: "#/components/schemas/ProcessingJob" description: Present when StartJob auto-splits into multiple jobs sibling_job_ids: type: array items: type: string format: uuid job_count: type: integer total_products_queued: type: integer ProcessingJobStartResponse: description: Single Job object, or Job plus split metadata (jobs, sibling_job_ids, …) allOf: - $ref: "#/components/schemas/ProcessingJob" PlanGateError: type: object properties: error: type: string code: type: string enum: - insufficient_credits - product_limit - ai_requires_upgrade - eprel_requires_upgrade - plan_gate upgrade_url: type: string example: /pricing FlatAPIError: type: object required: - error properties: error: type: string LegacyAPIError: type: object required: - error properties: error: type: object required: - code - message properties: code: type: string example: validation_error message: type: string requestId: type: string LegacyProcessStartEnvelope: type: object required: - data properties: data: type: object required: - process_id - message properties: process_id: type: string format: uuid message: type: string total_items: type: integer processed_items: type: integer job_count: type: integer description: Present when StartJob auto-splits sibling_job_ids: type: array items: type: string format: uuid total_products_queued: type: integer errors: type: array items: type: string description: Per-item upsert failures when some EANs still queued LegacyProcessStatusEnvelope: type: object required: - data properties: data: type: object required: - status - process_id properties: status: type: string description: Uppercase job status (COMPLETED, FAILED, PENDING, PROCESSING, …) example: COMPLETED process_id: type: string format: uuid processing_type: description: Echo of requested type (string or step array) oneOf: - type: string - type: array items: type: string items: type: array description: Present when status is COMPLETED items: $ref: "#/components/schemas/LegacyProcessItem" total_items: type: integer processed_at: type: string format: date-time message: type: string error: type: string description: Present when status is FAILED created_at: type: string format: date-time started_at: type: string format: date-time nullable: true LegacyProcessItem: type: object description: | One COMPLETED legacy process line (A1 / public contract). Successful items expose category as categories.unique_id, a plain-text description string (never a JSON array; HTML stripped), SEO meta_title / meta_description, optional eprel object or null, clean attributes, images, and dual-mode ids. Product display name is title; additive name mirrors the same processed title (dual-mode for scorecards / legacy clients that read name). required: - ean properties: ean: type: string id: type: string format: uuid description: | Legacy field: processed_products.id when enrichment succeeded. Do not treat as raw_products.id. Same value as processed_product_id. processed_product_id: type: string format: uuid description: | Explicit alias of id (processed_products.id). Prefer this name in new dual-mode clients; id remains for backward compatibility. raw_product_id: type: string format: uuid description: | raw_products.id for this job line. Use with POST /process raw_product_ids or dashboard catalog APIs. Present whenever the job product row exists. category: type: string nullable: true description: | categories.unique_id for the assigned category. Opaque string — may be numeric (e.g. "28" or "50") or slug-like. Not a URL path and not a UUID. category_name: type: string nullable: true description: Human-readable category display name (not the unique_id). title: type: string nullable: true description: | Product display name (processed title). Primary legacy field; same value as name when present. name: type: string nullable: true description: | Additive alias of title (same processed display name). Prefer title in new clients; name remains for scorecards and legacy readers. meta_title: type: string nullable: true description: | SEO title. Filled from processing meta or synthesized from title / category when empty so successful items are not left with null meta. meta_description: type: string nullable: true description: | SEO description (word-safe truncate). Distinct from body description when possible; synthesized from plain description when DB meta is empty. description: type: string nullable: true description: | Plain-text product body description. Always a string — never a one-element JSON array. Feed HTML tags are stripped; newlines may remain between paragraphs. attributes: type: object additionalProperties: true nullable: true description: | Characteristic catalog attributes only (brand, model, dims, warranty, …). Core fields and eprel_* keys are not duplicated here. main_image: type: string nullable: true more_images: type: array items: type: string nullable: true eprel: nullable: true type: object description: | EU energy label payload when EPREL data is available; otherwise null. Prefer this object over raw eprel_* keys inside attributes. Live shape keys: id, label, pdf, energy_class, energy_scale. properties: id: type: string description: EPREL product registration id (same as eprel_id). label: type: string pdf: type: string energy_class: type: string energy_scale: type: string status: type: string description: Per-item outcome — processed on success; not_found / failed / cancelled otherwise example: processed error: type: string examples: ProcessingJobAccepted: summary: Single processing job accepted (POST /process) value: id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21 company_id: 7c9e6679-7425-40de-944b-e07fc1f90ae7 status: pending total_products: 2 processed_products: 0 processing_type: full current_step: category step_progress: - step: category status: pending - step: title status: pending - step: description status: pending - step: attributes status: pending created_at: '2026-08-04T09:59:50Z' ProcessingJobSplitAccepted: summary: Auto-split batch (POST /process) value: id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21 company_id: 7c9e6679-7425-40de-944b-e07fc1f90ae7 status: pending total_products: 50 processed_products: 0 processing_type: full current_step: category created_at: '2026-08-04T09:59:50Z' jobs: - id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21 status: pending total_products: 50 - id: 1b4e28ba-2fa1-4d3a-9c6e-7f8a9b0c1d2e status: pending total_products: 50 sibling_job_ids: - 1b4e28ba-2fa1-4d3a-9c6e-7f8a9b0c1d2e job_count: 2 total_products_queued: 100 ProcessingJobRunning: summary: "Processing job in progress (GET /process/{id})" value: id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21 company_id: 7c9e6679-7425-40de-944b-e07fc1f90ae7 status: processing total_products: 25 processed_products: 8 processing_type: full current_step: title step_progress: - step: category status: done - step: title status: running - step: description status: pending - step: attributes status: pending started_at: '2026-08-04T10:00:00Z' created_at: '2026-08-04T09:59:50Z' LegacyProcessCompleted: summary: "Completed legacy poll with A1-shaped item (unique_id category, plain description, meta, eprel)" value: data: status: COMPLETED process_id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21 processing_type: full items: - ean: '8606019604493' id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb processed_product_id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb raw_product_id: cccccccc-cccc-cccc-cccc-cccccccccccc status: processed category: '50' category_name: Cookers title: VOX electric cooker EHT 6020 WG name: VOX electric cooker EHT 6020 WG meta_title: VOX electric cooker EHT 6020 WG | 50 meta_description: Affordable electric cooker with four plates and a 65 L fan oven. description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven. Energy class A with practical everyday capacity." attributes: brand: Vox product_model: EHT6020WG width: 0.6 m height: 0.85 m depth: 0.6 m weight: 42.81 kg warranty: 60 months main_image: https://images.example.com/products/vox-eht6020wg.jpg more_images: - https://images.example.com/products/vox-eht6020wg-side.jpg eprel: id: "1234567" label: https://eprel.ec.europa.eu/label/Example pdf: https://eprel.ec.europa.eu/fiches/Example.pdf energy_class: A energy_scale: A-G total_items: 1 processed_at: '2026-08-04T10:04:12Z' LegacyProcessInProgress: summary: In-progress legacy poll value: data: status: PROCESSING process_id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21 processing_type: full created_at: '2026-08-04T09:59:50Z' started_at: '2026-08-04T10:00:00Z' LegacyProcessFailed: summary: Failed legacy poll value: data: status: FAILED process_id: 8f14e45f-ceea-467a-9e5d-9c6b0e8f3a21 processing_type: full error: Processing failed responses: Unauthorized: description: Missing or invalid API key (RequireAPIKey / CodedError legacy envelope). content: application/json: schema: $ref: "#/components/schemas/LegacyAPIError" example: error: code: unauthorized message: Unauthorized Forbidden: description: Wrong company or insufficient role. Flat error string. Cross-tenant resource ids on API-key routes usually return 404 instead. content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: forbidden NotFound: description: Resource not found for this API key company (missing id or wrong tenant). content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Dashboard-shared handlers value: error: not found legacy: summary: Legacy v1Err helpers value: error: code: not_found message: Not found BadRequest: description: Invalid request or client validation error (handlers use HTTP 400). content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error helper value: error: invalid json legacy: summary: Legacy v1Err validation value: error: code: validation_error message: '''items'' array is required' ValidationError: description: Validation error. Public v1 handlers return HTTP 400 for these cases (OpenAPI also lists 422 for clients that expect that status). content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat validation value: error: invalid id legacy: summary: Legacy coded validation value: error: code: validation_error message: invalid id Conflict: description: Conflict (for example cannot demote or remove the last company admin). content: application/json: schema: $ref: "#/components/schemas/FlatAPIError" example: error: cannot demote the last admin TooManyRequests: description: "Rate limited (RateLimitV1Process heavy mutations and/or processing StartLimiter).\ \ May include Retry-After: 60." headers: Retry-After: schema: type: integer description: Seconds until retry (set by RateLimitV1Process) content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Middleware limiter value: error: rate limit exceeded legacy: summary: Processing StartLimiter value: error: code: rate_limited message: rate limit exceeded InternalServerError: description: Unexpected server failure (auth backend, enqueue, list/get failures). content: application/json: schema: oneOf: - $ref: "#/components/schemas/FlatAPIError" - $ref: "#/components/schemas/LegacyAPIError" examples: flat: summary: Flat Error helper value: error: list failed legacy: summary: Legacy coded internal error value: error: code: internal_error message: list failed `)