openapi: 3.1.0 info: title: closercat-commerce-protocol v1 — lo que expone CloserCat version: 1.0.0 summary: Endpoints a los que tu sistema empuja catálogo, disponibilidad y eventos. description: | Tu sistema llama estos endpoints para mantener al día el catálogo y la disponibilidad que usa el agente, y para avisar lo que pasa con apartados, pedidos y facturas. Mismas reglas que en la otra dirección: firma HMAC-SHA256 con tu llave (`X-CloserCat-Key-Id`), `X-CloserCat-Protocol: commerce/1.0`, `Idempotency-Key` en todo `PUT`/`POST`, montos como cadenas decimales y fechas ISO-8601 con zona. Los esquemas compartidos (producto, error, montos) se definen en `openapi-marca.yaml`. `connection_id` identifica tu conexión (marca × ambiente). Lo ves en CloserCat, en *Tu Oferta › Tu sistema de ventas*. license: name: CC BY 4.0 url: https://creativecommons.org/licenses/by/4.0/ contact: name: CloserCat — integraciones url: https://closercat.pro/desarrolladores/comercio servers: - url: https://api.closercat.pro/commerce/v1/{connection_id} description: Producción y sandbox (el ambiente lo define la conexión y su llave ck_live_ / ck_test_). variables: connection_id: default: cn_0000000000000000 security: - closercatSignature: [] tags: - name: Prueba description: Probar tu firma y el ambiente de la conexión. - name: Catálogo description: Productos y variantes (SKU por variante). - name: Disponibilidad description: Unidades vendibles en línea por SKU. - name: Eventos description: Avisos de apartados, pedidos y facturas. paths: /ping: get: tags: [Prueba] operationId: ping summary: Probar tu firma y el ambiente de la conexión parameters: - $ref: './openapi-marca.yaml#/components/parameters/Protocol' - $ref: './openapi-marca.yaml#/components/parameters/KeyId' - $ref: './openapi-marca.yaml#/components/parameters/Timestamp' - $ref: './openapi-marca.yaml#/components/parameters/Signature' responses: '200': description: Firma válida. content: application/json: schema: {$ref: '#/components/schemas/Ping'} '401': {$ref: '#/components/responses/Unauthorized'} '429': {$ref: '#/components/responses/RateLimited'} '503': {$ref: '#/components/responses/Unavailable'} /catalog: put: tags: [Catálogo] operationId: putCatalog summary: Enviar una parte de un lote de catálogo (catalog.push) description: | Un lote (`batch_id`) puede ir en varias partes (`part` de `total_parts`), de hasta 1000 productos y 5 MB cada una. Respondemos `202` en ≤ 2 s con lo aceptado y lo rechazado **por ítem**; el lote se aplica en segundo plano cuando llegan todas sus partes. Consulta el resultado en `GET /catalog/batches/{batch_id}`. - `mode: full` = foto completa: los productos que no vengan en el lote quedan archivados (nunca se borran). - `mode: delta` = solo cambios. Reenviar la misma parte con el mismo `batch_id` no la aplica dos veces (`Idempotency-Key` = `{batch_id}:{part}`). x-capability: catalog.push parameters: - $ref: './openapi-marca.yaml#/components/parameters/Protocol' - $ref: './openapi-marca.yaml#/components/parameters/KeyId' - $ref: './openapi-marca.yaml#/components/parameters/Timestamp' - $ref: './openapi-marca.yaml#/components/parameters/Signature' - $ref: './openapi-marca.yaml#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/CatalogBatch'} responses: '202': description: Parte recibida, con el detalle de los ítems rechazados. content: application/json: schema: {$ref: '#/components/schemas/BatchReceipt'} '400': {$ref: '#/components/responses/ValidationError'} '401': {$ref: '#/components/responses/Unauthorized'} '409': {$ref: '#/components/responses/Conflict'} '413': {$ref: '#/components/responses/TooLarge'} '429': {$ref: '#/components/responses/RateLimited'} '503': {$ref: '#/components/responses/Unavailable'} /catalog/batches/{batch_id}: get: tags: [Catálogo] operationId: getCatalogBatch summary: Resultado de un lote de catálogo x-capability: catalog.push parameters: - name: batch_id in: path required: true schema: {$ref: './openapi-marca.yaml#/components/schemas/Reference'} - $ref: './openapi-marca.yaml#/components/parameters/Protocol' - $ref: './openapi-marca.yaml#/components/parameters/KeyId' - $ref: './openapi-marca.yaml#/components/parameters/Timestamp' - $ref: './openapi-marca.yaml#/components/parameters/Signature' responses: '200': description: Estado del lote. content: application/json: schema: {$ref: '#/components/schemas/BatchStatus'} '401': {$ref: '#/components/responses/Unauthorized'} '404': description: Lote desconocido. content: application/json: schema: {$ref: './openapi-marca.yaml#/components/schemas/Error'} /availability: post: tags: [Disponibilidad] operationId: postAvailability summary: Enviar disponibilidad (availability.push) description: | Envía los cambios apenas ocurren (`mode: delta`, ≤ 60 s después del cambio) y una foto completa al día (`mode: full`). `seq` crece de forma estricta por conexión: un `seq` menor o igual al último aplicado se ignora (responde `202` con `applied: false`). `available` son unidades vendibles en línea, ya descontados tus apartados. x-capability: availability.push parameters: - $ref: './openapi-marca.yaml#/components/parameters/Protocol' - $ref: './openapi-marca.yaml#/components/parameters/KeyId' - $ref: './openapi-marca.yaml#/components/parameters/Timestamp' - $ref: './openapi-marca.yaml#/components/parameters/Signature' - $ref: './openapi-marca.yaml#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/AvailabilityBatch'} responses: '202': description: Recibida. content: application/json: schema: {$ref: '#/components/schemas/AvailabilityReceipt'} '400': {$ref: '#/components/responses/ValidationError'} '401': {$ref: '#/components/responses/Unauthorized'} '413': {$ref: '#/components/responses/TooLarge'} '429': {$ref: '#/components/responses/RateLimited'} '503': {$ref: '#/components/responses/Unavailable'} /events: post: tags: [Eventos] operationId: postEvent summary: Avisar lo que pasó con un apartado, un pedido o una factura description: | Responde `204` en ≤ 1 s. Reintenta hasta recibir un 2xx; un `event_id` repetido es un no-op (`204`). Los eventos de apartado son obligatorios con `holds.async`; los de pedido, con `orders.events`; la factura, con `invoice.events`. x-capability: orders.events x-capability-also: [holds.async, invoice.events] parameters: - $ref: './openapi-marca.yaml#/components/parameters/Protocol' - $ref: './openapi-marca.yaml#/components/parameters/KeyId' - $ref: './openapi-marca.yaml#/components/parameters/Timestamp' - $ref: './openapi-marca.yaml#/components/parameters/Signature' - $ref: './openapi-marca.yaml#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/Event'} responses: '204': {description: Recibido (o repetido).} '400': {$ref: '#/components/responses/ValidationError'} '401': {$ref: '#/components/responses/Unauthorized'} '404': description: La `reference` del evento no pertenece a esta conexión. content: application/json: schema: {$ref: './openapi-marca.yaml#/components/schemas/Error'} '429': {$ref: '#/components/responses/RateLimited'} '503': {$ref: '#/components/responses/Unavailable'} components: securitySchemes: closercatSignature: type: apiKey in: header name: X-CloserCat-Signature description: Igual que en `openapi-marca.yaml`; firmas con tu llave sobre `"{timestamp}.{cuerpo crudo}"`. responses: Unauthorized: description: Firma inválida, llave desconocida o revocada, o timestamp fuera de 300 s. content: application/json: schema: {$ref: './openapi-marca.yaml#/components/schemas/Error'} Conflict: description: '`idempotency_key_reused`: la misma clave con otro cuerpo.' content: application/json: schema: {$ref: './openapi-marca.yaml#/components/schemas/Error'} ValidationError: description: '`validation_error` del cuerpo completo (por ejemplo, `part` > `total_parts`). Los errores por ítem van en el 202.' content: application/json: schema: {$ref: './openapi-marca.yaml#/components/schemas/Error'} TooLarge: description: Cuerpo mayor al límite (catálogo 5 MB por parte; disponibilidad y eventos 256 KB). content: application/json: schema: {$ref: './openapi-marca.yaml#/components/schemas/Error'} RateLimited: description: '`rate_limited` (120 solicitudes por minuto por conexión). Respeta `Retry-After`.' headers: Retry-After: schema: {type: integer, minimum: 1} content: application/json: schema: {$ref: './openapi-marca.yaml#/components/schemas/Error'} Unavailable: description: '`unavailable`. Reintenta con espera creciente; no se pierde nada de lo ya aceptado.' content: application/json: schema: {$ref: './openapi-marca.yaml#/components/schemas/Error'} schemas: Ping: type: object additionalProperties: false required: [ok, environment, connection_id] properties: ok: {type: boolean} environment: {type: string, enum: [sandbox, production]} connection_id: {type: string} CatalogBatch: type: object additionalProperties: false required: [protocol_version, batch_id, mode, part, total_parts, products] properties: protocol_version: {type: string, const: commerce/1.0} batch_id: {$ref: './openapi-marca.yaml#/components/schemas/Reference'} mode: {type: string, enum: [full, delta]} part: {type: integer, minimum: 1} total_parts: {type: integer, minimum: 1, maximum: 500} products: type: array items: {$ref: './openapi-marca.yaml#/components/schemas/Product'} minItems: 0 maxItems: 1000 ItemError: type: object additionalProperties: false required: [field, code] properties: field: {type: string, description: 'Ruta del campo, por ejemplo variants[2].price.amount.'} code: {type: string, enum: [required, invalid_format, invalid_value, duplicate_sku, too_long, unknown_attribute]} RejectedItem: type: object additionalProperties: false required: [external_id, errors] properties: external_id: {type: [string, 'null']} sku: {type: [string, 'null']} errors: type: array items: {$ref: '#/components/schemas/ItemError'} minItems: 1 BatchReceipt: type: object additionalProperties: false required: [batch_id, part, accepted, rejected] properties: batch_id: {type: string} part: {type: integer} accepted: {type: integer, minimum: 0} rejected: type: array items: {$ref: '#/components/schemas/RejectedItem'} BatchStatus: type: object additionalProperties: false required: [batch_id, status, parts_received, total_parts] properties: batch_id: {type: string} status: {type: string, enum: [receiving, applying, applied, failed]} parts_received: {type: integer, minimum: 0} total_parts: {type: integer, minimum: 1} created: {type: integer, minimum: 0} updated: {type: integer, minimum: 0} archived: {type: integer, minimum: 0} needs_review: {type: integer, minimum: 0, description: 'Ítems que CloserCat no pudo emparejar con seguridad (los revisa tu equipo en CloserCat).'} rejected: {type: integer, minimum: 0} applied_at: {type: [string, 'null'], format: date-time} AvailabilityBatch: type: object additionalProperties: false required: [protocol_version, seq, as_of, mode, items] properties: protocol_version: {type: string, const: commerce/1.0} seq: {type: integer, minimum: 1} as_of: {type: string, format: date-time, description: 'Momento al que corresponde el dato en tu sistema.'} mode: {type: string, enum: [full, delta]} items: type: array items: {$ref: './openapi-marca.yaml#/components/schemas/AvailabilityItem'} minItems: 0 maxItems: 5000 AvailabilityReceipt: type: object additionalProperties: false required: [seq, applied, accepted, rejected] properties: seq: {type: integer} applied: {type: boolean, description: 'false si el seq ya se había aplicado o es viejo.'} accepted: {type: integer, minimum: 0} rejected: type: array items: {$ref: '#/components/schemas/RejectedItem'} EventType: type: string enum: [hold.confirmed, hold.rejected, hold.released, order.accepted, order.rejected, order.shipped, order.delivered, order.cancelled, invoice.issued] HoldEventData: type: object additionalProperties: false required: [reference] properties: reference: {$ref: './openapi-marca.yaml#/components/schemas/Reference'} erp_hold_id: {type: [string, 'null']} expires_at: {type: [string, 'null'], format: date-time} lines: type: array items: {$ref: './openapi-marca.yaml#/components/schemas/HoldLine'} reason: {type: [string, 'null'], enum: [insufficient_stock, expired, manual, null]} OrderEventData: type: object additionalProperties: false required: [reference] properties: reference: {$ref: './openapi-marca.yaml#/components/schemas/Reference'} erp_order_id: {type: [string, 'null']} reason: {type: [string, 'null'], maxLength: 200, description: 'En rejected/cancelled: motivo en una frase, sin datos personales.'} carrier: {type: [string, 'null'], maxLength: 80} tracking_number: {type: [string, 'null'], maxLength: 80} tracking_url: {type: [string, 'null'], format: uri} InvoiceEventData: type: object additionalProperties: false required: [reference, number, issued_at] properties: reference: {$ref: './openapi-marca.yaml#/components/schemas/Reference'} number: {type: string, maxLength: 40, description: 'Prefijo y número de la factura.'} issued_at: {type: string, format: date-time} cufe: {type: [string, 'null'], maxLength: 128} pdf_url: type: [string, 'null'] format: uri description: URL https temporal (mínimo 24 h) del PDF; CloserCat lo copia y lo envía solo si la compradora pidió factura. Event: type: object additionalProperties: false required: [protocol_version, event_id, type, occurred_at, data] properties: protocol_version: {type: string, const: commerce/1.0} event_id: {type: string, minLength: 1, maxLength: 128} type: {$ref: '#/components/schemas/EventType'} occurred_at: {type: string, format: date-time} data: description: La forma depende de `type` (ver las reglas `allOf` de este esquema). anyOf: - {$ref: '#/components/schemas/HoldEventData'} - {$ref: '#/components/schemas/OrderEventData'} - {$ref: '#/components/schemas/InvoiceEventData'} allOf: - if: properties: {type: {enum: [hold.confirmed, hold.rejected, hold.released]}} then: properties: {data: {$ref: '#/components/schemas/HoldEventData'}} - if: properties: {type: {enum: [order.accepted, order.rejected, order.shipped, order.delivered, order.cancelled]}} then: properties: {data: {$ref: '#/components/schemas/OrderEventData'}} - if: properties: {type: {const: invoice.issued}} then: properties: {data: {$ref: '#/components/schemas/InvoiceEventData'}}