Onboarding de parceiros (Embedded Signup)
POST /v1/onboarding/sessions e /v1/onboarding/domains — seu cliente conecta o WhatsApp Business de dentro do seu produto, sem passar pela dashboard.
Permite que o seu cliente final conecte o WhatsApp Business dele de dentro do seu produto, sem passar pela dashboard do WaBlast. Visão comercial do modelo de parceria: /parceiros.
Dois hosts diferentes. Os endpoints abaixo ficam em https://api.wablastmessage.com (autenticados pela sua chave wak_), mas a embed_url devolvida aponta para https://app.wablastmessage.com. Não troque um pelo outro.
POST /v1/onboarding/sessions
Cria a sessão de onboarding e devolve a URL que o cliente final deve abrir.
Chame sempre do seu servidor — nunca do navegador. A chave wak_ no front-end é chave vazada.
Body (todos opcionais):
| Campo | Tipo | Obrigatório | Regras |
|---|---|---|---|
external_ref | string | Não | 1–191 chars. O seu id do cliente final; volta no webhook e na consulta da sessão |
redirect_uri | string | Não | URL https, ≤ 2048 chars. O host precisa estar registrado como domínio REDIRECT ativo |
origin | string | Não | Só para popup no domínio do parceiro — hoje sempre falha (ver limitações abaixo) |
Pré-condição: ter ao menos um slot de conexão livre, senão 402 NO_FREE_SLOT.
Resposta 201:
Resposta 201
{
"id": "sess_abc",
"session_token": "wos_...",
"embed_url": "https://app.wablastmessage.com/embed/v1/connect?session=sess_abc&t=wos_...",
"expires_at": "2026-08-02T12:10:00.000Z",
"external_ref": "cliente-42"
} O session_token aparece uma única vez — no banco guardamos só o hash, não há como recuperá-lo. A sessão vale 10 minutos e é de uso único (consumo atômico: dois resgates simultâneos, só um passa). Expirou ou já foi usada? Crie outra sessão.
O dono da sessão é sempre a chave autenticada; campos de identidade no body são ignorados.
Erros na criação da sessão:
code | HTTP | O que significa |
|---|---|---|
NO_FREE_SLOT | 402 | Sem slot de conexão livre. Compre outra conexão ou libere uma |
REDIRECT_URI_NOT_ALLOWED | 400 | Host do redirect_uri não está registrado como domínio REDIRECT ativo |
ORIGIN_NOT_ALLOWED | 400 | origin não permitido |
DOMAIN_NOT_REGISTERED | 409 | Domínio de embed sem registro na Meta (ver limitações abaixo) |
INVALID_URL | 400 | URL malformada ou não-https |
INVALID_BODY | 400 | Falha de schema (ex.: external_ref acima de 191 caracteres) |
UNAUTHENTICATED | 401 | Chave ausente, inválida, revogada ou expirada |
RATE_LIMITED | 429 | Acima do limite de requisições |
Exemplo
curl -X POST https://api.wablastmessage.com/v1/onboarding/sessions \
-H "Authorization: Bearer wak_sua_chave" \
-H "Content-Type: application/json" \
-d '{"external_ref":"cliente-42","redirect_uri":"https://app.seusistema.com/wa/callback"}'const res = await fetch('https://api.wablastmessage.com/v1/onboarding/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.WABLAST_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
external_ref: 'cliente-42',
redirect_uri: 'https://app.seusistema.com/wa/callback',
}),
});
console.log(res.status, await res.json());import os, requests
res = requests.post(
'https://api.wablastmessage.com/v1/onboarding/sessions',
headers={'Authorization': 'Bearer ' + os.environ['WABLAST_API_KEY']},
json={
'external_ref': 'cliente-42',
'redirect_uri': 'https://app.seusistema.com/wa/callback',
},
)
print(res.status_code, res.json())<?php
$ch = curl_init('https://api.wablastmessage.com/v1/onboarding/sessions');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . getenv('WABLAST_API_KEY'),
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'external_ref' => 'cliente-42',
'redirect_uri' => 'https://app.seusistema.com/wa/callback',
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . " " . $res; GET /v1/onboarding/sessions/{id}
Fonte autoritativa do resultado do onboarding. Não trate o redirect do navegador como confirmação — o cliente pode fechar a aba. A confirmação é o webhook account.connected e, como fallback, o polling deste endpoint.
Resposta 200:
Resposta 200
{
"id": "sess_abc",
"status": "CONSUMED",
"external_ref": "cliente-42",
"account_id": "cms5cy8nx0010mvqwy9nbmzb8",
"failure_reason": null,
"expires_at": "2026-08-02T12:10:00.000Z",
"created_at": "2026-08-02T12:00:00.000Z"
} status | Significado |
|---|---|
PENDING | Criada, ainda não usada |
CONSUMING | Resgate em andamento (troca de token + conexão) |
CONSUMED | Sucesso — account_id preenchido |
FAILED | Falhou após o resgate — failure_reason preenchido |
EXPIRED | Passou dos 10 min sem uso (derivado na leitura, não é estado gravado) |
account_idé o id interno da conexão, o mesmo deGET /v1/accounts— não é o WABA ID. É ele que você passa comoaccount_idao enviar mensagens.failure_reasoné texto em pt-BR, sanitizado e truncado em 500 chars. Não é código estável — serve para o humano ler; ramifique porstatus.- Sessão de outro tenant devolve
404(não403): confirmar a existência já seria vazamento.
Exemplo
curl https://api.wablastmessage.com/v1/onboarding/sessions/sess_abc \
-H "Authorization: Bearer wak_sua_chave"const res = await fetch(
'https://api.wablastmessage.com/v1/onboarding/sessions/sess_abc',
{ headers: { 'Authorization': 'Bearer ' + process.env.WABLAST_API_KEY } },
);
console.log(res.status, await res.json());import os, requests
res = requests.get(
'https://api.wablastmessage.com/v1/onboarding/sessions/sess_abc',
headers={'Authorization': 'Bearer ' + os.environ['WABLAST_API_KEY']},
)
print(res.status_code, res.json())<?php
$ch = curl_init('https://api.wablastmessage.com/v1/onboarding/sessions/sess_abc');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . getenv('WABLAST_API_KEY'),
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . " " . $res; POST /v1/onboarding/domains
Registra domínio próprio para redirect_uri. Self-service, vale na hora.
Body (JSON):
{ "domain": "app.seusistema.com", "purpose": "REDIRECT" }
- Aceita host puro ou URL completa; guardamos só o host, em minúsculas.
- Recusa
localhost,.local,.internale IP literal (400 INVALID_DOMAIN). - Máximo de 20 domínios por conta →
409 DOMAIN_LIMIT_REACHEDse exceder. purpose: "REDIRECT"nasceACTIVE.purpose: "EMBED"nascePENDINGe depende de registro manual na Meta — hoje não há caminho self-service para ativá-lo.
Exemplo
curl -X POST https://api.wablastmessage.com/v1/onboarding/domains \
-H "Authorization: Bearer wak_sua_chave" \
-H "Content-Type: application/json" \
-d '{"domain":"app.seusistema.com","purpose":"REDIRECT"}'const res = await fetch('https://api.wablastmessage.com/v1/onboarding/domains', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.WABLAST_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain: 'app.seusistema.com', purpose: 'REDIRECT' }),
});
console.log(res.status, await res.json());import os, requests
res = requests.post(
'https://api.wablastmessage.com/v1/onboarding/domains',
headers={'Authorization': 'Bearer ' + os.environ['WABLAST_API_KEY']},
json={'domain': 'app.seusistema.com', 'purpose': 'REDIRECT'},
)
print(res.status_code, res.json())<?php
$ch = curl_init('https://api.wablastmessage.com/v1/onboarding/domains');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . getenv('WABLAST_API_KEY'),
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'domain' => 'app.seusistema.com',
'purpose' => 'REDIRECT',
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . " " . $res; GET /v1/onboarding/domains
Lista os domínios registrados da sua conta.
DELETE /v1/onboarding/domains/{id}
Remove o domínio. Domínio de outra conta → 404.
{ "deleted": true }
Eventos relacionados e limitações
Ao concluir o onboarding, você recebe o evento account.connected no seu endpoint de webhooks, com external_ref, waba_id, phone_number, display_name e onboarded_via no payload — case por data.external_ref e guarde o account_id do envelope. Nunca vai token nenhum nesse payload.
O evento account.connect_failed consta do catálogo mas não é emitido hoje — não construa tratamento de falha em cima dele. Detecte falha por GET /v1/onboarding/sessions/\{id\} (status: FAILED + failure_reason). Além disso, o account.connected pode não chegar quando um slot de conexão é reciclado (desconectar um cliente e onboardar outro no lugar). Por isso o polling da sessão é recomendado como fallback, não como opcional.
Popup no domínio do parceiro não é oferecido hoje. O campo origin existe no contrato, mas qualquer uso falha: rodar o Embedded Signup no domínio do parceiro depende de registro manual do domínio na Meta (purpose: "EMBED" nasce PENDING e não há caminho self-service para ativá-lo). O modo suportado é o popup a partir da nossa página (embed_url), em que o cliente final aceita os termos da Meta diretamente.
Dúvidas comerciais sobre o modelo de parceria (quem é dono de quê, pré-requisitos do cliente final, fricções da Meta): /parceiros.