Push pelo seu backend (webhook)
Use este modo quando o seu app já envia push por outra ferramenta (OneSignal, Braze, APNs próprio) ou quando você quer decidir o envio no seu backend. A Atalia continua decidindo quando avisar e monta a notificação; em vez de enviar pelo Firebase, ela manda um evento assinado para o seu servidor, que envia o push.
mensagem nova → a Atalia decide avisar → POST assinado no seu backend → o seu backend envia o push → o SDK abre a conversa no toque
1. Ligar no painel
No canal de webchat, em Editar canal › Instalação › App mobile e notificações push:
- Ligue Avisar por notificação quando chegar mensagem e Avisar o seu servidor (webhook).
- Informe a URL do webhook e clique em Salvar, no topo da página. Só
https, num endereço público. - Guarde o segredo do webhook: ele aparece uma vez só. Gerar novo segredo invalida o anterior na hora.
- Use Enviar evento de teste para conferir que o seu servidor recebe e valida a assinatura.
Os dois modos podem ficar ligados juntos: a Atalia envia pelo Firebase e também avisa o seu servidor.
2. Registrar o aparelho no app
O SDK registra na Atalia o token que o seu backend vai usar. O evento traz esses tokens em recipient.devices.
Atalia.setPushToken(token)
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Atalia.setAPNsToken(deviceToken, environment: .production) // .sandbox nos builds instalados pelo Xcode
}
Se o seu backend envia pelo Firebase também no iOS, registre o token do Firebase com Atalia.setPushToken(_:), como no modo Firebase. Se a sua ferramenta encontra o usuário pelo id dele, use recipient.external_id (o sub do JWT de identificação).
No app, o toque e a exibição com o app aberto são tratados como no modo Firebase: handleNotificationTap, handleNotification (Android) e presentationOptions(for:) (iOS).
3. Receber o evento
A cada notificação, a Atalia faz um POST com o corpo em JSON e estes cabeçalhos. O evento só sai quando o cliente tem um aparelho registrado ou está identificado.
| Cabeçalho | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Atalia-Webhooks/1.0 |
X-Atalia-Event | webchat.notification (ou webchat.test, no teste do painel) |
X-Atalia-Delivery | Id da entrega, wcp_<id da mensagem>. É o mesmo nas novas tentativas: use para não enviar o push duas vezes. |
X-Atalia-Signature | t=<unix>,v1=<assinatura>. Veja Conferir a assinatura. |
{
"id": "wcp_9001",
"type": "webchat.notification",
"created_at": "2026-10-26T12:00:00.000Z",
"channel": {
"id": 42,
"token": "wct_exemplo"
},
"recipient": {
"customer_id": 5531,
"external_id": "user-42",
"devices": [
{
"platform": "android",
"provider": "fcm",
"token": "fcm-token-do-aparelho",
"app_id": "br.com.loja.app",
"environment": "production",
"badge": true
},
{
"platform": "ios",
"provider": "apns",
"token": "apns-token-do-aparelho",
"app_id": "br.com.loja.app",
"environment": "production",
"badge": false
}
]
},
"conversation": {
"id": 812,
"unread_count": 1
},
"message": {
"id": 9001,
"sender_type": "agent",
"sender_kind": "agent"
},
"notification": {
"title": "Ana",
"body": "Seu pedido saiu para entrega",
"badge": 2,
"thread_id": "atalia-conv-812",
"data": {
"atalia": "1",
"v": "1",
"channel_token": "wct_exemplo",
"conversation_id": "812",
"message_id": "9001",
"sender_type": "agent"
}
}
}
| Campo | O que é |
|---|---|
id | O mesmo valor do X-Atalia-Delivery. |
channel | Canal de webchat que gerou a notificação. |
recipient.customer_id | Contato na Atalia. |
recipient.external_id | Usuário identificado no app (o sub do JWT). null para visitante anônimo. |
recipient.devices[] | Aparelhos registrados pelo SDK: platform (android, ios), provider (fcm, apns), token, app_id, environment (production, sandbox) e badge. |
recipient.devices[].badge | false quando o app cuida do número no ícone (appBadge: false no SDK iOS): não mande badge para esse aparelho. |
conversation.unread_count | Não lidas nesta conversa. |
message.sender_type | agent, bot ou system. |
message.sender_kind | Quem escreveu: agent (atendente), ai (agente de IA), bot (bot de fluxo) ou system. |
notification.title, body | Prontos para mostrar, já respeitando o texto na tela bloqueada escolhido no painel. |
notification.badge | Não lidas do cliente no canal, para o número no ícone. |
notification.thread_id | Agrupa por conversa: a notificação nova substitui a anterior. |
notification.data | Dados que o SDK lê no toque. Todos os valores são texto. |
O evento webchat.test, do botão Enviar evento de teste, traz só id, type, created_at e channel. Responda com sucesso a ele também.
4. Conferir a assinatura
v1 é o HMAC-SHA256, em hexadecimal, de "<t>.<corpo>" com o segredo do webhook, onde <corpo> são os bytes exatos que chegaram. Recuse a requisição quando a assinatura não bate ou quando t tem mais de 5 minutos: assim um evento capturado não pode ser reenviado depois.
import crypto from 'node:crypto';
import express from 'express';
export function ataliaSignature(secret, timestamp, rawBody) {
return crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
}
export function verifyAtaliaSignature(header, rawBody, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(String(header).split(',').map((part) => part.split('=')));
const timestamp = Number(parts.t);
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = Buffer.from(ataliaSignature(secret, timestamp, rawBody), 'hex');
const received = Buffer.from(parts.v1 || '', 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
// express.raw: a assinatura vale para os bytes que chegaram, não para um JSON remontado.
app.post('/webhooks/atalia', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifyAtaliaSignature(req.get('X-Atalia-Signature'), rawBody, process.env.ATALIA_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(rawBody);
// ...envie o push com event.notification para event.recipient.devices
res.sendStatus(200);
});
<?php
function ataliaSignature(string $secret, int $timestamp, string $rawBody): string {
return hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
}
function verifyAtaliaSignature(string $header, string $rawBody, string $secret, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $header) as $pair) {
[$key, $value] = array_pad(explode('=', $pair, 2), 2, '');
$parts[$key] = $value;
}
$timestamp = (int) ($parts['t'] ?? 0);
if ($timestamp === 0 || abs(time() - $timestamp) > $tolerance) {
return false;
}
return hash_equals(ataliaSignature($secret, $timestamp, $rawBody), $parts['v1'] ?? '');
}
$rawBody = file_get_contents('php://input'); // o corpo cru, antes de qualquer json_decode
$header = $_SERVER['HTTP_X_ATALIA_SIGNATURE'] ?? '';
if (!verifyAtaliaSignature($header, $rawBody, getenv('ATALIA_WEBHOOK_SECRET'))) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true);
// ...envie o push com $event['notification'] para $event['recipient']['devices']
http_response_code(200);
Vetor de teste
Para testar a sua função antes de ligar o webhook, calcule a assinatura destes valores. O resultado tem que ser exatamente o v1 abaixo (a checagem de tempo recusaria este t, que é fixo; teste só o cálculo).
segredo: segredo-de-exemplo
t: 1792915200
corpo: {"id":"wcp_test_1792915200000","type":"webchat.test","created_at":"2026-10-26T12:00:00.000Z","channel":{"id":42,"token":"wct_exemplo"}}
a091c4ffa57a3ea786f526fe1d50d9f8165f868a99fd7faf7e4e6c409bb5883c
const corpo = '{"id":"wcp_test_1792915200000","type":"webchat.test","created_at":"2026-10-26T12:00:00.000Z","channel":{"id":42,"token":"wct_exemplo"}}';
console.assert(
ataliaSignature('segredo-de-exemplo', 1792915200, corpo) === 'a091c4ffa57a3ea786f526fe1d50d9f8165f868a99fd7faf7e4e6c409bb5883c',
);
$corpo = '{"id":"wcp_test_1792915200000","type":"webchat.test","created_at":"2026-10-26T12:00:00.000Z","channel":{"id":42,"token":"wct_exemplo"}}';
assert(ataliaSignature('segredo-de-exemplo', 1792915200, $corpo) === 'a091c4ffa57a3ea786f526fe1d50d9f8165f868a99fd7faf7e4e6c409bb5883c');
5. Responder e enviar o push
Responda com um status 2xx em até 10 segundos. A Atalia não segue redirecionamento: um 3xx conta como falha.
Falha passageira (sem resposta em 10 segundos, erro de rede, 408, 429 ou 5xx) é tentada de novo, até 3 vezes no total, com 30 e 60 segundos de intervalo. Os outros erros não são repetidos. Como a mesma entrega pode chegar mais de uma vez, deduplique pelo X-Atalia-Delivery.
Ao montar o push, leve o notification do evento assim:
| Do evento | Para o push |
|---|---|
notification.title, notification.body | Título e texto. |
notification.data | Dados da notificação, sem mudar nada. No APNs, as chaves vão no primeiro nível do payload, ao lado de aps. É a chave atalia que faz o SDK reconhecer o toque. |
notification.thread_id | thread-id e apns-collapse-id no iOS; tag no Android. |
notification.badge | aps.badge no iOS, só para aparelhos com badge: true. |
{
"aps": {
"alert": { "title": "Ana", "body": "Seu pedido saiu para entrega" },
"sound": "default",
"badge": 2,
"thread-id": "atalia-conv-812"
},
"atalia": "1",
"v": "1",
"channel_token": "wct_exemplo",
"conversation_id": "812",
"message_id": "9001",
"sender_type": "agent"
}
Se o push sair sem as chaves de notification.data, a notificação aparece, mas o SDK não a reconhece: o toque só abre o app, e não o chat.