{"openapi":"3.1.0","info":{"title":"QRfolio API","version":"1.0.0","description":"API para crear y administrar códigos QR dinámicos. La facturación y cuota API son independientes de los créditos utilizados en la aplicación web."},"servers":[{"url":"/api/v1","description":"Servidor actual"}],"tags":[{"name":"QR","description":"Creación y administración de recursos QR"},{"name":"Images","description":"Descarga de imágenes listas para usar"}],"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","bearerFormat":"qrf_live_...","description":"API key creada en /app/api. Envíala únicamente desde un servidor."}},"parameters":{"Page":{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},"PageSize":{"in":"query","name":"pageSize","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}},"schemas":{"Error":{"type":"object","required":["statusCode","message"],"properties":{"statusCode":{"type":"integer","example":400},"message":{"type":"string","example":"Datos inválidos"},"error":{"type":"string","example":"Bad Request"}}},"BankPayload":{"type":"object","additionalProperties":false,"required":["beneficiary","bank","clabe"],"properties":{"beneficiary":{"type":"string","minLength":2,"maxLength":100,"example":"Ana Martínez"},"bank":{"type":"string","minLength":2,"maxLength":60,"example":"BBVA"},"clabe":{"type":"string","pattern":"^\\d{18}$","description":"CLABE mexicana válida de 18 dígitos","example":"032180000118359719"},"account":{"type":"string","pattern":"^\\d{5,20}$","example":"0123456789"}}},"RedirectPayload":{"type":"object","additionalProperties":false,"required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"URL pública HTTP/HTTPS. No se admiten hosts privados, credenciales ni el dominio de QRfolio.","example":"https://example.com/catalogo"}}},"ContentPayload":{"type":"object","additionalProperties":false,"required":["text"],"properties":{"text":{"type":"string","minLength":1,"maxLength":10000,"example":"Horario: lunes a viernes de 9:00 a 18:00"}}},"WhatsappPayload":{"type":"object","additionalProperties":false,"required":["phone"],"properties":{"phone":{"type":"string","pattern":"^\\+[1-9]\\d{7,14}$","description":"Teléfono internacional en formato E.164","example":"+525512345678"},"message":{"type":"string","maxLength":1000,"example":"Hola, necesito información"}}},"VcardPayload":{"type":"object","additionalProperties":false,"required":["fullName"],"properties":{"fullName":{"type":"string","minLength":1,"maxLength":100,"example":"Ana Martínez"},"jobTitle":{"type":"string","maxLength":100,"example":"Directora comercial"},"company":{"type":"string","maxLength":100,"example":"Empresa Demo"},"phones":{"type":"array","maxItems":3,"uniqueItems":true,"items":{"type":"string","pattern":"^\\+[1-9]\\d{7,14}$"},"example":["+525512345678"]},"email":{"type":"string","format":"email","maxLength":254,"example":"ana@example.com"},"website":{"type":"string","format":"uri","maxLength":2048,"example":"https://example.com"}}},"Link":{"type":"object","additionalProperties":false,"required":["label","url"],"properties":{"label":{"type":"string","minLength":1,"maxLength":80,"example":"Sitio oficial"},"url":{"type":"string","format":"uri","maxLength":2048,"example":"https://example.com"},"ageRestricted":{"type":"boolean","description":"Marca contenido para mayores de edad. Algunos dominios se clasifican automáticamente.","example":false}}},"LinksPayload":{"type":"object","additionalProperties":false,"required":["links"],"properties":{"links":{"type":"array","minItems":1,"maxItems":20,"items":{"$ref":"#/components/schemas/Link"}}}},"CreateQr":{"oneOf":[{"type":"object","additionalProperties":false,"required":["title","type","payload"],"properties":{"title":{"type":"string","minLength":1,"maxLength":80},"type":{"const":"bank"},"payload":{"$ref":"#/components/schemas/BankPayload"}}},{"type":"object","additionalProperties":false,"required":["title","type","payload"],"properties":{"title":{"type":"string","minLength":1,"maxLength":80},"type":{"const":"redirect"},"payload":{"$ref":"#/components/schemas/RedirectPayload"}}},{"type":"object","additionalProperties":false,"required":["title","type","payload"],"properties":{"title":{"type":"string","minLength":1,"maxLength":80},"type":{"const":"content"},"payload":{"$ref":"#/components/schemas/ContentPayload"}}},{"type":"object","additionalProperties":false,"required":["title","type","payload"],"properties":{"title":{"type":"string","minLength":1,"maxLength":80},"type":{"const":"whatsapp"},"payload":{"$ref":"#/components/schemas/WhatsappPayload"}}},{"type":"object","additionalProperties":false,"required":["title","type","payload"],"properties":{"title":{"type":"string","minLength":1,"maxLength":80},"type":{"const":"vcard"},"payload":{"$ref":"#/components/schemas/VcardPayload"}}},{"type":"object","additionalProperties":false,"required":["title","type","payload"],"properties":{"title":{"type":"string","minLength":1,"maxLength":80},"type":{"const":"links"},"payload":{"$ref":"#/components/schemas/LinksPayload"}}}],"discriminator":{"propertyName":"type"}},"EditQr":{"type":"object","additionalProperties":false,"required":["title","payload","version"],"properties":{"title":{"type":"string","minLength":1,"maxLength":80},"payload":{"type":"object","description":"Debe conservar el esquema correspondiente al tipo original del QR."},"version":{"type":"integer","minimum":1,"description":"Versión obtenida en la consulta más reciente."}}},"ActiveQr":{"type":"object","additionalProperties":false,"required":["active"],"properties":{"active":{"type":"boolean","example":false}}},"Quota":{"type":"object","required":["subscriptionId","quota","used","periodEnd","unlimited"],"properties":{"subscriptionId":{"type":"string","format":"uuid"},"quota":{"type":"integer","description":"0 cuando el plan es ilimitado"},"used":{"type":"integer"},"periodEnd":{"type":"string","format":"date-time"},"unlimited":{"type":"boolean"}}},"Qr":{"type":"object","required":["id","slug","type","title","payload","active","version","url","image_png_url","image_svg_url"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"type":{"type":"string","enum":["bank","redirect","content","whatsapp","vcard","links"]},"title":{"type":"string"},"payload":{"type":"object"},"active":{"type":"boolean"},"effective_active":{"type":"boolean"},"version":{"type":"integer"},"source":{"const":"api"},"cost":{"const":0},"url":{"type":"string","format":"uri"},"image_png_url":{"type":"string","format":"uri"},"image_svg_url":{"type":"string","format":"uri"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CreatedQr":{"allOf":[{"$ref":"#/components/schemas/Qr"},{"type":"object","required":["quota"],"properties":{"quota":{"$ref":"#/components/schemas/Quota"}}}]},"QrPage":{"type":"object","required":["items","total","page","pageSize","pages"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Qr"}},"total":{"type":"integer"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"pages":{"type":"integer"}}}}},"security":[{"ApiKey":[]}],"paths":{"/qrs":{"get":{"tags":["QR"],"summary":"Listar QR","description":"Devuelve únicamente recursos creados mediante la API.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"}],"responses":{"200":{"description":"Listado paginado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrPage"}}}},"400":{"description":"Solicitud o payload inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key inválida, expirada o revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope insuficiente, cuenta bloqueada o acceso API inactivo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso no encontrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Cuota mensual o límite por minuto agotado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["QR"],"summary":"Crear un QR","description":"Consume una creación de la cuota del periodo. Los reintentos con la misma llave y el mismo contenido no vuelven a consumir cuota.","parameters":[{"in":"header","name":"Idempotency-Key","required":true,"description":"Identificador ASCII único por operación, máximo 128 caracteres.","schema":{"type":"string","maxLength":128},"example":"pedido-8472"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQr"}}}},"responses":{"201":{"description":"QR creado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedQr"}}}},"400":{"description":"Solicitud o payload inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key inválida, expirada o revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope insuficiente, cuenta bloqueada o acceso API inactivo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso no encontrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"La misma Idempotency-Key fue utilizada con datos diferentes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Cuota mensual o límite por minuto agotado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/qrs/{id}":{"get":{"tags":["QR"],"summary":"Consultar un QR","parameters":[{"in":"path","name":"id","required":true,"description":"UUID del recurso QR","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Recurso QR","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Qr"}}}},"400":{"description":"Solicitud o payload inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key inválida, expirada o revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope insuficiente, cuenta bloqueada o acceso API inactivo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso no encontrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Cuota mensual o límite por minuto agotado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["QR"],"summary":"Editar un QR","description":"No consume cuota. El tipo no puede cambiar y version implementa control de concurrencia optimista.","parameters":[{"in":"path","name":"id","required":true,"description":"UUID del recurso QR","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditQr"}}}},"responses":{"200":{"description":"QR actualizado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Qr"}}}},"400":{"description":"Solicitud o payload inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key inválida, expirada o revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope insuficiente, cuenta bloqueada o acceso API inactivo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso no encontrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"La versión enviada ya no es la actual","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Cuota mensual o límite por minuto agotado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/qrs/{id}/active":{"patch":{"tags":["QR"],"summary":"Activar o desactivar","description":"No consume cuota. Un QR desactivado deja de resolver públicamente.","parameters":[{"in":"path","name":"id","required":true,"description":"UUID del recurso QR","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveQr"}}}},"responses":{"200":{"description":"Estado actualizado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Qr"}}}},"400":{"description":"Solicitud o payload inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key inválida, expirada o revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope insuficiente, cuenta bloqueada o acceso API inactivo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso no encontrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Cuota mensual o límite por minuto agotado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/qrs/{id}/image.png":{"get":{"tags":["Images"],"summary":"Descargar PNG","parameters":[{"in":"path","name":"id","required":true,"description":"UUID del recurso QR","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Imagen PNG de 1024 px","content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Solicitud o payload inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key inválida, expirada o revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope insuficiente, cuenta bloqueada o acceso API inactivo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso no encontrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Cuota mensual o límite por minuto agotado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/qrs/{id}/image.svg":{"get":{"tags":["Images"],"summary":"Descargar SVG","parameters":[{"in":"path","name":"id","required":true,"description":"UUID del recurso QR","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Imagen SVG vectorial","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"400":{"description":"Solicitud o payload inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"API key inválida, expirada o revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope insuficiente, cuenta bloqueada o acceso API inactivo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso no encontrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Cuota mensual o límite por minuto agotado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}