{"openapi":"3.1.0","info":{"title":"CeroManual API","version":"1.0.0","description":"Envía mensajes de WhatsApp, transfiere archivos y recibe eventos en tu propio webhook. Referencia legible: https://app.ceromanual.com/reference"},"servers":[{"url":"https://app.ceromanual.com"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Clave API por número (cmk_…), creada en la consola. 401 invalid-api-key si falta, es desconocida o fue revocada."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Código estable del error."},"detail":{"type":"string","description":"Detalle legible (p. ej. rutas de campo)."}}},"SendResult":{"type":"object","required":["status","replayed","messageId","errorCode","errorMessage"],"properties":{"status":{"enum":["succeeded","rejected","unknown"]},"replayed":{"type":"boolean","description":"true si es una repetición idempotente."},"messageId":{"type":["string","null"],"description":"wamid de Meta cuando status=succeeded."},"errorCode":{"type":["string","null"]},"errorMessage":{"type":["string","null"]}}},"TemplateRequest":{"type":"object","additionalProperties":false,"required":["name","language","category","body"],"properties":{"name":{"type":"string","minLength":1,"maxLength":512,"pattern":"^[a-z0-9_]+$","description":"Minúsculas, dígitos y guion bajo."},"language":{"type":"string","pattern":"^[a-z]{2,3}(_[A-Z]{2})?$","description":"Código de idioma de Meta: es_MX, es, en_US, …"},"category":{"enum":["MARKETING","UTILITY"],"description":"AUTHENTICATION no se ofrece (exige cuerpo predefinido + botón OTP). Meta puede recategorizar al revisar."},"body":{"type":"string","minLength":1,"maxLength":1024,"description":"Variables posicionales y consecutivas desde {{1}} — sin huecos ni nombres."},"examples":{"type":"array","maxItems":100,"items":{"type":"string","minLength":1,"maxLength":300},"description":"Obligatorio si el cuerpo tiene variables: un valor de muestra por variable, en orden. Rechazado si no hay variables."}}},"TemplateResult":{"type":"object","required":["id","status","category"],"properties":{"id":{"type":"string","description":"Id de plantilla de Meta."},"status":{"type":"string","description":"Estado de revisión de Meta, literal (normalmente PENDING; la aprobación es asíncrona y se ve en WhatsApp Manager)."},"category":{"type":"string","description":"Categoría que Meta almacenó."}}},"EventEnvelope":{"type":"object","required":["version","delivery_id","event_type","occurred_at","data"],"properties":{"version":{"const":1},"delivery_id":{"type":"string","description":"Estable entre reintentos — deduplica con él."},"event_type":{"enum":["verification","message","message_echo","status"]},"occurred_at":{"type":"string","format":"date-time"},"data":{"type":"object","description":"verification: data vacío ({}) — es solo una prueba de alcance, no hay nada que devolver. message: mensaje entrante (message_id, from?, from_user_id?, from_parent_user_id?, username?, type, contenido; los archivos llevan una URL de /api/media/{id}). message_echo: mensaje que el negocio envió desde la app de WhatsApp Business en un número en coexistencia (message_id, to, type, contenido) — ignora los event_type que no reconozcas. status: { message_id, status: sent|delivered|read|failed|played, recipient_id?, errors? }."}}},"SendRequest":{"description":"Unión etiquetada estricta — los campos desconocidos se rechazan. Los archivos se referencian solo por el id devuelto por POST /api/media.","oneOf":[{"type":"object","additionalProperties":false,"required":["to","type","text"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"text"},"text":{"type":"object","additionalProperties":false,"required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":4096},"preview_url":{"type":"boolean"}}}}},{"type":"object","additionalProperties":false,"required":["to","type","image"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"image"},"image":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string"},"caption":{"type":"string","maxLength":1024}}}}},{"type":"object","additionalProperties":false,"required":["to","type","video"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"video"},"video":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string"},"caption":{"type":"string","maxLength":1024}}}}},{"type":"object","additionalProperties":false,"required":["to","type","audio"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"audio"},"audio":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string"}}}}},{"type":"object","additionalProperties":false,"required":["to","type","document"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"document"},"document":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string"},"caption":{"type":"string","maxLength":1024},"filename":{"type":"string","maxLength":240}}}}},{"type":"object","additionalProperties":false,"required":["to","type","sticker"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"sticker"},"sticker":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string"}}}}},{"type":"object","additionalProperties":false,"required":["to","type","location"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"location"},"location":{"type":"object","additionalProperties":false,"required":["latitude","longitude"],"properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"name":{"type":"string"},"address":{"type":"string"}}}}},{"type":"object","additionalProperties":false,"required":["to","type","contacts"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"contacts"},"contacts":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"object","required":["name"],"properties":{"name":{"type":"object","required":["formatted_name"],"properties":{"formatted_name":{"type":"string"}}}}}}}},{"type":"object","additionalProperties":false,"required":["to","type","reaction"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"reaction"},"reaction":{"type":"object","additionalProperties":false,"required":["message_id","emoji"],"properties":{"message_id":{"type":"string"},"emoji":{"type":"string","description":"Vacío quita la reacción."}}}}},{"type":"object","additionalProperties":false,"required":["to","type","interactive"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"interactive"},"interactive":{"type":"object","required":["type"],"properties":{"type":{"enum":["button","list"]}},"description":"Botones: hasta 3, título ≤ 20. Listas: hasta 10 secciones y 10 filas en total. Ver /reference."}}},{"type":"object","additionalProperties":false,"required":["to","type","template"],"properties":{"to":{"type":"string","pattern":"^\\+?[0-9]{7,15}$","description":"Teléfono del destinatario: 7–15 dígitos, '+' opcional."},"type":{"const":"template"},"template":{"type":"object","additionalProperties":false,"required":["name","language"],"properties":{"name":{"type":"string","minLength":1,"maxLength":512,"pattern":"^[a-z0-9_]+$","description":"Nombre de una plantilla APROBADA en la WABA del número."},"language":{"type":"object","additionalProperties":false,"required":["code"],"properties":{"code":{"type":"string","pattern":"^[a-z]{2,3}(_[A-Z]{2})?$"}}},"components":{"type":"array","maxItems":12,"description":"Rellena las variables de la plantilla aprobada. Reglas que este esquema no puede expresar y el servidor aplica en tiempo de ejecución (400 invalid-request): como máximo UN header y UN body; parámetros del body todos posicionales o todos con parameter_name único; index único por botón. Ver /reference para los detalles legibles.","items":{"oneOf":[{"type":"object","additionalProperties":false,"required":["type","parameters"],"properties":{"type":{"const":"header"},"parameters":{"type":"array","minItems":1,"maxItems":1,"items":{"oneOf":[{"type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"const":"text"},"parameter_name":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[a-z0-9_]+$","description":"Solo para plantillas con variables con nombre; en todas las del body o en ninguna."},"text":{"type":"string","minLength":1,"maxLength":1024}}},{"type":"object","additionalProperties":false,"required":["type","image"],"properties":{"type":{"const":"image"},"image":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string","minLength":1,"maxLength":256}},"description":"Solo por id de POST /api/media — no se aceptan URLs."}}},{"type":"object","additionalProperties":false,"required":["type","video"],"properties":{"type":{"const":"video"},"video":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string","minLength":1,"maxLength":256}},"description":"Solo por id de POST /api/media — no se aceptan URLs."}}},{"type":"object","additionalProperties":false,"required":["type","document"],"properties":{"type":{"const":"document"},"document":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string","minLength":1,"maxLength":256},"filename":{"type":"string","minLength":1,"maxLength":240}}}}}]}}},"description":"Como máximo un header."},{"type":"object","additionalProperties":false,"required":["type","parameters"],"properties":{"type":{"const":"body"},"parameters":{"type":"array","minItems":1,"maxItems":100,"items":{"oneOf":[{"type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"const":"text"},"parameter_name":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[a-z0-9_]+$","description":"Solo para plantillas con variables con nombre; en todas las del body o en ninguna."},"text":{"type":"string","minLength":1,"maxLength":1024}}},{"type":"object","additionalProperties":false,"required":["type","currency"],"properties":{"type":{"const":"currency"},"parameter_name":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[a-z0-9_]+$","description":"Solo para plantillas con variables con nombre; en todas las del body o en ninguna."},"currency":{"type":"object","additionalProperties":false,"required":["fallback_value","code","amount_1000"],"properties":{"fallback_value":{"type":"string","minLength":1,"maxLength":128},"code":{"type":"string","pattern":"^[A-Z]{3}$"},"amount_1000":{"type":"integer"}}}}},{"type":"object","additionalProperties":false,"required":["type","date_time"],"properties":{"type":{"const":"date_time"},"parameter_name":{"type":"string","minLength":1,"maxLength":128,"pattern":"^[a-z0-9_]+$","description":"Solo para plantillas con variables con nombre; en todas las del body o en ninguna."},"date_time":{"type":"object","additionalProperties":false,"required":["fallback_value"],"properties":{"fallback_value":{"type":"string","minLength":1,"maxLength":128}}}}}]},"description":"Todos posicionales (en el orden de {{1}}…{{n}}) o todos con parameter_name único — mezclar o repetir nombres se rechaza."}},"description":"Como máximo un body."},{"type":"object","additionalProperties":false,"required":["type","sub_type","index","parameters"],"properties":{"type":{"const":"button"},"sub_type":{"const":"quick_reply"},"index":{"oneOf":[{"type":"string","pattern":"^[0-9]$"},{"type":"integer","minimum":0,"maximum":9}],"description":"Posición del botón en la plantilla aprobada, desde 0; única por botón."},"parameters":{"type":"array","minItems":1,"maxItems":1,"items":{"type":"object","additionalProperties":false,"required":["type","payload"],"properties":{"type":{"const":"payload"},"payload":{"type":"string","minLength":1,"maxLength":1000}}}}}},{"type":"object","additionalProperties":false,"required":["type","sub_type","index","parameters"],"properties":{"type":{"const":"button"},"sub_type":{"const":"url"},"index":{"oneOf":[{"type":"string","pattern":"^[0-9]$"},{"type":"integer","minimum":0,"maximum":9}],"description":"Posición del botón en la plantilla aprobada, desde 0; única por botón."},"parameters":{"type":"array","minItems":1,"maxItems":1,"items":{"type":"object","additionalProperties":false,"required":["type","text"],"properties":{"type":{"const":"text"},"text":{"type":"string","minLength":1,"maxLength":1000,"description":"El sufijo dinámico de la URL del botón."}}}}}}]}}},"description":"El único tipo enviable FUERA de la ventana de 24 h. Un desajuste con la plantilla aprobada es un 422 definitivo con el código 132xxx de Meta."}}}]}}},"paths":{"/api/send":{"post":{"operationId":"sendMessage","summary":"Enviar un mensaje","description":"Idempotente por (número, Idempotency-Key); el resultado se recuerda 7 días. Cuerpo máx. 64 KB. Fuera de la ventana de 24 h de Meta solo pueden enviarse plantillas aprobadas (type=template); el resto responde 422 con status=rejected.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"Un identificador único por mensaje (usa un UUID)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendRequest"}}}},"responses":{"200":{"description":"Meta aceptó el mensaje (la entrega se confirma por webhook).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendResult"}}}},"400":{"description":"invalid-request — esquema o Idempotency-Key ausente/inválida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"invalid-api-key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"needs-reauth — Meta rechazó las credenciales guardadas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"wrong-state | in-flight | key-reuse.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"payload-too-large — cuerpo > 64 KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Rechazo definitivo (status=rejected con errorCode/errorMessage) — ventana de 24 h cerrada, o plantilla desconocida/pausada/con variables que no coinciden (código 132xxx).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendResult"}}}},"429":{"description":"rate-limited — espera Retry-After segundos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Segundos."}}},"503":{"description":"temporary — reintenta con la misma Idempotency-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Resultado incierto (status=unknown) — Meta pudo haberlo aceptado; no reintentes con otra clave.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendResult"}}}}}}},"/api/templates":{"post":{"operationId":"createTemplate","summary":"Crear una plantilla de mensaje","description":"La plantilla pertenece a la WABA (los números que la comparten comparten plantillas). Sin Idempotency-Key: Meta rechaza por sí mismo un (nombre, idioma) duplicado. Crear no es poder usar: la revisión de Meta es asíncrona — una vez APROBADA se envía con POST /api/send (type=template).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateRequest"}}}},"responses":{"200":{"description":"Plantilla enviada a revisión (status normalmente PENDING).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateResult"}}}},"400":{"description":"invalid-request — rutas de campo en detail.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"invalid-api-key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"needs-reauth — Meta rechazó las credenciales guardadas.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"wrong-state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"payload-too-large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"template-rejected — rechazo definitivo de Meta (nombre duplicado, política); detail lleva motivo, código y subcódigo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"rate-limited — espera Retry-After segundos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Segundos."}}},"503":{"description":"temporary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"unknown — la respuesta de Meta se perdió; la plantilla puede existir. Revisa WhatsApp Manager antes de reintentar.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/media":{"post":{"operationId":"uploadMedia","summary":"Subir un archivo","description":"Bytes crudos como cuerpo (sin multipart). Content-Type = MIME real; Content-Length exacto. Categorías y tamaños máximos en /reference. El id devuelto caduca en Meta a los ~30 días.","requestBody":{"required":true,"content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Archivo aceptado por Meta.","content":{"application/json":{"schema":{"type":"object","required":["mediaId","category"],"properties":{"mediaId":{"type":"string"},"category":{"enum":["image","sticker","audio","video","document"]}}}}}},"401":{"description":"invalid-api-key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"needs-reauth.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"wrong-state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"411":{"description":"length-required — falta Content-Length.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"payload-too-large — declarado u observado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"unsupported-type — MIME fuera de la lista.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"upload-rejected — Meta rechazó el archivo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"too-many-streams o límite de peticiones (Retry-After).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"temporary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"unknown — resultado incierto; volver a subir es seguro.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/media/{id}":{"get":{"operationId":"downloadMedia","summary":"Descargar un archivo recibido","description":"Transmite los bytes (Cache-Control: no-store). Los ids recibidos por webhook caducan en Meta a los ~7 días.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Los bytes del archivo, con su Content-Type real.","content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"invalid-api-key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"needs-reauth.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"not-found — id desconocido o de otro número (indistinguibles a propósito).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"wrong-state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Límite de descargas (Retry-After).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"unsafe-target — el CDN de Meta respondió de forma no permitida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"temporary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"delivery":{"post":{"operationId":"deliverEvent","summary":"Entrega de eventos a tu webhook","description":"Al menos una vez, sin orden — deduplica por delivery_id. Cabeceras: X-Delivery-ID, X-Timestamp, X-Retry-Attempt y X-Signature = hex(HMAC-SHA256(secreto, timestamp + '.' + cuerpo_crudo)). Responde 2xx rápido; reintentos con backoff hasta 24 h. Si la suscripción está impaga o bloqueada, la entrega se pausa (los eventos se encolan hasta 24 h y se reanudan al regularizar el pago). Al guardar la URL enviamos un POST de prueba (event_type=verification); tu endpoint solo tiene que responder 2xx — NO hay que devolver ningún nonce, así que un disparador de n8n/Make/Zapier o cualquier servidor funciona sin cambios. El secreto de firma se entrega en la consola una sola vez después de guardar y solo sirve para verificar la firma (opcional). Si no pudimos alcanzar tu endpoint, el guardado responde 400 invalid-url, 422 unsafe-url, 422 dns-failed (el dominio no resuelve) o 422 verification-failed con reason { kind: unreachable | tls | timeout | non-2xx } y, según el kind, timeoutMs o status.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventEnvelope"}}}},"responses":{"200":{"description":"Recibido; todo 2xx cuenta como entregado."}}}}}}