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[] omit internal UUIDs (id / processed_product_id / raw_product_id). Use GET /products when a UUID is needed. Display name is items[].name (title omitted when identical). 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, name is the product display name (title is omitted when identical). Internal UUIDs (id / processed_product_id / raw_product_id) are omitted — use GET /products for those. A1 cohort / Platform Demo / A1-prompt companies omit meta_title and meta_description even if stored. 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' status: processed category: Cookers category_id: '50' category_name: Cookers name: VOX electric cooker EHT 6020 WG 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 status: type: string description: | Job lifecycle status on enqueue. Always pending (or processing if the worker already picked it up). Never COMPLETED on start — poll GET /products/process/{id} for completion. example: pending message: type: string total_items: type: integer description: Number of products accepted into the job (enqueue size). processed_items: type: integer description: | Count of items that finished processing. Always 0 on start while the job is pending/processing; increments as the worker completes products. example: 0 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 the human-readable display name (category_id holds categories.unique_id), a description string that may include category formula HTML (h1/h2/h3/h4, p, ul — never a JSON array), optional SEO meta_title / meta_description (plain text; omitted for A1 cohort, Platform Demo, and any company with A1-style category role-section prompts — even if stored in DB), optional eprel object or null, clean attributes, and images. Product display name is name (primary). title is omitted when identical to name. Internal UUIDs (id, processed_product_id, raw_product_id) are omitted from this public shape — use catalog APIs when a product UUID is required. Property order prefers human-readable fields first (ean, name, category, description, attributes, images, eprel). required: - ean properties: ean: type: string category: type: string nullable: true description: | Human-readable category display name (same value as category_name when resolved). Prefer this for UI. Machine unique_id is category_id. ASSUMPTION: historically this field held categories.unique_id; clients that need the opaque id must read category_id (additive). category_id: 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 (mirrors category when both are set). name: type: string nullable: true description: | Product display name (processed title). Primary field for clients. title: type: string nullable: true description: | Optional legacy alias of name. Omitted when identical to name. 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. Omitted for A1 cohort, Platform Demo, and companies with A1-style category prompts (SEO meta is not used). 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. Omitted for A1 cohort, Platform Demo, and companies with A1-style category prompts (SEO meta is not used). description: type: string nullable: true description: | Product body description as a string — never a one-element JSON array. When the category description_template requires multi-section markup, this field retains formula HTML tags (h1/h2/h3/h4, p, ul). Feed-only plain text remains plain; meta_description stays plain SEO text. 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' status: processed category: Cookers category_id: '50' category_name: Cookers name: VOX electric cooker EHT 6020 WG 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 `)