{"openapi":"3.1.0","info":{"title":"AutomataPy API pública","version":"1.3.0","description":"API REST para conectar tu sistema (ERP, n8n, Zapier, tu backend) con tu CRM de AutomataPy.\n\n**Autenticación:** API key del negocio en el header `X-API-Key: apikey_...` (o `Authorization: Bearer apikey_...`). Se crea en el panel, en Conexiones → n8n / Webhooks → API Keys. La key identifica al negocio: ningún endpoint recibe un tenant_id.\n\n**Alcances de la key:** al crearla, el dueño elige qué puede hacer: `whatsapp` (enviar mensajes y plantillas, bajar la media), `crm:read` (leer contactos, conversaciones y mensajes) y `crm:write` (crear, editar y borrar contactos; incluye `crm:read`). Cada operación indica cuál pide (`x-required-scopes`, alcanza con uno); sin él responde 403 `api_key_scope_missing`. El alcance no se edita: para más permisos se crea otra key.\n\n**Ids de otro negocio** responden 404, igual que un id inexistente.\n\n**Paginación:** los listados usan cursor. Mandá `limit` (1-100, default 50) y, para la página siguiente, el `next_cursor` que devolvió la anterior como `cursor`, con los mismos filtros y el mismo `order`.\n\n**Sincronización incremental:** guardá el `updated_at` más nuevo que viste y la próxima vez pedí `updated_since=<ese valor>` (incluye el borde: puede repetir el último registro).\n\n**Webhooks:** los cambios que hacés por esta API NO disparan los webhooks `contact.created` / `contact.updated` (igual que la edición manual desde el panel): tu sistema ya sabe lo que escribió.\n\n**Límites de uso:** por minuto y por API key, indicados en cada operación (`x-rate-limit`). Al pasarte: 429.\n\n**Versionado:** v1. Cambios que rompen integraciones van a una versión nueva con 12 meses de convivencia (ver política de deprecación). Cada respuesta trae el header `X-API-Version`.","contact":{"email":"soporte@automatapy.com"}},"servers":[{"url":"https://api.automatapy.com","description":"Producción"}],"security":[{"ApiKeyHeader":[]},{"BearerApiKey":[]}],"tags":[{"name":"Contactos","description":"Contactos del CRM y su deal (etapa, monto, fecha de cierre)."},{"name":"Conversaciones","description":"Conversaciones por canal y su historial de mensajes (solo lectura)."},{"name":"WhatsApp","description":"Envío de mensajes por WhatsApp Business (Cloud API) y descarga de media."},{"name":"Meta","description":"Esta especificación."}],"paths":{"/api/v1/contacts":{"get":{"operationId":"listContacts","summary":"Listar contactos","description":"Ordenados por updated_at (desc por defecto). Excluye el contacto interno del probador del bot.","tags":["Contactos"],"x-rate-limit":{"limit":120,"window_seconds":60},"x-required-scopes":["crm:read"],"parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"},{"name":"stage","in":"query","schema":{"type":"string","enum":["new","interested","negotiating","closed","lost"]}},{"name":"updated_since","in":"query","description":"ISO 8601. Contactos con updated_at >= este instante.","schema":{"type":"string","format":"date-time"}},{"name":"q","in":"query","description":"Búsqueda por fragmento (mín. 2 caracteres): nombre, teléfono, email, código de cliente, notas y campos personalizados buscables.","schema":{"type":"string","minLength":2,"maxLength":100}},{"name":"whatsapp","in":"query","description":"Match exacto por WhatsApp (se normaliza igual que al guardar).","schema":{"type":"string","maxLength":40}},{"name":"external_ref","in":"query","description":"Match exacto por tu código de cliente.","schema":{"type":"string","maxLength":120}}],"responses":{"200":{"description":"Página de contactos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createContact","summary":"Crear contacto","description":"Cuenta para el tope de contactos del plan (400 plan_limit_reached_contacts al llegar). Si ya existe un contacto con el mismo WhatsApp, email o external_ref responde 409 con su id en existing_contact_id.","tags":["Contactos"],"x-rate-limit":{"limit":60,"window_seconds":60},"x-required-scopes":["crm:write"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreate"},"example":{"name":"María López","whatsapp":"0981 123 456","stage":"interested","value":1500000,"external_ref":"CLI-0042"}}}},"responses":{"201":{"description":"Contacto creado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/contacts/custom-fields":{"get":{"operationId":"listContactCustomFields","summary":"Campos personalizados de contactos","description":"Las claves que acepta custom_fields, con su tipo y opciones. Los define el dueño en Ajustes. `required` es informativo: el panel lo marca pero no lo exige.","tags":["Contactos"],"x-rate-limit":{"limit":120,"window_seconds":60},"x-required-scopes":["crm:read"],"responses":{"200":{"description":"Definiciones","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CustomFieldDef"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/contacts/{id}":{"get":{"operationId":"getContact","summary":"Obtener un contacto","tags":["Contactos"],"x-rate-limit":{"limit":120,"window_seconds":60},"x-required-scopes":["crm:read"],"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Contacto","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateContact","summary":"Editar un contacto","description":"Edición parcial: solo se tocan los campos presentes. null borra los campos que lo admiten. custom_fields se mergea.","tags":["Contactos"],"x-rate-limit":{"limit":60,"window_seconds":60},"x-required-scopes":["crm:write"],"parameters":[{"$ref":"#/components/parameters/Id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdate"},"example":{"stage":"closed","value":2300000,"custom_fields":{"nro_chip":"0981555111"}}}}},"responses":{"200":{"description":"Contacto actualizado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteContact","summary":"Borrar un contacto","description":"Borrado definitivo, igual que desde el panel: se van con él sus conversaciones y mensajes, sus deudas, sus cobros recurrentes y sus turnos (los que estaban por venir se borran también de Google Calendar, que le avisa la cancelación al cliente si estaba invitado); las ventas registradas quedan sin cliente. La respuesta informa lo que se borró. Irreversible.","tags":["Contactos"],"x-rate-limit":{"limit":60,"window_seconds":60},"x-required-scopes":["crm:write"],"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Contacto borrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactDeletion"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/conversations":{"get":{"operationId":"listConversations","summary":"Listar conversaciones","description":"Ordenadas por updated_at (desc por defecto); cada mensaje nuevo actualiza la conversación.","tags":["Conversaciones"],"x-rate-limit":{"limit":120,"window_seconds":60},"x-required-scopes":["crm:read"],"parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Order"},{"name":"contact_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","schema":{"type":"string","enum":["open","closed","archived"]}},{"name":"channel","in":"query","schema":{"type":"string","enum":["whatsapp","messenger","instagram","web"]}},{"name":"updated_since","in":"query","description":"ISO 8601. Conversaciones con updated_at >= este instante.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Página de conversaciones","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/conversations/{id}/messages":{"get":{"operationId":"listConversationMessages","summary":"Mensajes de una conversación","description":"Ordenados por created_at (asc por defecto: del más viejo al más nuevo). Sin las notas internas (role=system) salvo include_system=true.","tags":["Conversaciones"],"x-rate-limit":{"limit":120,"window_seconds":60},"x-required-scopes":["crm:read"],"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"}},{"name":"since","in":"query","description":"ISO 8601. Mensajes con created_at >= este instante.","schema":{"type":"string","format":"date-time"}},{"name":"include_system","in":"query","description":"Incluir notas internas (acciones del bot, recordatorios).","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Página de mensajes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagePage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/whatsapp/send":{"post":{"operationId":"sendWhatsappText","summary":"Enviar texto por WhatsApp","description":"Solo dentro de la ventana de 24 h desde el último mensaje del cliente (regla de Meta). Fuera de ella, usar send-template.","tags":["WhatsApp"],"x-rate-limit":{"limit":60,"window_seconds":60},"x-required-scopes":["whatsapp"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTextRequest"},"example":{"to":"595981123456","text":"¡Hola! Tu pedido ya salió."}}}},"responses":{"201":{"description":"Enviado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTextResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/whatsapp/send-template":{"post":{"operationId":"sendWhatsappTemplate","summary":"Enviar una plantilla aprobada","description":"Único camino para escribir primero (fuera de la ventana de 24 h). Requiere WhatsApp conectado por Cloud API.\n\n**Plantillas con imagen, video o documento en el encabezado:** mandá `header_media` con `link` (URL pública: la API la descarga, convierte el video a MP4 si hace falta y lo sube a WhatsApp; el link nunca le llega a Meta) o con `id` (un media id de WhatsApp que ya tengas). Sin `header_media` esas plantillas responden 400 `header_media_required`; con el tipo equivocado, 400 `header_media_type_mismatch`. Topes del link: imagen JPG o PNG 5 MB, video 64 MB (sale como MP4 de hasta 16 MB), PDF 100 MB.\n\n**Errores:** si el envío falla la respuesta NO es 201: 422 `template_rejected` (variables o encabezado que no coinciden con la plantilla, plantilla inexistente: corregí el pedido), 422 si el archivo del encabezado no es del formato que acepta WhatsApp o lo rechazó (`header_format_unsupported`, `media_format_rejected`, `header_media_rejected`), 409 fuera de la ventana sin plantilla válida, 413/503 por el tamaño o la conversión del archivo del encabezado, 502 si falló subir el encabezado a WhatsApp (`header_upload_failed`, `header_upload_unavailable`: se puede reintentar) o WhatsApp rechazó o no respondió el envío. El cuerpo trae `error`, `meta_code` y `user_message`.","tags":["WhatsApp"],"x-rate-limit":{"limit":60,"window_seconds":60},"x-required-scopes":["whatsapp"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTemplateRequest"},"example":{"to":"595981123456","template_name":"aviso_pedido","language_code":"es","body_variables":["María","#1042"]}}}},"responses":{"201":{"description":"Enviada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTemplateResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/OutsideWindow"},"413":{"$ref":"#/components/responses/MediaTooLarge"},"422":{"$ref":"#/components/responses/SendUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/WhatsappRejected"},"503":{"$ref":"#/components/responses/MediaBusy"}}}},"/api/v1/whatsapp/send-media":{"post":{"operationId":"sendWhatsappMedia","summary":"Enviar imagen, documento, video, audio, sticker o ubicación","description":"Para media, `url` tiene que ser pública: la API la descarga y la sube a WhatsApp. Para location, latitude + longitude. Dentro de la ventana de 24 h.\n\n**Topes y formatos del archivo de la URL:** imagen JPG o PNG hasta 5 MB; audio MP3, M4A, AAC, AMR u OGG (opus) hasta 16 MB; documento hasta 100 MB; sticker WEBP hasta 100 KB; video hasta 64 MB en cualquier formato común (MP4, MOV de iPhone, HEVC, WEBM, 3GP...).\n\n**Video:** WhatsApp solo acepta MP4 H.264 + AAC de hasta 16 MB. Si el tuyo no cumple, la API lo convierte sola (con videos largos puede tardar hasta un par de minutos) y, si hace falta, baja la calidad para que entre. Si ni así entra, responde 413 `video_too_long`: mandá uno más corto.\n\n**Errores:** si el envío falla la respuesta NO es 201: 413 tamaño, 422 formato o parámetro, 409 ventana de 24 h cerrada (usá send-template), 502 WhatsApp rechazó o no respondió, 503 conversión de video ocupada. El cuerpo trae `error`, `meta_code` y `user_message`.","tags":["WhatsApp"],"x-rate-limit":{"limit":30,"window_seconds":60},"x-required-scopes":["whatsapp"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMediaRequest"},"example":{"to":"595981123456","type":"video","url":"https://ejemplo.com/demo.mov","caption":"Así queda instalado"}}}},"responses":{"201":{"description":"Enviado a WhatsApp (la entrega se confirma después por el webhook de estado)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMediaResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/OutsideWindow"},"413":{"$ref":"#/components/responses/MediaTooLarge"},"422":{"$ref":"#/components/responses/SendUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/WhatsappRejected"},"503":{"$ref":"#/components/responses/MediaBusy"}}}},"/api/v1/whatsapp/send-interactive":{"post":{"operationId":"sendWhatsappInteractive","summary":"Enviar botones, listas o CTA","description":"`interactive` va tal cual lo define WhatsApp Cloud API (button, list, cta_url, ...). Dentro de la ventana de 24 h.","tags":["WhatsApp"],"x-rate-limit":{"limit":60,"window_seconds":60},"x-required-scopes":["whatsapp"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendInteractiveRequest"},"example":{"to":"595981123456","interactive":{"type":"button","body":{"text":"¿Confirmás el turno?"},"action":{"buttons":[{"type":"reply","reply":{"id":"si","title":"Sí"}},{"type":"reply","reply":{"id":"no","title":"No"}}]}}}}}},"responses":{"201":{"description":"Enviado a WhatsApp (la entrega se confirma después por el webhook de estado)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMediaResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/OutsideWindow"},"422":{"$ref":"#/components/responses/SendUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/WhatsappRejected"}}}},"/api/v1/media/{messageId}":{"get":{"operationId":"downloadMessageMedia","summary":"Descargar la media de un mensaje","description":"Bytes de la imagen/audio/video/documento/sticker de un mensaje. Es la URL que viene en `media_url` del webhook y en `media.url` de los mensajes.","tags":["WhatsApp"],"x-rate-limit":{"limit":120,"window_seconds":60},"x-required-scopes":["whatsapp","crm:read"],"parameters":[{"name":"messageId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Archivo. Content-Type es el tipo real (image/jpeg, audio/ogg, application/pdf...).","content":{"application/octet-stream":{"schema":{"type":"string","contentMediaType":"application/octet-stream"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/openapi.json":{"get":{"operationId":"getOpenApiSpec","summary":"Esta especificación (OpenAPI 3.1)","description":"Pública, sin API key. Importable en Postman, Insomnia, n8n o cualquier generador de clientes.","tags":["Meta"],"security":[],"responses":{"200":{"description":"Spec OpenAPI","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}}},"components":{"securitySchemes":{"ApiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key del negocio (apikey_...)."},"BearerApiKey":{"type":"http","scheme":"bearer","description":"La misma API key como Bearer: Authorization: Bearer apikey_..."}},"parameters":{"Id":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"Limit":{"name":"limit","in":"query","description":"Tamaño de página.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},"Cursor":{"name":"cursor","in":"query","description":"next_cursor de la página anterior.","schema":{"type":"string","maxLength":512}},"Order":{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}},"responses":{"BadRequest":{"description":"Request inválido (validación, cursor inválido, límite del plan, WhatsApp no conectado...)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Falta la API key o es inválida / revocada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"La API key no tiene el alcance que pide la operación (api_key_scope_missing; el cuerpo trae required_scopes)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No existe (o es de otro negocio)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"Ya existe un contacto con ese WhatsApp, email o external_ref","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateError"}}}},"RateLimited":{"description":"Demasiadas solicitudes en el último minuto","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"OutsideWindow":{"description":"Pasaron más de 24 h desde el último mensaje del cliente (error outside_24h_window, meta_code 131047). Para escribirle, usá send-template.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"statusCode":409,"message":"send_failed","error":"outside_24h_window","meta_code":131047,"user_message":"Pasaron más de 24 horas desde el último mensaje del cliente. Para escribirle ahora necesitás una plantilla aprobada."}}}},"MediaTooLarge":{"description":"El archivo pasa el tope de su tipo (file_too_large, url_too_large), el video no entra en 16 MB ni bajando la calidad (video_too_large, video_too_long) o WhatsApp lo rechazó por tamaño (media_too_large).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"statusCode":413,"message":"url_too_large","error":"url_too_large","max_bytes":67108864,"user_message":"El archivo de la URL pesa más de 64 MB."}}}},"SendUnprocessable":{"description":"WhatsApp no acepta el contenido así: formato rechazado (media_format_rejected), video ilegible o imposible de convertir (video_unreadable, video_transcode_failed, video_timeout), audio en un formato no soportado (audio_format_unsupported), archivo de encabezado de plantilla en un formato que WhatsApp no acepta (header_format_unsupported) o que rechazó recién subido (header_media_rejected), parámetro inválido como botones de más o textos largos (invalid_parameter) o número que no puede recibir (recipient_unavailable).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"statusCode":422,"message":"send_failed","error":"media_format_rejected","meta_code":131053,"user_message":"WhatsApp no aceptó el formato del video. Mandalo en MP4 (H.264 + AAC) y volvé a adjuntarlo."}}}},"WhatsappRejected":{"description":"WhatsApp rechazó o no respondió el envío (send_failed). meta_code y provider_error traen el detalle de Meta. Se puede reintentar.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"statusCode":502,"message":"send_failed","error":"send_failed","meta_code":131000,"user_message":"WhatsApp no pudo entregar el mensaje. Revisá la conexión en Conexiones o probá con otro número."}}}},"MediaBusy":{"description":"Hay otros videos convirtiéndose (video_queue_full) o la conversión no está disponible (video_processing_unavailable). Reintentá en unos minutos.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["message"],"additionalProperties":true,"properties":{"statusCode":{"type":"integer"},"message":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Código de error estable (ej. contact_not_found, invalid_whatsapp, invalid_cursor) o la lista de errores de validación."},"error":{"type":"string","description":"En los envíos a WhatsApp, el motivo estable para decidir qué hacer (outside_24h_window, media_format_rejected, media_too_large, invalid_parameter, recipient_unavailable, send_failed o el código del paso que falló, como video_unreadable). En otros errores puede ser el nombre del status (ej. \"Bad Request\")."},"user_message":{"type":"string","description":"Explicación en español para mostrar a una persona, cuando la hay."},"meta_code":{"type":["integer","null"],"description":"Código de error de WhatsApp Cloud API (ej. 131047, 131053, 100), cuando el rechazo vino de Meta."},"provider_error":{"type":"string","description":"Respuesta cruda de Meta (recortada), para diagnóstico."}}},"DuplicateError":{"type":"object","required":["message","field","existing_contact_id"],"properties":{"message":{"type":"string","const":"duplicate_contact"},"field":{"type":["string","null"],"enum":["whatsapp","email","external_ref",null]},"existing_contact_id":{"type":["string","null"],"format":"uuid","description":"El contacto que ya tiene ese dato (null si no se pudo determinar)."},"user_message":{"type":"string"}}},"Contact":{"type":"object","required":["id","name","whatsapp","email","stage","status","value","closed_at","tags","notes","assigned_to","custom_fields","external_ref","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"whatsapp":{"type":["string","null"],"description":"E.164 sin \"+\"."},"email":{"type":["string","null"]},"stage":{"type":"string","enum":["new","interested","negotiating","closed","lost"]},"status":{"type":"string","enum":["active","inactive"]},"value":{"type":["number","null"],"description":"Monto del deal."},"closed_at":{"type":["string","null"],"format":"date-time","description":"Última vez que pasó a closed (lo mantiene el sistema)."},"tags":{"type":"array","items":{"type":"string"}},"notes":{"type":["string","null"]},"assigned_to":{"type":["string","null"],"format":"uuid"},"custom_fields":{"type":"object","additionalProperties":true},"external_ref":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ContactCreate":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"whatsapp":{"type":["string","null"],"maxLength":40,"description":"Se guarda en E.164 sin \"+\" con las mismas reglas del panel: \"0981 123 456\" → \"595981123456\". Un número imposible da 400 invalid_whatsapp. null lo borra."},"email":{"type":["string","null"],"format":"email","maxLength":254},"stage":{"type":"string","enum":["new","interested","negotiating","closed","lost"],"description":"Etapa del pipeline (el \"deal\"). Pasar a closed sella closed_at; salir de closed lo limpia."},"value":{"type":["number","null"],"minimum":0,"maximum":9999999999.99,"description":"Monto del deal, hasta 2 decimales. null lo borra."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":60},"description":"Reemplaza la lista completa. Se trimean y deduplican."},"notes":{"type":["string","null"],"maxLength":10000},"custom_fields":{"type":"object","additionalProperties":true,"description":"Campos personalizados del negocio: { key: valor } con las claves de GET /api/v1/contacts/custom-fields. Tipos: number → número; date → \"YYYY-MM-DD\"; select → una de sus opciones; text/phone → texto. Una clave desconocida da 400 unknown_custom_field; un valor inválido, 400 invalid_custom_field. null o \"\" borra la clave. En PATCH se MERGEA: las claves que no mandás quedan como están."},"external_ref":{"type":["string","null"],"maxLength":120,"description":"Tu id del contacto (código de cliente). Único por negocio: repetido da 409."},"assigned_to":{"type":["string","null"],"format":"uuid","description":"Usuario del equipo responsable. Recibe el aviso \"Te asignaron un cliente\". null lo desasigna."}}},"ContactUpdate":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"whatsapp":{"type":["string","null"],"maxLength":40,"description":"Se guarda en E.164 sin \"+\" con las mismas reglas del panel: \"0981 123 456\" → \"595981123456\". Un número imposible da 400 invalid_whatsapp. null lo borra."},"email":{"type":["string","null"],"format":"email","maxLength":254},"stage":{"type":"string","enum":["new","interested","negotiating","closed","lost"],"description":"Etapa del pipeline (el \"deal\"). Pasar a closed sella closed_at; salir de closed lo limpia."},"value":{"type":["number","null"],"minimum":0,"maximum":9999999999.99,"description":"Monto del deal, hasta 2 decimales. null lo borra."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":60},"description":"Reemplaza la lista completa. Se trimean y deduplican."},"notes":{"type":["string","null"],"maxLength":10000},"custom_fields":{"type":"object","additionalProperties":true,"description":"Campos personalizados del negocio: { key: valor } con las claves de GET /api/v1/contacts/custom-fields. Tipos: number → número; date → \"YYYY-MM-DD\"; select → una de sus opciones; text/phone → texto. Una clave desconocida da 400 unknown_custom_field; un valor inválido, 400 invalid_custom_field. null o \"\" borra la clave. En PATCH se MERGEA: las claves que no mandás quedan como están."},"external_ref":{"type":["string","null"],"maxLength":120,"description":"Tu id del contacto (código de cliente). Único por negocio: repetido da 409."},"assigned_to":{"type":["string","null"],"format":"uuid","description":"Usuario del equipo responsable. Recibe el aviso \"Te asignaron un cliente\". null lo desasigna."}}},"ContactPage":{"type":"object","description":"Página de resultados. Si has_more es true, pedí la siguiente con cursor=next_cursor (mismos filtros y order).","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"next_cursor":{"type":["string","null"]},"has_more":{"type":"boolean"}}},"ContactDeletion":{"type":"object","required":["id","deleted","impact"],"properties":{"id":{"type":"string","format":"uuid"},"deleted":{"type":"boolean","const":true},"impact":{"type":"object","required":["pending_debts_deleted","pending_debts_total_pyg","active_recurring_charges_deleted","sales_unlinked","upcoming_appointments_deleted"],"properties":{"pending_debts_deleted":{"type":"integer","description":"Deudas que el negocio todavía esperaba cobrar."},"pending_debts_total_pyg":{"type":"integer","description":"Suma de esas deudas, en guaraníes."},"active_recurring_charges_deleted":{"type":"integer"},"sales_unlinked":{"type":"integer","description":"Ventas registradas que quedan sin cliente (no se borran)."},"upcoming_appointments_deleted":{"type":"integer","description":"Turnos pendientes o confirmados que todavía no habían pasado. Su evento de Google Calendar también se borra."}}}}},"CustomFieldDef":{"type":"object","required":["key","label","type","options","required"],"properties":{"key":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,39}$"},"label":{"type":"string"},"type":{"type":"string","enum":["text","number","date","select","phone"]},"options":{"type":"array","items":{"type":"string"},"description":"Solo para type=select."},"required":{"type":"boolean"}}},"Conversation":{"type":"object","required":["id","contact_id","channel","status","needs_human","bot_enabled","bot_paused_until","last_message_at","last_message_preview","last_message_role","message_count","unread_count","started_at","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"contact_id":{"type":"string","format":"uuid"},"channel":{"type":"string","enum":["whatsapp","messenger","instagram","web"]},"status":{"type":"string","enum":["open","closed","archived"]},"needs_human":{"type":"boolean","description":"El bot la derivó a una persona."},"bot_enabled":{"type":"boolean"},"bot_paused_until":{"type":["string","null"],"format":"date-time"},"last_message_at":{"type":["string","null"],"format":"date-time"},"last_message_preview":{"type":["string","null"]},"last_message_role":{"type":["string","null"]},"message_count":{"type":"integer"},"unread_count":{"type":"integer"},"started_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ConversationPage":{"type":"object","description":"Página de resultados. Si has_more es true, pedí la siguiente con cursor=next_cursor (mismos filtros y order).","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Conversation"}},"next_cursor":{"type":["string","null"]},"has_more":{"type":"boolean"}}},"Message":{"type":"object","required":["id","conversation_id","role","direction","type","body","channel","status","status_updated_at","external_id","sender_user_id","media","location","interactive_reply_id","template_name","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"conversation_id":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["user","assistant","agent","system"],"description":"user = el cliente; assistant = el bot; agent = una persona del equipo; system = nota interna."},"direction":{"type":"string","enum":["inbound","outbound","internal"]},"type":{"type":"string","examples":["text","image","audio","document","location","interactive","template"]},"body":{"type":"string","description":"Texto del mensaje. En media es el caption o un rótulo ([Imagen]); en notas de voz, la transcripción si la hay."},"channel":{"type":"string","enum":["whatsapp","messenger","instagram","web"]},"status":{"type":["string","null"],"enum":["sent","delivered","read","failed",null],"description":"Entrega de los salientes."},"status_updated_at":{"type":["string","null"],"format":"date-time"},"external_id":{"type":["string","null"],"description":"Id en el proveedor (wamid)."},"sender_user_id":{"type":["string","null"],"format":"uuid","description":"Usuario del equipo que lo envió desde el panel."},"media":{"oneOf":[{"type":"null"},{"type":"object","required":["kind","url","mime_type","size_bytes","filename","caption"],"properties":{"kind":{"type":"string","enum":["image","audio","video","document","sticker"]},"url":{"type":["string","null"],"description":"GET con tu API key para bajar los bytes."},"mime_type":{"type":["string","null"]},"size_bytes":{"type":["integer","null"]},"filename":{"type":["string","null"]},"caption":{"type":["string","null"]}}}]},"location":{"oneOf":[{"type":"null"},{"type":"object","required":["latitude","longitude","name","address"],"properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"name":{"type":["string","null"]},"address":{"type":["string","null"]}}}]},"interactive_reply_id":{"type":["string","null"],"description":"Id del botón u opción que eligió el cliente."},"template_name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}},"MessagePage":{"type":"object","description":"Página de resultados. Si has_more es true, pedí la siguiente con cursor=next_cursor (mismos filtros y order).","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"next_cursor":{"type":["string","null"]},"has_more":{"type":"boolean"}}},"SendTextRequest":{"type":"object","required":["to","text"],"additionalProperties":false,"properties":{"to":{"type":"string","description":"Número en E.164 sin \"+\" (595981123456)."},"text":{"type":"string","minLength":1},"contact_id":{"type":"string","format":"uuid","description":"Opcional: contacto existente. Si no, se resuelve (o se crea) por el número."},"metadata":{"type":"object","additionalProperties":true,"description":"Datos tuyos para guardar con el mensaje."}}},"SendTextResult":{"type":"object","required":["ok","provider","channel","contact_id","conversation_id","message_id","whatsapp_message_id"],"properties":{"ok":{"type":"boolean","const":true},"provider":{"type":"string","examples":["meta_cloud"]},"channel":{"type":"string","examples":["whatsapp"]},"contact_id":{"type":"string","format":"uuid"},"conversation_id":{"type":"string","format":"uuid"},"message_id":{"type":"string","format":"uuid","description":"Id del mensaje en el CRM (sirve para GET /api/v1/conversations/{id}/messages)."},"whatsapp_message_id":{"type":["string","null"],"description":"wamid de WhatsApp."},"auto_assigned":{"type":"boolean","description":"Presente (true) si el envío asignó el contacto a alguien."}}},"SendTemplateRequest":{"type":"object","required":["to","template_name","language_code"],"additionalProperties":false,"properties":{"to":{"type":"string","description":"Número en E.164 sin \"+\"."},"template_name":{"type":"string","description":"Nombre de la plantilla aprobada en Meta."},"language_code":{"type":"string","description":"Idioma aprobado (es, en_US...)."},"body_variables":{"type":"array","items":{"type":"string"},"description":"Valores de {{1}}, {{2}}... del cuerpo, en orden."},"url_button_param":{"type":"string","description":"Parte dinámica del primer botón URL, si la plantilla lo tiene."},"contact_id":{"type":"string","format":"uuid"},"header_media":{"type":"object","required":["type"],"additionalProperties":false,"description":"Obligatorio si la plantilla tiene encabezado de imagen, video o documento. Lleva `link` o `id`, uno de los dos.","properties":{"type":{"type":"string","enum":["image","video","document"],"description":"Tiene que coincidir con el encabezado de la plantilla."},"link":{"type":"string","format":"uri","maxLength":2048,"description":"URL pública del archivo (http/https, puerto 80 o 443; no se aceptan direcciones internas)."},"id":{"type":"string","maxLength":200,"description":"Media id de WhatsApp ya subido (dura ~30 días)."},"filename":{"type":"string","maxLength":240,"description":"Solo document: el nombre que ve el cliente."}}},"metadata":{"type":"object","additionalProperties":true}}},"SendTemplateResult":{"type":"object","required":["ok","provider","channel","contact_id","conversation_id","message_id","whatsapp_message_id"],"properties":{"ok":{"type":"boolean","const":true},"provider":{"type":"string","examples":["meta_cloud"]},"channel":{"type":"string","examples":["whatsapp"]},"contact_id":{"type":"string","format":"uuid"},"conversation_id":{"type":"string","format":"uuid"},"message_id":{"type":"string","format":"uuid","description":"Id del mensaje en el CRM (sirve para GET /api/v1/conversations/{id}/messages)."},"whatsapp_message_id":{"type":["string","null"],"description":"wamid de WhatsApp."}}},"SendMediaRequest":{"type":"object","required":["to","type"],"additionalProperties":false,"properties":{"to":{"type":"string","description":"Número en E.164 sin \"+\"."},"type":{"type":"string","enum":["image","document","video","audio","sticker","location"]},"url":{"type":"string","format":"uri","description":"URL pública del archivo (http/https, puerto 80 o 443; no se aceptan direcciones internas). Obligatoria salvo type=location. Topes: imagen 5 MB, audio 16 MB, video 64 MB (se convierte a MP4 de hasta 16 MB), documento 100 MB, sticker 100 KB."},"caption":{"type":"string","maxLength":1024,"description":"Texto debajo del archivo. Solo image, video y document."},"filename":{"type":"string","maxLength":255,"description":"Solo document."},"latitude":{"type":"number","minimum":-90,"maximum":90,"description":"Obligatoria con type=location."},"longitude":{"type":"number","minimum":-180,"maximum":180,"description":"Obligatoria con type=location."},"location_name":{"type":"string","maxLength":200},"location_address":{"type":"string","maxLength":500}}},"SendInteractiveRequest":{"type":"object","required":["to","interactive"],"additionalProperties":false,"properties":{"to":{"type":"string","description":"Número en E.164 sin \"+\"."},"interactive":{"type":"object","additionalProperties":true,"description":"Objeto interactive de WhatsApp Cloud API."}}},"SendMediaResult":{"type":"object","required":["messageId","error","dbMessageId"],"properties":{"messageId":{"type":["string","null"],"description":"wamid de WhatsApp."},"error":{"type":["string","null"],"description":"Siempre null: si el envío falla, la respuesta es 4xx/5xx con el motivo (ver Error)."},"dbMessageId":{"type":["string","null"],"format":"uuid","description":"Id del mensaje en el CRM."}}}}}}