# 4send — Documentação completa da API Base URL: https://api.4send.me/v1 · Auth: Authorization: Bearer --- name: 4send-api description: >- Contexto da API do 4send (WhatsApp + e-mail): base URL, autenticação por API key, envio de mensagens, contatos, tags, campos personalizados, fluxos, idempotência e webhooks assinados. Use ao construir, integrar ou depurar qualquer coisa que chame a API do 4send (arquivos com "4send", "integração" ou "webhook"). --- # API do 4send Você está ajudando a integrar a **API do 4send** — WhatsApp + e-mail transacional, contatos e fluxos (jornadas automáticas). ## Essencial - **Base URL:** `https://api.4send.me/v1` - **Auth:** cabeçalho `Authorization: Bearer ` - **Chaves:** `4s_test_...` = sandbox (NÃO envia de verdade, ideal pra desenvolver); `4s_live_...` = produção (envia pra valer). Crie em Integrações → Tokens de API. - **⚠️ CHAME SEMPRE DO SERVIDOR (backend). NUNCA do navegador.** A API key é um segredo. Em código que roda no navegador (React, Vue, Next client component, HTML, extensão, app web) a key fica **visível para qualquer visitante** da página, que passaria a enviar mensagens e ler os contatos da conta. - Chamada com `4s_live_` **vinda de um navegador é recusada com `403`** — a API detecta pelo cabeçalho `Origin`. Não tente contornar: o bloqueio existe para proteger a key de quem te contratou. - `4s_test_` **funciona no navegador** (é sandbox, não dispara nada). Use pra prototipar a tela; mova a chamada pro backend antes de usar `4s_live_`. - **Está montando um conector/integração dentro de um produto?** Guarde a key no seu backend e faça a chamada de lá. Nunca entregue a key ao frontend, nem a repasse para o navegador do seu cliente. - Sem backend? Use uma camada de servidor (rota de API do seu framework, função serverless, n8n/Make) como intermediária. - **Formato:** JSON. Datas em ISO 8601. Ids têm prefixo por recurso (`msg_`, `ct_`, `flow_`, `run_`, `tag_`, `field_`). - **Envio é assíncrono:** o POST responde `202` com `status: "queued"`; o resultado final chega por **webhook** ou consultando `GET /messages/{id}`. - **Acesso depende do plano** da conta (a key só vale se o plano libera a API). ## Endpoints ### Enviar mensagem — `POST /messages` (escopo `messages:send`) ```json { "channel": "whatsapp", "to": { "phone": "+5551999999999" }, "whatsapp": { "body": "Olá {{nome}}! Seu pedido saiu 🚚" }, "variables": { "nome": "João" } } ``` E-mail: `"channel": "email"`, `to.email`, e `email: { subject, html, reply_to?, preview_text? }`. Destinatário: `to.phone`, `to.email` **ou** `to.contact_id`. Mídia no WhatsApp: `whatsapp: { media_url, media_type: image|video|audio|document }`. Resposta `202`: ```json { "id": "msg_663f...", "status": "queued", "channel": "whatsapp", "to": "+5551999999999", "provider_message_id": null, "created_at": "..." } ``` ### Status da mensagem — `GET /messages/{id}` (escopo `messages:read`) Status: `queued → sent → delivered → read` (ou `failed`). ### Criar/atualizar contato — `POST /contacts` (escopo `contacts:write`) ```json { "phone": "+5551999999999", "name": "João", "tags": ["tag_663f..."], "fields": { "plano": "gold" } } ``` Deduplica por telefone; na falta, por e-mail. Campos omitidos NÃO são apagados. Também: `GET /contacts/{id}` e `GET /contacts?phone=&email=` (escopo `contacts:read`). ### Colocar contato num fluxo — `POST /flows/{flow_id}/enroll` (escopo `flows:enroll`) ```json { "to": { "phone": "+5551999999999" }, "variables": { "nome": "João" } } ``` Use `GET /flows` (escopo `flows:read`) pra descobrir o `flow_id`. O fluxo precisa estar `active` (senão `409`). ### Tirar contato do fluxo — `POST /flows/{flow_id}/unenroll` (escopo `flows:enroll`) ```json { "to": { "phone": "+5551999999999" }, "reason": "comprou" } ``` Interrompe a jornada: o contato para de receber o que faltava. **`flow_id` aceita `all`** pra tirar de TODAS as jornadas de uma vez — é o que você quer no caso clássico ("comprou → para tudo", "cancelou", "pediu pra parar"), sem listar fluxo por fluxo. Resposta: `{ "unenrolled": 1, "runs": [...] }`. Idempotente: quem não está em jornada nenhuma devolve `unenrolled: 0` e `200`, não erro — então dá pra chamar sempre, sem checar antes. O `reason` vai no relatório e no webhook `fluxo.parado`, e é o que separa "comprou" de "desistiu". ### Tags — `POST /tags` `{ "name": "cliente-vip", "color": "#FF4E33" }` · `GET /tags` Escopos `tags:write` / `tags:read`. Use o `id` retornado no campo `tags` de um contato. ### Campos personalizados — `POST /custom-fields` `{ "name": "Plano", "key": "plano", "type": "text" }` · `GET /custom-fields` Escopos `fields:write` / `fields:read`. A `key` vira a variável `{{key}}` nas mensagens. ### Consumo — `GET /usage` `{ "period": "2026-08", "whatsapp": 1234, "email": 567, "total": 1801, "limit": null }` ## Idempotência (não duplicar envio) Em POSTs de envio (`/messages`) e matrícula (`/flows/{id}/enroll`), mande o cabeçalho `Idempotency-Key: `. Convenção recomendada: `/` (ex.: `pedido-enviado/12345`). Retenção 24h. - mesma key + mesmo corpo → devolve a resposta guardada (não reenvia). - mesma key + corpo diferente → `422`. ## Erros Sempre no formato: ```json { "error": { "type": "invalid_request", "message": "...", "param": "to.phone", "doc_url": "https://docs.4send.me" } } ``` `type`: `authentication` (401) · `forbidden` (403) · `invalid_request` (400/422) · `not_found` (404) · `conflict` (409) · `rate_limit` (429) · `quota_exceeded` (403) · `api_error` (500). ## Limite de requisições ~120 req/min por chave. Ao exceder → `429` com cabeçalhos `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` e `Retry-After`. ## Webhooks (o aviso de volta) O 4send faz POST no endpoint que você cadastrar (Integrações → Webhooks) quando algo acontece. Eventos: `mensagem.enviada`, `mensagem.entregue`, `mensagem.lida`, `mensagem.falhou`, `fluxo.concluido`, `fluxo.parado`, `contato.criado`, `contato.atualizado`. Cada entrega vai assinada em `X-4send-Signature: t=,v1=`. Verifique com o secret do webhook — detalhes e código em **webhooks.md**. ## Regras de ouro - **A chamada sai do SERVIDOR, nunca do navegador.** Se o código que você está escrevendo roda no navegador do usuário final, a integração está no lugar errado — mova pro backend. `4s_live_` no navegador é recusado com `403`. - Em produção, comece testando com uma chave `4s_test_` (não gasta cota, não dispara). - Sempre mande `Idempotency-Key` nos envios. - Sempre verifique a assinatura do webhook contra o **corpo cru** (raw body). - Consulte **openapi.yaml** (spec completa) e **examples.md** (curl/Node/Python). --- # 4send — Exemplos de código Base URL: `https://api.4send.me/v1` · Auth: `Authorization: Bearer ` ## Enviar um WhatsApp ### cURL ```bash curl -X POST https://api.4send.me/v1/messages \ -H "Authorization: Bearer $FOURSEND_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "channel": "whatsapp", "to": { "phone": "+5551999999999" }, "whatsapp": { "body": "Olá {{nome}}! Seu pedido saiu 🚚" }, "variables": { "nome": "João" } }' ``` ### Node.js ```javascript import { randomUUID } from 'node:crypto'; const API = 'https://api.4send.me/v1'; const KEY = process.env.FOURSEND_API_KEY; async function sendWhatsapp(phone, body, variables = {}) { const res = await fetch(`${API}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID(), }, body: JSON.stringify({ channel: 'whatsapp', to: { phone }, whatsapp: { body }, variables }), }); if (!res.ok) throw new Error((await res.json()).error?.message || res.statusText); return res.json(); // { id: "msg_...", status: "queued", ... } } // status depois: async function getMessage(id) { const res = await fetch(`${API}/messages/${id}`, { headers: { Authorization: `Bearer ${KEY}` } }); return res.json(); } ``` ### Python ```python import os, uuid, requests API = "https://api.4send.me/v1" KEY = os.environ["FOURSEND_API_KEY"] def send_whatsapp(phone, body, variables=None): r = requests.post( f"{API}/messages", headers={ "Authorization": f"Bearer {KEY}", "Idempotency-Key": str(uuid.uuid4()), }, json={"channel": "whatsapp", "to": {"phone": phone}, "whatsapp": {"body": body}, "variables": variables or {}}, ) r.raise_for_status() return r.json() ``` ## Enviar um e-mail ```bash curl -X POST https://api.4send.me/v1/messages \ -H "Authorization: Bearer $FOURSEND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel": "email", "to": { "email": "joao@exemplo.com" }, "email": { "subject": "Seu pedido saiu", "html": "

Olá, {{nome}}!

" }, "variables": { "nome": "João" } }' ``` ## Criar contato e colocar num fluxo ```bash # 1) cria/atualiza o contato curl -X POST https://api.4send.me/v1/contacts \ -H "Authorization: Bearer $FOURSEND_API_KEY" -H "Content-Type: application/json" \ -d '{ "phone": "+5551999999999", "name": "João", "fields": { "plano": "gold" } }' # 2) descobre os fluxos ativos curl https://api.4send.me/v1/flows -H "Authorization: Bearer $FOURSEND_API_KEY" # 3) matricula o contato no fluxo curl -X POST https://api.4send.me/v1/flows/flow_663f.../enroll \ -H "Authorization: Bearer $FOURSEND_API_KEY" -H "Content-Type: application/json" \ -d '{ "to": { "phone": "+5551999999999" }, "variables": { "nome": "João" } }' ``` ## Dica de fluxo típico (CRM → 4send) Evento no seu sistema (ex.: pedido criado) → chame `POST /messages` (aviso na hora) ou `POST /flows/{id}/enroll` (jornada). Assine os webhooks pra saber quando foi entregue/lido. Use `4s_test_` enquanto desenvolve. --- # 4send — Webhooks (o aviso de volta) Cadastre um endpoint HTTP em **Integrações → Webhooks**. O 4send faz `POST` nele quando eventos acontecem. Ao criar o webhook, você recebe um **secret** — guarde-o pra verificar a assinatura. ## Formato do corpo ```json { "event": "mensagem.entregue", "timestamp": "2026-08-04T12:00:00.000Z", "namespace": "default", "data": { "id": "msg_663f...", "channelType": "whatsapp", "status": "delivered", "contact": "ct_663f..." } } ``` ## Eventos | Evento | Quando | |---|---| | `mensagem.enviada` | saiu do 4send | | `mensagem.entregue` | entregue no aparelho | | `mensagem.lida` | lida pelo destinatário | | `mensagem.falhou` | falha no envio | | `fluxo.concluido` | o contato percorreu a jornada até o fim | | `fluxo.parado` | o contato SAIU antes do fim (comprou, respondeu, foi removido). Traz `reason` quando informado — é o que separa "converteu" de "desistiu" | | `contato.criado` / `contato.atualizado` | mudança no contato | ## Assinatura (HMAC) Toda entrega leva o cabeçalho: ``` X-4send-Signature: t=,v1= ``` O `v1` é `HMAC_SHA256(secret, ".")` em hex. **Verifique sempre contra o CORPO CRU** (raw body) — se você re-serializar o JSON, a assinatura quebra. ### Verificar em Node.js (Express) ```javascript import crypto from 'node:crypto'; import express from 'express'; const app = express(); const SECRET = process.env.FOURSEND_WEBHOOK_SECRET; // IMPORTANTE: capture o corpo CRU (não use express.json() antes de verificar) app.post('/webhooks/4send', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('X-4send-Signature') || ''; const m = /t=([^,]+),v1=(.+)/.exec(header); if (!m) return res.sendStatus(400); const [, t, sig] = m; const raw = req.body.toString('utf8'); const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${raw}`).digest('hex'); const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); if (!ok) return res.sendStatus(401); const evt = JSON.parse(raw); // ... trate evt.event / evt.data res.sendStatus(200); }); ``` ### Verificar em Python ```python import hmac, hashlib, re def verify(raw_body: bytes, header: str, secret: str) -> bool: m = re.match(r"t=([^,]+),v1=(.+)", header or "") if not m: return False t, sig = m.group(1), m.group(2) expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(sig, expected) ``` ## Boas práticas - Responda `2xx` rápido; processe o evento em background. - O 4send re-tenta em caso de falha (backoff) — trate entregas repetidas de forma idempotente (ex.: dedupe por `data.id` + `event`). --- ## Especificação OpenAPI 3.1 ```yaml openapi: 3.1.0 info: title: 4send API version: "1.0.0" description: | API pública do 4send — envie WhatsApp e e-mail, gerencie contatos, tags e campos personalizados, e coloque contatos em fluxos (jornadas automáticas). Autenticação por API key (cabeçalho `Authorization: Bearer `). Crie chaves em Integrações → Tokens de API. Chaves `4s_test_...` simulam o envio (não disparam de verdade); `4s_live_...` enviam pra valer. contact: name: Suporte 4send url: https://app.4send.me servers: - url: https://api.4send.me/v1 description: Produção security: - ApiKey: [] tags: - name: Mensagens - name: Contatos - name: Fluxos - name: Tags - name: Campos personalizados - name: Conta paths: /: get: tags: [Conta] summary: Verificar a chave description: Confirma que a chave é válida e diz o modo (test/live). responses: "200": description: Chave válida content: application/json: example: { ok: true, version: "v1", workspace: "6a60...", mode: "live" } /messages: post: tags: [Mensagens] summary: Enviar mensagem (WhatsApp ou e-mail) description: | Envio transacional. Responde na hora com `202` e o status `queued`; o resultado final chega por webhook ou consultando `GET /messages/{id}`. Suporta o cabeçalho `Idempotency-Key` para evitar envio duplicado. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MessageCreate" } examples: whatsapp: summary: WhatsApp com variável value: channel: whatsapp to: { phone: "+5551999999999" } whatsapp: { body: "Olá {{nome}}! Seu pedido saiu 🚚" } variables: { nome: "João" } email: summary: E-mail value: channel: email to: { email: "joao@exemplo.com" } email: subject: "Seu pedido saiu" html: "

Olá, {{nome}}!

Seu pedido está a caminho.

" variables: { nome: "João" } responses: "202": description: Aceito (na fila) content: application/json: schema: { $ref: "#/components/schemas/Message" } "400": { $ref: "#/components/responses/Error" } "401": { $ref: "#/components/responses/Error" } "403": { $ref: "#/components/responses/Error" } "404": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } "429": { $ref: "#/components/responses/Error" } /messages/{id}: get: tags: [Mensagens] summary: Ver status da mensagem parameters: - name: id in: path required: true schema: { type: string, example: "msg_663f0a..." } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Message" } "404": { $ref: "#/components/responses/Error" } /contacts: post: tags: [Contatos] summary: Criar / atualizar contato description: Deduplica por telefone; na falta, por e-mail. Campos omitidos não são apagados. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ContactCreate" } example: phone: "+5551999999999" name: "João" tags: ["tag_663f..."] fields: { plano: "gold" } responses: "200": description: Contato criado ou atualizado content: application/json: schema: { $ref: "#/components/schemas/Contact" } "400": { $ref: "#/components/responses/Error" } get: tags: [Contatos] summary: Buscar contato por telefone ou e-mail parameters: - { name: phone, in: query, schema: { type: string } } - { name: email, in: query, schema: { type: string } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Contact" } "404": { $ref: "#/components/responses/Error" } /contacts/{id}: get: tags: [Contatos] summary: Buscar contato por id parameters: - { name: id, in: path, required: true, schema: { type: string, example: "ct_663f..." } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Contact" } "404": { $ref: "#/components/responses/Error" } /flows: get: tags: [Fluxos] summary: Listar fluxos ativos responses: "200": description: OK content: application/json: schema: type: array items: { $ref: "#/components/schemas/Flow" } /flows/{flow_id}/enroll: post: tags: [Fluxos] summary: Colocar um contato num fluxo parameters: - { name: flow_id, in: path, required: true, schema: { type: string, example: "flow_663f..." } } - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: type: object properties: to: { $ref: "#/components/schemas/Recipient" } variables: { type: object, additionalProperties: true } example: to: { phone: "+5551999999999" } variables: { nome: "João", codigo: "A1B2" } responses: "201": description: Contato matriculado content: application/json: schema: { $ref: "#/components/schemas/FlowRun" } "404": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } /tags: get: tags: [Tags] summary: Listar tags responses: "200": description: OK content: application/json: schema: { type: array, items: { $ref: "#/components/schemas/Tag" } } post: tags: [Tags] summary: Criar tag requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string, example: "cliente-vip" } color: { type: string, example: "#FF4E33" } responses: "201": description: Tag criada content: application/json: schema: { $ref: "#/components/schemas/Tag" } "409": { $ref: "#/components/responses/Error" } /custom-fields: get: tags: [Campos personalizados] summary: Listar campos personalizados responses: "200": description: OK content: application/json: schema: { type: array, items: { $ref: "#/components/schemas/CustomField" } } post: tags: [Campos personalizados] summary: Criar campo personalizado description: A `key` vira a variável `{{key}}` nas mensagens. requestBody: required: true content: application/json: schema: type: object required: [name, key] properties: name: { type: string, example: "Plano" } key: { type: string, example: "plano" } type: { type: string, enum: [text, number, date, boolean], default: text } responses: "201": description: Campo criado content: application/json: schema: { $ref: "#/components/schemas/CustomField" } "409": { $ref: "#/components/responses/Error" } /usage: get: tags: [Conta] summary: Consumo do mês responses: "200": description: OK content: application/json: example: period: "2026-08" whatsapp: 1234 email: 567 total: 1801 limit: null daily_limit_per_channel: 1000 components: securitySchemes: ApiKey: type: http scheme: bearer description: "Sua API key (4s_live_... ou 4s_test_...) no cabeçalho Authorization." parameters: IdempotencyKey: name: Idempotency-Key in: header required: false description: UUID gerado pelo cliente. Repetir a mesma chamada com a mesma key não duplica o envio. schema: { type: string, example: "550e8400-e29b-41d4-a716-446655440000" } responses: Error: description: Erro content: application/json: schema: { $ref: "#/components/schemas/Error" } example: error: type: invalid_request message: "O contato não tem telefone p/ WhatsApp." param: "to.phone" doc_url: "https://docs.4send.me" schemas: Error: type: object properties: error: type: object properties: type: type: string description: "authentication | forbidden | invalid_request | not_found | conflict | rate_limit | quota_exceeded | api_error" message: { type: string } param: { type: string } doc_url: { type: string } Recipient: type: object description: Destinatário — informe phone, email OU contact_id. properties: phone: { type: string, example: "+5551999999999" } email: { type: string, example: "joao@exemplo.com" } contact_id: { type: string, example: "ct_663f..." } name: { type: string } MessageCreate: type: object required: [channel, to] properties: channel: { type: string, enum: [whatsapp, email] } to: { $ref: "#/components/schemas/Recipient" } whatsapp: type: object properties: body: { type: string, description: "Texto (aceita {{variaveis}})" } media_url: { type: string } media_type: { type: string, enum: [image, video, audio, document] } email: type: object properties: subject: { type: string } html: { type: string } reply_to: { type: string } preview_text: { type: string } variables: type: object additionalProperties: true description: Valores das {{variaveis}} do texto. channel_id: type: string description: Canal específico (senão usa o conectado do tipo). Message: type: object properties: id: { type: string, example: "msg_663f0a..." } status: { type: string, enum: [queued, sent, delivered, read, failed] } channel: { type: string, enum: [whatsapp, email] } to: { type: string, nullable: true } provider_message_id: { type: string, nullable: true } error: { type: string } sent_at: { type: string, nullable: true } delivered_at: { type: string, nullable: true } read_at: { type: string, nullable: true } created_at: { type: string } ContactCreate: type: object properties: phone: { type: string } email: { type: string } name: { type: string } tags: { type: array, items: { type: string }, description: "ids de tag (tag_...)" } fields: { type: object, additionalProperties: true } opt_out_whatsapp: { type: boolean } opt_out_email: { type: boolean } Contact: type: object properties: id: { type: string, example: "ct_663f..." } phone: { type: string, nullable: true } email: { type: string, nullable: true } name: { type: string, nullable: true } tags: { type: array, items: { type: string } } fields: { type: object, additionalProperties: true } opt_out_whatsapp: { type: boolean } opt_out_email: { type: boolean } created_at: { type: string } updated_at: { type: string } Flow: type: object properties: id: { type: string, example: "flow_663f..." } name: { type: string } status: { type: string, example: "active" } FlowRun: type: object properties: id: { type: string, example: "run_663f..." } flow_id: { type: string, example: "flow_663f..." } contact_id: { type: string, example: "ct_663f..." } status: { type: string, example: "active" } created_at: { type: string } Tag: type: object properties: id: { type: string, example: "tag_663f..." } name: { type: string } color: { type: string, nullable: true } created_at: { type: string } CustomField: type: object properties: id: { type: string, example: "field_663f..." } name: { type: string } key: { type: string } type: { type: string, enum: [text, number, date, boolean] } created_at: { type: string } ```