openapi: 3.0.3 info: title: Donde lo pillo API version: 1.1.0 description: | API para descubrir productos y sincronizar catálogos de tiendas y vendedores. Las rutas bajo /public son de solo lectura y no requieren credenciales. Las rutas /products de escritura requieren las credenciales API de la cuenta vendedora y una suscripción de pago activa. Las rutas de lectura pública y las rutas de sincronización aplican cada una un límite de 120 solicitudes por minuto y dirección IP; las respuestas 429 incluyen Retry-After: 60. contact: name: Donde lo pillo email: hola@dondelopillo.com servers: - url: https://dondelopillo.com/api/v1 description: Producción - url: https://dondelopillo.test/api/v1 description: Desarrollo local tags: - name: Descubrimiento público description: Búsqueda y consulta de productos para web y aplicaciones móviles. - name: Catálogo de vendedor description: Escritura del catálogo propio mediante credenciales de vendedor. security: [] paths: /public/products: get: tags: [Descubrimiento público] summary: Buscar y filtrar productos description: | Solo devuelve productos activos de vendedores activos. Los filtros se pueden combinar. La paginación empieza en 1; limit por defecto 24 y máximo 50. Todos los precios del catálogo se expresan en euros (EUR), también en los filtros y ordenaciones por precio. Límite: 120 solicitudes por minuto y dirección IP. parameters: - { name: q, in: query, description: Búsqueda en título, descripción y etiquetas., schema: { type: string, maxLength: 200 } } - { name: tag, in: query, description: Etiqueta exacta del producto., schema: { type: string, maxLength: 100 } } - { name: seller, in: query, description: Slug público del vendedor., schema: { type: string, maxLength: 190 } } - { name: category, in: query, description: Ruta de categoría devuelta por /public/categories; incluye sus subcategorías., schema: { type: string, maxLength: 500, example: hogar-y-decoracion/iluminacion } } - { name: min_price, in: query, description: Precio mínimo inclusivo; hasta dos decimales., schema: { type: number, minimum: 0, maximum: 9999999999.99, multipleOf: 0.01 } } - { name: max_price, in: query, description: Precio máximo inclusivo; hasta dos decimales., schema: { type: number, minimum: 0, maximum: 9999999999.99, multipleOf: 0.01 } } - { name: sort, in: query, description: Orden de resultados., schema: { type: string, enum: [relevance, newest, price_asc, price_desc], default: relevance } } - { name: page, in: query, description: Página de resultados (base 1; máximo 100.000)., schema: { type: integer, minimum: 1, maximum: 100000, default: 1 } } - { name: limit, in: query, description: Resultados por página., schema: { type: integer, minimum: 1, maximum: 50, default: 24 } } responses: '200': description: Resultados y datos de paginación. content: application/json: schema: { $ref: '#/components/schemas/ProductListResponse' } '422': { $ref: '#/components/responses/ValidationError' } '429': { $ref: '#/components/responses/RateLimited' } /public/products/{id}: get: tags: [Descubrimiento público] summary: Obtener un producto description: Devuelve una ficha pública si el producto está activo y su vendedor también. parameters: - { name: id, in: path, required: true, schema: { type: integer, minimum: 1 } } responses: '200': description: Ficha pública del producto. content: application/json: schema: { $ref: '#/components/schemas/ProductResponse' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/RateLimited' } /public/tags: get: tags: [Descubrimiento público] summary: Listar etiquetas disponibles description: Devuelve etiquetas presentes en productos activos, con su slug y número de productos. responses: '200': description: Etiquetas ordenadas por número de productos. content: application/json: schema: type: object required: [success, data, meta] properties: success: { type: boolean, example: true } data: { type: array, items: { $ref: '#/components/schemas/Tag' } } meta: type: object properties: count: { type: integer } '429': { $ref: '#/components/responses/RateLimited' } /public/categories: get: tags: [Descubrimiento público] summary: Listar categorías y subcategorías description: Devuelve el árbol de categorías activas, sus rutas públicas y el total de productos activos en cada rama. responses: '200': description: Árbol del catálogo. content: application/json: schema: { $ref: '#/components/schemas/CategoryListResponse' } '429': { $ref: '#/components/responses/RateLimited' } /products: post: tags: [Catálogo de vendedor] security: [{ apiUser: [], apiKey: [] }] summary: Crear o actualizar un producto description: Crea o actualiza por SKU dentro de la cuenta autenticada. seller_id nunca se admite del cliente. parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ProductInput' } responses: '201': { $ref: '#/components/responses/WriteSuccess' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '422': { $ref: '#/components/responses/ValidationError' } '429': { $ref: '#/components/responses/RateLimited' } '500': { $ref: '#/components/responses/ServerError' } /products/bulk: post: tags: [Catálogo de vendedor] security: [{ apiUser: [], apiKey: [] }] summary: Crear o actualizar hasta 500 productos parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [products] properties: products: type: array maxItems: 500 items: { $ref: '#/components/schemas/ProductInput' } responses: '200': { $ref: '#/components/responses/WriteSuccess' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '422': { $ref: '#/components/responses/ValidationError' } '429': { $ref: '#/components/responses/RateLimited' } '500': { $ref: '#/components/responses/ServerError' } /products/{sku}: delete: tags: [Catálogo de vendedor] security: [{ apiUser: [], apiKey: [] }] summary: Desactivar un producto por SKU parameters: - { name: sku, in: path, required: true, schema: { type: string, minLength: 1, maxLength: 190 } } responses: '200': { $ref: '#/components/responses/WriteSuccess' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } '422': { $ref: '#/components/responses/ValidationError' } '500': { $ref: '#/components/responses/ServerError' } /products/delete-bulk: post: tags: [Catálogo de vendedor] security: [{ apiUser: [], apiKey: [] }] summary: Desactivar hasta 500 productos por SKU requestBody: required: true content: application/json: schema: type: object required: [skus] properties: skus: { type: array, maxItems: 500, items: { type: string, minLength: 1, maxLength: 190 } } responses: '200': { $ref: '#/components/responses/WriteSuccess' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '422': { $ref: '#/components/responses/ValidationError' } '429': { $ref: '#/components/responses/RateLimited' } '500': { $ref: '#/components/responses/ServerError' } /products/skus: get: tags: [Catálogo de vendedor] security: [{ apiUser: [], apiKey: [] }] summary: Listar los SKU del catálogo autenticado parameters: - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 5000, default: 5000 } } - { name: cursor, in: query, description: Cursor recibido en next_cursor., schema: { type: string } } - { name: active, in: query, description: Devuelve solo activos o todos., schema: { type: string, enum: ['1', all], default: '1' } } responses: '200': { $ref: '#/components/responses/WriteSuccess' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '429': { $ref: '#/components/responses/RateLimited' } components: securitySchemes: apiUser: type: apiKey in: header name: X-API-User description: Usuario API mostrado al crear credenciales en el panel de vendedor. apiKey: type: apiKey in: header name: X-API-Key description: Clave secreta API. Se muestra una sola vez al crearla. parameters: IdempotencyKey: name: Idempotency-Key in: header required: false description: Reutiliza de forma segura una solicitud tras un timeout; mismo valor y mismo cuerpo devuelven el resultado anterior. schema: { type: string, maxLength: 100 } schemas: Product: type: object required: [id, title, description, price, currency, image_url, product_url, listing_url, seller, tags, category, updated_at] properties: id: { type: integer, example: 184 } title: { type: string, maxLength: 500, example: Lámpara de sobremesa restaurada } description: { type: string, maxLength: 16000, example: Luz cálida, base de madera y pantalla de lino. } price: { type: string, pattern: '^\d+(\.\d{1,2})?$', example: '35.00' } currency: { type: string, enum: [EUR], example: EUR } condition: { type: string, maxLength: 190, nullable: true, example: Segunda mano } image_url: { type: string, format: uri, maxLength: 1000 } product_url: { type: string, format: uri, maxLength: 1000, description: URL externa del producto en la tienda vendedora. } listing_url: { type: string, format: uri, description: Ficha pública del producto en Donde lo pillo. } tags: { type: array, maxItems: 50, items: { type: string, maxLength: 100 } } category: { allOf: [{ $ref: '#/components/schemas/Category' }], nullable: true, description: Puede ser null en fichas antiguas aún sin clasificar. } seller: { $ref: '#/components/schemas/Seller' } updated_at: { type: string, format: date-time } ProductInput: type: object required: [title, description, price, image_url, product_url, category] properties: sku: { type: string, maxLength: 190, description: Identificador único del producto dentro del catálogo del vendedor. Si se omite se genera uno. } title: { type: string, maxLength: 500 } description: { type: string, maxLength: 16000 } price: { type: string, pattern: '^\d+(?:[.,]\d{1,2})?$', maxLength: 13, description: Importe no negativo, hasta 9.999.999.999,99 y con un máximo de dos decimales. } currency: { type: string, enum: [EUR], default: EUR, description: Todos los precios deben indicarse en euros. } condition: { type: string, maxLength: 190, nullable: true } tags: oneOf: - type: array maxItems: 50 items: { type: string, maxLength: 100 } - type: string maxLength: 5149 description: Hasta 50 etiquetas. Si se envía como texto, sepáralas con barra vertical, coma o punto y coma. image_url: { type: string, format: uri, maxLength: 1000, description: URL HTTP(S) de la imagen del producto. } product_url: { type: string, format: uri, maxLength: 1000, description: URL HTTP(S) del producto en la tienda vendedora. } category: { type: string, maxLength: 500, description: Ruta activa obtenida de /public/categories, por ejemplo hogar-y-decoracion/iluminacion. } active: { type: boolean, default: true } Seller: type: object required: [name, slug, url] properties: name: { type: string } slug: { type: string } url: { type: string, format: uri } Tag: type: object properties: name: { type: string } slug: { type: string } product_count: { type: integer } Category: type: object required: [id, name, slug, path, url, product_count, children] properties: id: { type: integer } name: { type: string } slug: { type: string } path: { type: string, example: hogar-y-decoracion/iluminacion } url: { type: string, format: uri } product_count: { type: integer, description: Total de productos activos de esta categoría y sus subcategorías. } children: { type: array, items: { $ref: '#/components/schemas/Category' } } CategoryListResponse: type: object required: [success, data, meta] properties: success: { type: boolean, example: true } data: { type: array, items: { $ref: '#/components/schemas/Category' } } meta: type: object properties: count: { type: integer, description: Número de categorías principales. } ProductListResponse: type: object required: [success, data, meta] properties: success: { type: boolean, example: true } data: { type: array, items: { $ref: '#/components/schemas/Product' } } meta: type: object required: [page, limit, total, pages] properties: page: { type: integer } limit: { type: integer } total: { type: integer } pages: { type: integer } ProductResponse: type: object required: [success, data] properties: success: { type: boolean, example: true } data: { $ref: '#/components/schemas/Product' } Error: type: object required: [success, error] properties: success: { type: boolean, example: false } error: type: object required: [code, message] properties: code: { type: string } message: { type: string } responses: WriteSuccess: description: Operación correcta. content: application/json: schema: { type: object, description: Envoltorio success, data y meta según la operación. } Unauthorized: description: Credenciales API ausentes o inválidas. content: application/json: schema: { $ref: '#/components/schemas/Error' } Forbidden: description: La cuenta está suspendida o no tiene un plan de pago activo para sincronizar el catálogo. content: application/json: schema: { $ref: '#/components/schemas/Error' } ServerError: description: Error temporal al guardar la operación. Se puede reintentar con la misma Idempotency-Key cuando proceda. content: application/json: schema: { $ref: '#/components/schemas/Error' } ValidationError: description: Parámetros o datos inválidos. content: application/json: schema: { $ref: '#/components/schemas/Error' } RateLimited: description: Se superó el límite de solicitudes por dirección IP. headers: Retry-After: description: Segundos hasta poder volver a solicitar. schema: { type: integer } content: application/json: schema: { $ref: '#/components/schemas/Error' } NotFound: description: Producto inexistente, inactivo o de vendedor no activo. content: application/json: schema: { $ref: '#/components/schemas/Error' }