> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ararahq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Atendimento com vários atendentes

> Monte o suporte no WhatsApp com time: Brain responde primeiro, passa para humano, e o atendente responde dentro da janela de 24h.

Ao final, o Brain responde as perguntas repetidas, passa a conversa para o time quando precisa, seu backend é avisado por webhook e o atendente responde pela API ou pelo painel.

## Antes de começar

* Chave de API com permissão `ADMIN` na variável `ARARA_API_KEY`. Ver [Autenticação](/api-reference/authentication).
* Número dedicado conectado na organização. Ver [Números](/numbers).
* Conversas disponível no seu plano. Sem acesso, `GET /v1/conversations` responde `403 PLAN_FEATURE_LOCKED`.
* Template aprovado para retomar conversa com a janela fechada (aqui, `retomada_atendimento`). Ver [Templates](/concepts/templates).
* Webhook configurado, com o secret (`whsec_...`) em `ARARA_WEBHOOK_SECRET`. Ver [Webhooks](/webhooks).

## Passo a passo

<Steps>
  <Step title="Convide os atendentes">
    Pelo [painel](https://app.ararahq.com) ou pela API:

    ```bash theme={null}
    curl -X POST https://api.ararahq.com/v1/team/invitations \
      -H "Authorization: Bearer $ARARA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "emails": ["ana@empresa.com", "carlos@empresa.com"], "role": "AGENT" }'
    ```

    Papéis: `OWNER`, `ADMIN`, `DEVELOPER`, `AGENT`, `BILLING`, `VIEWER`. Só `OWNER` e `ADMIN` convidam. `OWNER` não é atribuído por convite, só por transferência. O número de membros é limitado pelo plano.
  </Step>

  <Step title="Configure o Brain para responder primeiro">
    ```bash theme={null}
    curl -X PATCH https://api.ararahq.com/v1/brain/config \
      -H "Authorization: Bearer $ARARA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "active": true,
        "assistMode": "AUTO",
        "exitTriggers": {
          "customerAsks": ["atendente", "falar com humano", "reclamação"],
          "maxMessages": 12,
          "confidenceThreshold": 0.5
        }
      }'
    ```

    | `assistMode` | Comportamento |
    | - | - |
    | `AUTO` | Responde sozinho e passa para humano quando um gatilho de saída dispara |
    | `COPILOT` | Não responde. O atendente pede uma sugestão com `POST /v1/brain/suggest-reply` (`conversationId` e `customerMessage`) |
    | `OFF` | Desligado |

    | Gatilho de saída | `reason` do handoff |
    | - | - |
    | Mensagem contém uma frase de `customerAsks` | `CUSTOMER_REQUEST` |
    | Histórico com o contato chega a `maxMessages` | `MAX_MESSAGES` |
    | Resposta com confiança abaixo de `confidenceThreshold` (0 a 1) | `LOW_CONFIDENCE` |
    | Limite mensal de conversas do Brain atingido | `QUOTA_EXCEEDED` |

    O Brain depende do plano e de número conectado. `active: true` é recusado enquanto houver pré-requisito pendente. Acompanhe o consumo com `GET /v1/brain/usage`. Detalhes em [Brain](/brain).
  </Step>

  <Step title="Receba os eventos">
    Três eventos importam: `message.received` (cliente mandou mensagem), `conversation.needs_human` (Brain passou para humano) e `message.status_updated` (status da resposta enviada). Valide a assinatura antes de processar. Exemplo em Next.js (App Router, runtime Node):

    ```typescript theme={null}
    // app/api/webhooks/arara/route.ts
    import { createHmac, timingSafeEqual } from 'node:crypto';

    const TOLERANCE_SECONDS = 300;

    function isValidSignature(rawBody: string, timestamp: string | null, signature: string | null): boolean {
      if (!timestamp || !signature) return false;
      const ts = Number(timestamp);
      if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > TOLERANCE_SECONDS) return false;
      const expected = 'sha256=' + createHmac('sha256', process.env.ARARA_WEBHOOK_SECRET!)
        .update(`${timestamp}.${rawBody}`)
        .digest('hex');
      const a = Buffer.from(expected);
      const b = Buffer.from(signature);
      return a.length === b.length && timingSafeEqual(a, b);
    }

    export async function POST(request: Request) {
      const rawBody = await request.text();
      const valid = isValidSignature(
        rawBody,
        request.headers.get('x-arara-timestamp'),
        request.headers.get('x-arara-signature'),
      );
      if (!valid) return new Response('invalid signature', { status: 401 });

      const deliveryId = request.headers.get('x-arara-delivery-id');
      // Deduplique por deliveryId antes de processar: a Arara retenta entregas que falham.

      const event = JSON.parse(rawBody) as { event: string; data: Record<string, unknown> };

      switch (event.event) {
        case 'message.received':
          // data.from, data.body, data.type, data.media_url
          break;
        case 'conversation.needs_human':
          // data.handoffId, data.conversationId, data.reason, data.summary
          break;
      }

      return new Response(null, { status: 200 });
    }
    ```
  </Step>

  <Step title="Atenda os handoffs">
    Quando passa para humano, o Brain avisa o cliente e cria um handoff:

    ```bash theme={null}
    # Pendentes
    curl "https://api.ararahq.com/v1/brain/handoffs?resolved=false" \
      -H "Authorization: Bearer $ARARA_API_KEY"

    # Marcar como resolvido
    curl -X POST https://api.ararahq.com/v1/brain/handoffs/{id}/resolve \
      -H "Authorization: Bearer $ARARA_API_KEY"

    # Devolver a conversa para o Brain
    curl -X POST https://api.ararahq.com/v1/brain/handoffs/{id}/return-to-brain \
      -H "Authorization: Bearer $ARARA_API_KEY"
    ```

    Cada handoff traz `id`, `conversationId`, `reason`, `summary`, `briefing` (resumo para o atendente, quando gerado), `createdAt` e `resolvedAt`.
  </Step>

  <Step title="Responda dentro da janela, ou reabra com template">
    Com a janela de 24h aberta, o atendente responde pelo painel, em **Conversas**, ou por `POST /v1/conversations/reply`. Com a janela fechada, a API responde `400` e você precisa enviar um template. A função abaixo decide sozinha:

    ```typescript theme={null}
    // reply-or-reopen.ts
    const API = 'https://api.ararahq.com';
    const TIMEOUT_MS = 10_000;
    const headers = {
      Authorization: `Bearer ${process.env.ARARA_API_KEY}`,
      'Content-Type': 'application/json',
    };

    interface WindowStatus {
      phone: string;
      isWindowOpen: boolean;
      conversationId: string | null;
    }

    function post(path: string, body: unknown, extraHeaders: Record<string, string> = {}) {
      return fetch(`${API}${path}`, {
        method: 'POST',
        headers: { ...headers, ...extraHeaders },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(TIMEOUT_MS),
      });
    }

    export async function replyOrReopen(phone: string, text: string, ticketId: string) {
      const windowRes = await post('/v1/conversations/window-status', { phones: [phone] });
      const { results } = (await windowRes.json()) as { results: WindowStatus[] };
      const item = results[0];

      if (item?.isWindowOpen && item.conversationId) {
        return post('/v1/conversations/reply', { conversationId: item.conversationId, body: text });
      }

      return post(
        '/v1/messages',
        { receiver: phone, templateName: 'retomada_atendimento', templateVariables: ['Olá'] },
        { 'Idempotency-Key': `reopen-${ticketId}` },
      );
    }
    ```

    O telefone em `phones` precisa estar no formato gravado na conversa (`customerPhone` em `GET /v1/conversations`). Enviar o template **não** abre a janela. Ela só abre quando o cliente responde. Ver [Janela de 24h](/concepts/24h-window).

    Ao terminar, feche a conversa com `PATCH /v1/conversations/{conversationId}/status` e `{ "status": "CLOSED" }`. Os outros endpoints de conversa estão em [Mensagens](/api-reference/messages#endpoints).
  </Step>
</Steps>

## Armadilhas

<Warning>
  A Arara **não tem** atribuição de conversa a atendente nem trava contra dois atendentes respondendo a mesma conversa. Quem usa só o painel combina a divisão no time. Em ferramenta própria, atribua do seu lado ao receber `message.received` e só libere `POST /v1/conversations/reply` para quem tem a trava.
</Warning>

* **Uma conversa por número de cliente.** Pedido, troca e cadastro do mesmo cliente caem no mesmo histórico. Para separar por ticket, espelhe as mensagens na sua ferramenta via webhook.
* **Opt-out é automático.** Quem responde `STOP`, `SAIR`, `PARAR`, `CANCELAR` ou variações recebe uma confirmação e é marcado como opt-out. `START` ou `VOLTAR` desfaz. Ver [Compliance](/concepts/compliance).

## Próximos passos

<CardGroup cols={2}>
  <Card title="Brain" icon="brain" href="/brain">
    Configuração, base de conhecimento e ações.
  </Card>

  <Card title="Janela de 24h" icon="clock" href="/concepts/24h-window">
    Quando dá para responder sem template.
  </Card>

  <Card title="Eventos de webhook" icon="bell" href="/webhooks/events">
    Payload de cada evento.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.