{"openapi":"3.1.0","info":{"title":"Contrato da API de WhatsApp (Marketing · LED Cloud)","version":"1.0.0","description":"Contrato que o app **Marketing** da LED Cloud usa para enviar campanhas de WhatsApp pela conexão **própria** da organização.\n\nQualquer servidor que implemente os endpoints abaixo funciona. O formato segue a **Evolution API (v1/v2)**, então uma Evolution\npareada funciona sem adaptação; outras APIs precisam apenas expor as mesmas rotas, campos e códigos de erro.\n\n**A API de WhatsApp não está inclusa na assinatura do Marketing: é um serviço à parte.** Você pode usar a sua própria API\n(compatível com este contrato) ou contratar um servidor de API com a LED Cloud.\n\n### Como o app chama\n- O cliente informa em *Conexões*: **URL base** (`https://`, endereço público), **nome da instância** e **apikey**.\n- Toda chamada leva o cabeçalho `apikey`. A chave fica criptografada no Marketing e nunca é exibida nem registrada em logs.\n- Todos os caminhos levam o nome da instância (`{instance}`).\n- O app não segue redirecionamentos e usa timeouts curtos (15 s no estado, 20 s nos demais).\n\n### Como o app classifica erros de envio\n| Situação | Resposta esperada | Efeito no app |\n|---|---|---|\n| apikey inválida | `401` ou `403` | pausa a campanha (erro do canal) e marca a conexão com falha |\n| instância inexistente | `404` com \"instance\" no corpo | pausa a campanha (erro do canal) |\n| instância desconectada | qualquer status com \"Connection Closed\" / \"not connected\" no corpo | pausa a campanha (erro do canal) |\n| número sem WhatsApp | `400` com `exists: false` no corpo | falha só daquele destinatário |\n| indisponível ou limitando | `429`, `5xx` ou sem resposta | repete depois (erro transitório) |\n| outra recusa `4xx` | `4xx` | falha só daquele destinatário |\n\n### Webhook de entrada\nO Marketing expõe uma URL pública por conexão (`/api/whatsapp/webhook/{token}`) e a registra em `POST /webhook/set/{instance}`.\nO servidor de WhatsApp deve enviar a essa URL os eventos descritos em **Webhooks de entrada** (corpo JSON, `POST`).\nO segredo é o próprio token da URL.\n\n### Conformidade\nDisparo em massa por WhatsApp pode levar ao **banimento do número**. O app impõe intervalo mínimo, limite diário e consentimento,\nmas a responsabilidade pelo número e pela base legal é de quem contrata. Veja `/whatsapp-api` no app.\n"},"servers":[{"url":"https://{host}","description":"Servidor de API do cliente (a URL base pode ter um prefixo de caminho).","variables":{"host":{"default":"api.exemplo.com.br"}}}],"security":[{"apikey":[]}],"tags":[{"name":"Instância"},{"name":"Mensagens"},{"name":"Webhook"}],"paths":{"/instance/connectionState/{instance}":{"get":{"tags":["Instância"],"summary":"Estado da conexão da instância","description":"Usado por \"Testar conexão\" e antes de cada disparo. A instância só está apta quando `state` é `open`.","operationId":"connectionState","parameters":[{"$ref":"#/components/parameters/instance"}],"responses":{"200":{"description":"Estado atual (`open` = pareada e conectada).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionState"},"example":{"instance":{"instanceName":"minha-instancia","state":"open"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/InstanceNotFound"}}}},"/message/sendText/{instance}":{"post":{"tags":["Mensagens"],"summary":"Envia uma mensagem de texto","description":"Envia **uma** mensagem de texto. O app envia uma por vez, respeitando intervalo mínimo e limite diário.\nO número vai em formato internacional, só dígitos (ex.: `5511999998888`).\nA resposta deve trazer o id da mensagem em `key.id` (usado para casar entrega e leitura).\n","operationId":"sendText","parameters":[{"$ref":"#/components/parameters/instance"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTextRequest"},"example":{"number":"5511999998888","text":"Olá, Maria! Responda SAIR para não receber mais mensagens."}}}},"responses":{"200":{"$ref":"#/components/responses/SendOk"},"201":{"$ref":"#/components/responses/SendOk"},"400":{"description":"Número sem WhatsApp (falha só daquele destinatário) ou requisição inválida.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"status":400,"error":"Bad Request","response":{"message":[{"jid":"5511999998888@s.whatsapp.net","exists":false,"number":"5511999998888"}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/InstanceNotFound"},"429":{"description":"Limitando. O app tenta de novo depois."},"500":{"description":"Erro do servidor ou instância desconectada (\"Connection Closed\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"status":500,"error":"Internal Server Error","response":{"message":"Connection Closed"}}}}}}}},"/webhook/set/{instance}":{"post":{"tags":["Webhook"],"summary":"Registra o webhook de entrada","description":"Chamado por \"Gerar e registrar webhook\". Se a sua API não tiver esta rota (ou responder erro), o app mostra a URL\npara você cadastrar manualmente; o restante continua funcionando.\n","operationId":"setWebhook","parameters":[{"$ref":"#/components/parameters/instance"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetWebhookRequest"},"example":{"webhook":{"enabled":true,"url":"https://marketing.ledcloud.com.br/api/whatsapp/webhook/TOKEN","byEvents":false,"base64":false,"events":["MESSAGES_UPSERT","MESSAGES_UPDATE"]}}}}},"responses":{"200":{"$ref":"#/components/responses/WebhookOk"},"201":{"$ref":"#/components/responses/WebhookOk"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/InstanceNotFound"}}}}},"webhooks":{"messagesUpsert":{"post":{"tags":["Webhook"],"summary":"Mensagem recebida de um contato","description":"Enviada ao Marketing quando um contato escreve para a instância. O app só usa o **fato** da resposta e, se o texto for\n**SAIR, PARAR, STOP ou DESCADASTRAR** (a palavra sozinha), suprime o número na hora. O texto nunca é guardado.\nMensagens com `fromMe: true` são ignoradas.\n","operationId":"onMessagesUpsert","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagesUpsertEvent"},"example":{"event":"messages.upsert","instance":"minha-instancia","data":{"key":{"remoteJid":"5511999998888@s.whatsapp.net","fromMe":false,"id":"ABCD1234"},"message":{"conversation":"SAIR"}}}}}},"responses":{"200":{"description":"Recebido (o Marketing sempre responde 200 a eventos válidos, inclusive irrelevantes, para a API não reenviar)."}}}},"messagesUpdate":{"post":{"tags":["Webhook"],"summary":"Confirmação de entrega ou leitura","description":"Enviada quando uma mensagem **enviada por nós** é entregue ou lida. `keyId` é o `key.id` devolvido por `sendText`.\nStatus aceitos: `DELIVERY_ACK`/`DELIVERED` (entregue) e `READ`/`PLAYED` (lida).\n","operationId":"onMessagesUpdate","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagesUpdateEvent"},"example":{"event":"messages.update","instance":"minha-instancia","data":{"keyId":"3EB0ABCDEF","fromMe":true,"status":"DELIVERY_ACK"}}}}},"responses":{"200":{"description":"Recebido."}}}}},"components":{"securitySchemes":{"apikey":{"type":"apiKey","in":"header","name":"apikey","description":"Chave da instância/servidor. Criptografada no Marketing; nunca logada."}},"parameters":{"instance":{"name":"instance","in":"path","required":true,"description":"Nome da instância (o mesmo informado em Conexões).","schema":{"type":"string","minLength":1,"maxLength":100}}},"responses":{"SendOk":{"description":"Mensagem aceita para envio.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTextResponse"},"example":{"key":{"remoteJid":"5511999998888@s.whatsapp.net","fromMe":true,"id":"3EB0ABCDEF"},"status":"PENDING","message":{"conversation":"Olá, Maria!"}}}}},"WebhookOk":{"description":"Webhook registrado.","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"type":"object","properties":{"url":{"type":"string"},"enabled":{"type":"boolean"}}}}}}}},"Unauthorized":{"description":"apikey recusada (erro do canal: pausa a campanha).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"status":401,"error":"Unauthorized","response":{"message":"Unauthorized"}}}}},"InstanceNotFound":{"description":"Instância inexistente (o corpo deve citar \"instance\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"status":404,"error":"Not Found","response":{"message":["The \"minha-instancia\" instance does not exist"]}}}}}},"schemas":{"ConnectionState":{"type":"object","description":"O app aceita `instance.state` (Evolution) ou `state` na raiz.","properties":{"instance":{"type":"object","properties":{"instanceName":{"type":"string"},"state":{"type":"string","description":"`open` = conectada. Qualquer outro valor (`close`, `connecting`...) é tratado como desconectada.","example":"open"}}},"state":{"type":"string"}}},"SendTextRequest":{"type":"object","required":["number","text"],"properties":{"number":{"type":"string","pattern":"^[0-9]{10,15}$","description":"Número internacional, só dígitos."},"text":{"type":"string","minLength":1,"maxLength":4096}}},"SendTextResponse":{"type":"object","description":"O id pode vir em `key.id` (preferido), `messageId` ou `id`.","properties":{"key":{"type":"object","properties":{"remoteJid":{"type":"string"},"fromMe":{"type":"boolean"},"id":{"type":"string"}}},"messageId":{"type":"string"},"id":{"type":"string"},"status":{"type":"string"}}},"SetWebhookRequest":{"type":"object","required":["webhook"],"properties":{"webhook":{"type":"object","required":["enabled","url"],"properties":{"enabled":{"type":"boolean"},"url":{"type":"string","format":"uri"},"byEvents":{"type":"boolean"},"base64":{"type":"boolean"},"events":{"type":"array","items":{"type":"string"}}}}}},"MessagesUpsertEvent":{"type":"object","required":["event","data"],"properties":{"event":{"type":"string","description":"`messages.upsert` (também aceita `MESSAGES_UPSERT`)."},"instance":{"type":"string"},"data":{"type":"object","required":["key"],"properties":{"key":{"type":"object","required":["remoteJid","fromMe"],"properties":{"remoteJid":{"type":"string","description":"`<número>@s.whatsapp.net`. Identificadores `@lid` (sem número) não são reconhecidos."},"fromMe":{"type":"boolean"},"id":{"type":"string"}}},"message":{"type":"object","properties":{"conversation":{"type":"string"},"extendedTextMessage":{"type":"object","properties":{"text":{"type":"string"}}}}}}}}},"MessagesUpdateEvent":{"type":"object","required":["event","data"],"properties":{"event":{"type":"string","description":"`messages.update` (também aceita `MESSAGES_UPDATE`)."},"instance":{"type":"string"},"data":{"type":"object","required":["keyId","status"],"properties":{"keyId":{"type":"string"},"fromMe":{"type":"boolean"},"status":{"type":"string","enum":["DELIVERY_ACK","DELIVERED","READ","PLAYED","SERVER_ACK","PENDING"]}}}}},"Error":{"type":"object","properties":{"status":{"type":"integer"},"error":{"type":"string"},"response":{"type":"object","properties":{"message":{"description":"Texto ou lista. O app procura palavras-chave (ver tabela de erros)."}}}}}}}}