> ## 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.

# Next.js

> Envie mensagens de uma Server Action e receba os eventos da Arara num route handler do Next.js (App Router).

Ao final desta página, seu projeto Next.js envia um template por uma Server Action e registra o status de cada mensagem que chega pelo webhook.

## Antes de começar

* Node 18 ou mais novo e Next 15 ou mais novo, com App Router. No Pages Router o caminho é análogo.
* O SDK instalado e uma chave de API em `ARARA_API_KEY`. Siga [SDK Node](/sdks/node).
* Um template aprovado. Os exemplos usam `confirmacao_pedido`. Veja [Templates](/templates/policy).
* Uma URL `https` pública para o webhook. Em desenvolvimento, veja [Testar localmente](/webhooks#testar-localmente).

<Note>
  O SDK roda no runtime Node do Next.js (route handlers e Server Actions). No Edge Runtime, chame a API REST com `fetch`: o SDK depende do `axios`, que não roda em edge.
</Note>

## Instância do SDK

Crie uma instância única. Em desenvolvimento, o hot reload recriaria o cliente a cada alteração.

```typescript theme={null}
// lib/arara.ts
import { NodeSDK } from '@ararahq/sdk';

declare global {
  // eslint-disable-next-line no-var
  var araraInstance: NodeSDK | undefined;
}

export const arara =
  globalThis.araraInstance ??
  new NodeSDK({
    apiKey: process.env.ARARA_API_KEY!,
  });

if (process.env.NODE_ENV !== 'production') {
  globalThis.araraInstance = arara;
}
```

As demais opções do cliente estão em [SDK Node](/sdks/node).

## Enviar numa Server Action

```typescript theme={null}
// app/checkout/actions.ts
'use server';

import { arara } from '@/lib/arara';
import { revalidatePath } from 'next/cache';

export async function sendOrderConfirmation(orderId: string, phone: string, customerName: string) {
  const response = await arara.messages.send(
    {
      receiver: phone,
      templateName: 'confirmacao_pedido',
      templateVariables: [customerName, orderId],
    },
    { idempotencyKey: `order-confirmation-${orderId}` },
  );

  revalidatePath('/admin/orders');
  return { id: response.id, status: response.status };
}
```

A API responde `202` com o `status` inicial, por exemplo `PENDING`. A entrega chega depois, pelo webhook.

No client component:

```tsx theme={null}
// app/checkout/page.tsx
'use client';

import { sendOrderConfirmation } from './actions';
import { useTransition } from 'react';

export default function CheckoutPage() {
  const [pending, startTransition] = useTransition();

  return (
    <form action={(formData) => {
      startTransition(async () => {
        await sendOrderConfirmation(
          formData.get('orderId') as string,
          formData.get('phone') as string,
          formData.get('name') as string,
        );
      });
    }}>
      {/* campos */}
      <button disabled={pending}>{pending ? 'Enviando...' : 'Confirmar pedido'}</button>
    </form>
  );
}
```

## Webhook

<Steps>
  <Step title="Guarde o secret">
    No painel, abra [Webhooks](https://app.ararahq.com/dashboard/webhooks), gere o secret e copie o valor para o `.env.local`:

    ```ini theme={null}
    ARARA_WEBHOOK_SECRET=whsec_...
    ```
  </Step>

  <Step title="Crie o route handler">
    O handler lê o corpo bruto, verifica a assinatura e registra o ID e o status de cada mensagem.

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

    export const runtime = 'nodejs';
    export const dynamic = 'force-dynamic';

    const TOLERANCE_SECONDS = 300;

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

    function isRecord(value: unknown): value is Record<string, unknown> {
      return typeof value === 'object' && value !== null;
    }

    export async function POST(req: NextRequest) {
      const secret = process.env.ARARA_WEBHOOK_SECRET;
      if (!secret) {
        return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
      }

      const body = await req.text();
      const timestamp = req.headers.get('x-arara-timestamp') ?? '';
      const signature = req.headers.get('x-arara-signature') ?? '';

      if (!isValidSignature(body, timestamp, signature, secret)) {
        return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
      }

      const payload: unknown = JSON.parse(body);
      if (!isRecord(payload) || !isRecord(payload.data)) {
        return new NextResponse(null, { status: 204 });
      }

      const deliveryId = req.headers.get('x-arara-delivery-id');
      const { data } = payload;

      switch (payload.event) {
        case 'message.status_updated':
          console.info('arara.message.status', { deliveryId, id: data.id, status: data.status });
          break;
        case 'message.received':
          console.info('arara.message.received', { deliveryId, id: data.id, from: data.from, type: data.type });
          break;
        case 'conversation.needs_human':
          console.info('arara.handoff', { deliveryId, conversationId: data.conversationId, reason: data.reason });
          break;
      }

      return new NextResponse(null, { status: 204 });
    }
    ```

    Troque os `console.info` pela gravação no seu banco. `data.id` é o mesmo ID devolvido no envio. `data.status` vem em minúsculas: `queued`, `sent`, `delivered`, `read`, `failed`.
  </Step>

  <Step title="Cadastre a URL">
    Na mesma tela do painel, informe `https://seu-dominio.com/api/webhooks/arara` e dispare o evento de teste.
  </Step>
</Steps>

<Warning>
  A assinatura depende dos bytes exatos do corpo. Use `req.text()` antes de verificar. Com `req.json()` a conta não fecha.
</Warning>

Responda `2xx` rápido. Quando a entrega falha, a Arara tenta de novo, então o handler precisa aguentar o mesmo evento mais de uma vez. Use o header `X-Arara-Delivery-Id` para deduplicar. Prazos e intervalos estão em [Entrega](/webhooks#entrega).

### Tipos do evento

O SDK exporta guards de tipo para o corpo do evento:

```typescript theme={null}
import { WebhookUtils } from '@ararahq/sdk';

const payload: unknown = JSON.parse(body);

if (WebhookUtils.isMessageStatusEvent(payload)) {
  console.log(payload.data.status, payload.data.receiver);
}
```

<Note>
  O tipo `MessageStatusData` do SDK 2.0.0 declara `messageId`, mas a API envia nesse campo o UUID interno. O id `ara_msg_...` (o mesmo devolvido no envio) está em `data.id`, que o tipo não declara: leia-o com um cast, por exemplo `(payload.data as { id?: string }).id`.
</Note>

## Consultar status de uma mensagem

Para gravar o status, prefira o webhook `message.status_updated`. Se você precisa consultar sob demanda (tela de suporte, reconciliação), use `arara.messages.get`:

```typescript theme={null}
// app/api/messages/[id]/route.ts
import { AraraError } from '@ararahq/sdk';
import { arara } from '@/lib/arara';

export const runtime = 'nodejs';

export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  try {
    const msg = await arara.messages.get(id);
    return Response.json({ id: msg.id, status: msg.status, reason: msg.reason });
  } catch (error) {
    const notFound = error instanceof AraraError && error.statusCode === 404;
    return Response.json({ error: 'Mensagem indisponível' }, { status: notFound ? 404 : 502 });
  }
}
```

Aqui `status` vem em maiúsculas (`PENDING`, `SENT`, `DELIVERED`, `READ`, `FAILED`...), diferente do webhook, que usa minúsculas. Quando a mensagem falha, `reason` traz o motivo.

<Warning>
  `GET /v1/messages/{id}` só devolve mensagens enviadas pelo mesmo usuário dono da chave. Mensagem enviada por outro membro da organização responde 403. A chave também precisa da permissão `READ` (ou `ADMIN`).
</Warning>

## Idempotência

Em rotas que enviam mensagem (Server Action, handler de webhook que responde ao cliente), passe `idempotencyKey`. O SDK envia no header `Idempotency-Key`:

```typescript theme={null}
await arara.messages.send(
  { receiver: '+5511999999999', templateName: 'confirmacao_pedido', templateVariables: ['João', 'ARA-123'] },
  { idempotencyKey: 'order-confirmation-ARA-123' }
);
```

Derive a chave do seu evento (id do pedido), não de `randomUUID()`: assim uma nova tentativa da função reaproveita a mesma chave. A mesma chave devolve a mensagem original sem novo envio. Em `POST /v1/messages` isso vale mesmo se o payload mudar (a API não compara destinatário nem variáveis), então nunca reaproveite a chave para outra mensagem.

## Próximos passos

<CardGroup cols={2}>
  <Card title="Eventos" icon="list-check" href="/webhooks/events">
    Corpo e campos de cada evento.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/webhooks">
    Retentativas, deduplicação e teste local.
  </Card>

  <Card title="SDK Node" icon="node" href="/sdks/node">
    Métodos disponíveis na versão publicada.
  </Card>

  <Card title="Receitas" icon="book-open" href="/recipes">
    Carrinho abandonado, confirmação de pedido, suporte.
  </Card>
</CardGroup>


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