API Pública · v1

Integre tudo
programaticamente.

REST. JSON. Auth por chave. Paginação cursor. Rate limit por chave e por empresa. Sem SDK obrigatório, sem dance OAuth — copie o curl, cole no seu backend e funciona.

URL base
https://codewo.com.br/api/v1
Rate limit

120/min por chave

600/min por empresa

Visão geral

Toda request precisa de duas coisas: URL base + path e header Authorization: Bearer cwo_live_xxx. Respostas são sempre JSON. Códigos HTTP seguem REST padrão (200/201/4xx/5xx).

curl https://codewo.com.br/api/v1/me \
  -H "Authorization: Bearer cwo_live_xxx"
Resposta 200 OK
json
{
  "company": { "id": 1, "name": "Acme Inc.", "slug": "acme" },
  "api_key": {
    "id": 42, "name": "Backend produção",
    "prefix": "cwo_live_3tapst", "last_four": "zEuS",
    "scopes": ["conversations:read", "messages:write"],
    "last_used_at": "2026-04-23T18:32:01+00:00",
    "expires_at": null
  },
  "rate_limit_per_minute": 120,
  "api_version": "v1"
}
Criar minha primeira chave

Autenticação

Chaves cwo_live_* no padrão Stripe/Linear. SHA-256 em repouso, mostrado UMA vez na criação.

Hash em repouso

O segredo é exibido UMA vez na criação. O backend só guarda sha256(token).

Prefixo identificável

GitHub Secret Scanning detecta cwo_live_ em commits e revoga automaticamente.

Scopes

Cada chave carrega scopes. Endpoint sem o scope correto retorna 403 insufficient_scope.

ScopeOperaçãoPermite
conversations:read
read
Listar/ver conversas
conversations:write
write
Atualizar status, atribuição
messages:read
read
Histórico de mensagens
messages:write
write
Enviar mensagens
contacts:read
read
Listar/ver contatos
contacts:write
write
Criar/editar contatos
contacts:delete
delete
Remover contatos
channels:read
read
Listar canais conectados
users:read
read
Listar agentes (necessário pra obter sender_id)
templates:read
read
Templates de mensagem e HSM

Paginação

Todos os endpoints de listagem usam paginação cursor — mais previsível que offset durante updates frequentes.

?limitQuantidade por página. Default 25, máx 100.
?cursorID do último item retornado. Use o valor de next_cursor da resposta anterior.
Resposta com paginação
json
{
  "data": [ /* items... */ ],
  "next_cursor": "37",
  "has_more": true,
  "limit": 25
}

// Próxima página: GET ?cursor=37&limit=25
// Quando has_more=false, fim da lista.

Rate limit

Dois buckets independentes. Qualquer um excedido = 429.

Por chave

120/min

Bucket isolado por api_key_id. Chave comprometida não derruba as outras.

Por empresa

600/min

Cap agregado company_id. Impede multiplicar cota criando N chaves.

Headers

X-RateLimit-Limit

Limite total na janela atual.

X-RateLimit-Remaining

Quantas requests ainda cabem.

X-RateLimit-Reset

Timestamp Unix em que a janela reseta.

X-RateLimit-Scope

Qual bucket está mais apertado: key | company.

Retry-After

Em respostas 429: segundos pra esperar antes de tentar de novo.

X-Request-Id

ID único da request (ULID). Cite no suporte.

Erros

Trate pelo campo code, não pela mensagem. A mensagem pode mudar; o code é estável.

Formato padrão
json
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not have the required scope: contacts:write.",
    "required_scope": "contacts:write"
  }
}
Validation error (422)
json
{
  "error": {
    "code": "validation_failed",
    "message": "O campo nome é obrigatório.",
    "details": {
      "name": ["O campo nome é obrigatório."],
      "email": ["O e-mail informado é inválido."]
    }
  }
}
HTTPcodeSignificado
401
missing_api_keyHeader Authorization ausente.
401
invalid_api_keyToken não existe ou está mal formado.
401
revoked_api_keyA chave foi revogada.
401
expired_api_keyA chave passou da data de expiração.
403
ip_not_allowedO IP de origem não está na allowlist da chave.
403
insufficient_scopeA chave não tem o scope necessário.
404
not_foundRecurso não existe ou pertence a outra empresa.
405
method_not_allowedMétodo HTTP errado pra esse endpoint.
422
channel_inactiveCanal alvo está inativo.
422
channel_disconnectedCanal externo desconectado (precisa reautenticar).
422
validation_failedPayload inválido. details traz cada erro por campo.
429
rate_limitedExcedeu o limite. Header Retry-After traz o tempo de espera.
500
internal_errorErro inesperado do nosso lado. Já fomos notificados.

Identidade

GET
/v1/me
qualquer

Devolve a empresa dona da chave + metadado da chave + rate limit configurado. Use pra sanity-check do token.

Resposta 200 OK
json
{
  "company": { "id": 1, "name": "Acme Inc.", "slug": "acme" },
  "api_key": {
    "id": 42, "name": "Backend produção",
    "prefix": "cwo_live_3tapst", "last_four": "zEuS",
    "scopes": ["conversations:read", "messages:write"],
    "last_used_at": "2026-04-23T18:32:01+00:00",
    "expires_at": null
  },
  "rate_limit_per_minute": 120,
  "api_version": "v1"
}

Usuários (agentes)

Lista os agentes da empresa. Use isso pra obter o sender_id que é obrigatório em POST /v1/conversations/{id}/messages. Pra integrações automáticas, crie um user dedicado tipo "API Bot" no painel e use o id dele sempre.

GET
/v1/users
users:read

Lista agentes da empresa.

Query params

qBusca em first_name, last_name, email.
is_activetrue | false
GET
/v1/users/{id}
users:read

Agente individual.

Canais

Canais conectados (WhatsApp, Telegram, Email, WebChat...). Read-only — credentials NUNCA são expostas.

GET
/v1/channels
channels:read

Lista todos os canais da empresa, com status de sincronização.

Query params

typewhatsapp_web | whatsapp | telegram | email | webchat | facebook | instagram | sms
is_activetrue | false
limit, cursorpaginação
Resposta 200 OK
json
{
  "data": [
    {
      "id": 2,
      "name": "WhatsApp Pessoal",
      "type": "whatsapp_web",
      "provider": "wuzapi",
      "is_active": true,
      "sync_status": "connected",
      "last_sync_at": "2026-04-23T13:00:00+00:00",
      "error_message": null,
      "created_at": "2026-04-22T10:00:00+00:00"
    }
  ],
  "next_cursor": "2",
  "has_more": true,
  "limit": 25
}
GET
/v1/channels/{id}
channels:read

Detalhes de um canal específico.

Conversas

GET
/v1/conversations
conversations:read

Lista conversas (open, pending, closed, etc.) com contato eager-loaded.

Query params

statusopen | pending | in_progress | resolved | closed | spam
prioritylow | medium | high | urgent
assigned_to_idfiltrar por user_id atribuído
contact_idconversas de um contato específico
Resposta 200 OK
json
{
  "data": [
    {
      "id": 38,
      "contact": { "id": 17, "name": "Maria", "phone": "5511999999999", "email": null },
      "contact_id": 17,
      "status": "open",
      "priority": "medium",
      "assigned_to": { "type": "user", "id": 1 },
      "message_count": 12,
      "unread_count": 3,
      "first_message_at": "2026-04-22T09:00:00+00:00",
      "last_message_at": "2026-04-23T13:30:00+00:00",
      "ai": { "summary": null, "sentiment": "neutral", "category": "vendas",
              "intent": "comprar", "urgency_score": 4 },
      "created_at": "2026-04-22T09:00:00+00:00",
      "updated_at": "2026-04-23T13:30:00+00:00"
    }
  ],
  "next_cursor": "37",
  "has_more": true,
  "limit": 25
}
GET
/v1/conversations/{id}
conversations:read

Conversa individual com contato carregado.

Mensagens

GET
/v1/conversations/{id}/messages
messages:read

Histórico de mensagens da conversa. Notas internas são excluídas por padrão.

Query params

include_internal_notespassar pra incluir notas internas
POST
/v1/conversations/{id}/messages
messages:write

Envia mensagem na conversa. Reusa o pipeline interno — dispara via canal externo (WhatsApp, Telegram, etc.).

Body

{
  "content": "Olá! Recebemos sua solicitação.",
  "sender_id": 5,
  "content_type": "text",
  "channel_id": 2,
  "is_internal_note": false
}
contentObrigatório. Texto da mensagem (max 10000).
sender_idObrigatório. ID do agente que envia. Use GET /v1/users pra listar IDs válidos. Não tem fallback — atribuição é responsabilidade do cliente.
content_typetext (default) | html
channel_idOpcional — se omitido, usa o último canal da conversa.
is_internal_noteDefault false. Se true, persiste mas não envia ao canal.
Resposta 201 Created
json
{
  "data": {
    "id": 1024,
    "conversation_id": 38,
    "channel_id": 2,
    "channel_type": "whatsapp_web",
    "sender_type": "user",
    "sender_id": 1,
    "content": "Olá! Recebemos sua solicitação.",
    "content_type": "text",
    "status": "pending",
    "is_internal_note": false,
    "sent_at": "2026-04-23T13:45:12+00:00",
    "created_at": "2026-04-23T13:45:12+00:00"
  }
}

Erros de canal

  • channel_inactive (422) — canal está desabilitado
  • channel_disconnected (422) — WhatsApp/Telegram desconectado, precisa reautenticar

Contatos

CRUD completo. Soft delete (DELETE faz soft delete; restore não está exposto na v1).

GET
/v1/contacts
contacts:read

Lista contatos com filtros.

Query params

qBusca em name, email, phone, document.
statusactive | inactive | blocked
sourcegoogle_ads, meta_ads, direct, manual, api...
typepf | pj
GET
/v1/contacts/{id}
contacts:read

Contato individual com endereço e attribution.

POST
/v1/contacts
contacts:write

Cria contato. Source default = 'api' se não informado.

Body
json
{
  "name": "João Silva",
  "email": "joao@empresa.com.br",
  "phone": "+5511999998888",
  "type": "pf",
  "document": "123.456.789-00",
  "company_name": "Empresa SA",
  "city": "São Paulo",
  "state": "SP",
  "source": "site-form",
  "metadata": { "lead_score": 85 }
}
Resposta 201 Created
json
{
  "data": {
    "id": 23,
    "name": "João Silva",
    "email": "joao@empresa.com.br",
    "phone": "+5511999998888",
    "type": "pf",
    "company_name": "Empresa SA",
    "status": "active",
    "source": "site-form",
    "attribution": { "source": null, "campaign": null, "touchpoints_count": 0 },
    "address": { "line": null, "city": "São Paulo", "state": "SP",
                 "country": "Brasil", "postal_code": null },
    "first_contact_at": null,
    "created_at": "2026-04-23T13:50:00+00:00"
  }
}
PATCH
/v1/contacts/{id}
contacts:write

Atualiza parcialmente. Só os campos enviados são alterados.

Body (qualquer campo do schema)
json
{
  "notes": "Cliente VIP — atender com prioridade",
  "metadata": { "lead_score": 95 }
}
DELETE
/v1/contacts/{id}
contacts:delete

Soft delete. Histórico de conversas e logs persistem.

Resposta 200 OK
json
{ "data": null, "deleted": true }

Templates

Dois tipos: messages (atalhos internos do agente, ex: /oi) e HSM (templates Meta-aprovados pra WhatsApp Cloud).

GET
/v1/templates/messages
templates:read

Templates internos de mensagem usados pelos agentes (atalhos).

Query params

qBusca em name, shortcut, content.
categoryCategoria do template.
is_activetrue | false
GET
/v1/templates/messages/{id}
templates:read

Template de mensagem individual.

GET
/v1/templates/hsm
templates:read

Templates HSM Meta-approved (WhatsApp Cloud), com componentes (header/body/footer/buttons).

Query params

statusapproved | pending | rejected | disabled
languagept_BR, en_US, etc.
channel_idFiltrar por canal específico
GET
/v1/templates/hsm/{id}
templates:read

Template HSM individual com todos os componentes.

Pronto pra primeira chamada?

Crie sua chave em segundos. Sem cartão, sem aprovação manual.