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

# SDK PHP / Laravel

> SDK PHP oficial da Arara (ararahq/sdk): PHP 8.2+, Guzzle 7, argumentos nomeados, Idempotency-Key automática, retry seguro e exceções tipadas. Funciona em Laravel e PHP puro.

## Instalar

```bash theme={null}
composer require ararahq/sdk
```

[![Packagist](https://img.shields.io/packagist/v/ararahq/sdk)](https://packagist.org/packages/ararahq/sdk)

Versão estável publicada: **2.0.0**. Requer PHP 8.2+, `ext-json` e `guzzlehttp/guzzle` ^7.0. Autoload PSR-4 no namespace `Arara\`.

## Configurar

```php theme={null}
use Arara\Arara;
use Arara\Config;

$sdk = new Arara(new Config(
    apiKey: getenv('ARARA_API_KEY'),
    baseUrl: 'https://api.ararahq.com', // opcional, este é o default
    timeout: 30,                        // opcional, em segundos
    retryTimes: 3,                      // opcional
    retryDelayMs: 100,                  // opcional, base do backoff
    apiVersion: 'v1',                   // opcional, este é o default
));
```

O SDK monta as URLs como `{baseUrl}/{apiVersion}/...`. Não inclua `/v1` no `baseUrl`.

### Laravel

Registre o cliente como singleton e receba-o por injeção. Setup completo em [Integração Laravel](/integracoes/laravel).

```php theme={null}
// app/Providers/AppServiceProvider.php
use Arara\Arara;
use Arara\Config;

public function register(): void
{
    $this->app->singleton(Arara::class, fn () => new Arara(new Config(
        apiKey: config('services.arara.key'),
    )));
}

// Em qualquer controller ou job
public function store(Request $request, Arara $sdk)
{
    $sdk->messages->send(
        receiver: $request->phone,
        templateName: 'confirmacao_pedido',
        variables: [$request->name, $request->orderId],
        idempotencyKey: 'pedido-' . $request->orderId . '-confirmacao',
    );
}
```

## Enviar a primeira mensagem

Você precisa de plano assinado, número conectado, um template aprovado e saldo. Veja [Antes de começar](/quickstart#antes-de-começar). Troque `pedido_confirmado` pelo nome do seu template.

Salve como `enviar.php`:

```php theme={null}
<?php

require 'vendor/autoload.php';

use Arara\Arara;
use Arara\Config;

$sdk = new Arara(new Config(apiKey: getenv('ARARA_API_KEY')));

$response = $sdk->messages->send(
    receiver: '+5511999999999',
    templateName: 'pedido_confirmado',
    variables: ['João', '12345'],
    idempotencyKey: 'pedido-12345-confirmacao',
);

echo $response['id'], PHP_EOL;     // ara_msg_...
echo $response['status'], PHP_EOL; // status inicial da mensagem
```

Rode:

```bash theme={null}
ARARA_API_KEY=ara_live_... php enviar.php
```

Os métodos devolvem `array` (o JSON da resposta decodificado). Só `templates->list()` e `smartLinks->list()` devolvem `Arara\Pagination\PaginatedResponse`, com `data` e `pagination`. O `receiver` aceita `+5511999999999`, `whatsapp:+5511999999999` ou só os dígitos: o SDK não valida o formato, quem valida é a API (número inválido volta como 422).

Com variáveis no corpo do template e mídia no header:

```php theme={null}
$sdk->messages->send(
    receiver: '+5511999999999',
    templateName: 'confirmacao_pedido',
    variables: [
        'https://meusite.com/nota/123.pdf', // header (mídia opcional)
        'João',                              // {{1}}
        'ARA-12345',                         // {{2}}
    ],
);
```

Para responder dentro da janela de 24h, mande `body` em vez de `templateName`:

```php theme={null}
$sdk->messages->send(
    receiver: '+5511999999999',
    body: 'Oi! Vi sua dúvida e respondo já já.',
);
```

Cada chamada de `send` leva um header `Idempotency-Key`. Passe `idempotencyKey:` derivado do evento do seu sistema (id do pedido, id da tentativa) para que reenviar o mesmo pedido não duplique a mensagem. Sem ele, o SDK gera um UUID novo por chamada.

Para consultar uma mensagem depois do envio, use `$sdk->messages->get($id)`. O status também chega pelo webhook `message.status_updated`.

Para até 1000 mensagens do mesmo template numa chamada:

```php theme={null}
$sdk->messages->sendBatch('pedido_confirmado', [
    ['receiver' => '+5511999999999', 'variables' => ['João', '12345']],
    ['receiver' => '+5511888888888', 'variables' => ['Maria', '12346']],
], idempotencyKey: 'lote-2026-10-10');
```

## Templates

`get` e `delete` recebem o **id** do template (UUID), não o nome.

```php theme={null}
// Listar: a resposta vem paginada (PaginatedResponse)
$page = $sdk->templates->list(page: 0, size: 50);
foreach ($page->data as $template) {
    echo $template['name'], ' ', $template['status'], PHP_EOL;
}
// $page->pagination->totalPages, $page->hasNextPage()

// Achar pelo nome
$found = $sdk->templates->list(name: 'pedido_confirmado');

// Criar
$template = $sdk->templates->create([
    'name' => 'promo_natal',
    'category' => 'MARKETING',
    'language' => 'pt_BR',
    'body' => 'Oi {{1}}, oferta de Natal: {{2}}% off até {{3}}.',
    'samples' => ['1' => 'João', '2' => '30', '3' => 'sexta'],
]);

// Detalhe
$detail = $sdk->templates->get($template['id']);

// Remover
$sdk->templates->delete($template['id']);
```

`templates->getStatus($id)` e `templates->analytics($id)` também existem.

## Tratar erros

```php theme={null}
use Arara\Exceptions\AraraException;
use Arara\Exceptions\PlanFeatureLockedException;
use Arara\Exceptions\RateLimitException;

try {
    $sdk->messages->send(
        receiver: '+5511999999999',
        templateName: 'confirmacao_pedido',
        idempotencyKey: 'pedido-12345-confirmacao',
    );
} catch (RateLimitException $e) {
    sleep($e->retryAfter ?? 1);
} catch (PlanFeatureLockedException $e) {
    echo "Recurso {$e->feature} exige o plano {$e->upgradeTo}";
} catch (AraraException $e) {
    echo "HTTP {$e->statusCode} {$e->errorCode}: {$e->getMessage()}";
    print_r($e->details);
}
```

| Exceção | Quando |
| - | - |
| `BadRequestException` | 400 |
| `AuthenticationException` | 401 |
| `NotFoundException` | 404 |
| `ValidationException` | 422, ou campo obrigatório vazio / mais de um tipo de conteúdo antes do envio |
| `RateLimitException` | 429 (`$retryAfter` em segundos) |
| `ForbiddenException` | 403 |
| `PlanFeatureLockedException` | 403 `PLAN_FEATURE_LOCKED` (`$feature`, `$currentPlan`, `$upgradeTo`) |
| `InternalServerException` | 5xx (`$statusCode` traz o status real, como 502 ou 503) |
| `AraraException` | qualquer outro status, como 402 e 409 |

Todas ficam em `Arara\Exceptions\` e estendem `AraraException`, que expõe `$statusCode`, `$errorCode` (`error.code` da API), `$details` (`error.details`), `$retryAfter`, `$response` (corpo decodificado) e `getMessage()`. A API responde 401 para chave inválida, revogada ou expirada, e 403 quando o header `Authorization` está ausente ou a chave não tem permissão para a rota; nesses dois casos o corpo vem fora do envelope e `$errorCode` é `null`. Falha de conexão chega como exceção do Guzzle (`GuzzleHttp\Exception\ConnectException`), não como `AraraException`. Lista de códigos em [Erros](/erros).

## Retentativas e idempotência

Via middleware de retry do Guzzle, o SDK retenta até `retryTimes` vezes em erro de conexão, 429 e qualquer 5xx. Respeita `Retry-After` quando numérico; sem ele, espera `retryDelayMs × 2^(tentativa-1)`. Para desligar: `retryTimes: 0`.

`POST` e `PATCH` só são repetidos quando a requisição leva `Idempotency-Key`. `messages->send()`, `messages->sendBatch()` e `campaigns->create()` mandam a chave sempre e reaproveitam a mesma em todas as tentativas, então um retry não duplica a mensagem. Chamadas `POST`/`PATCH` sem a chave (por exemplo `templates->create()`) não são repetidas.

## Webhooks

A string assinada é `"{X-Arara-Timestamp}.{corpo cru}"`. Valide antes de processar e trate o evento pelo campo `event`. O SDK faz a validação com `WebhookUtils::verifySignature()`, passando o corpo cru, o header de assinatura, o secret e o timestamp (tolerância padrão de 300 segundos):

```php theme={null}
use Arara\Utils\WebhookUtils;

$payload = file_get_contents('php://input');

$valid = WebhookUtils::verifySignature(
    payload: $payload,
    signature: $_SERVER['HTTP_X_ARARA_SIGNATURE'] ?? '',
    secret: getenv('ARARA_WEBHOOK_SECRET'),
    timestamp: $_SERVER['HTTP_X_ARARA_TIMESTAMP'] ?? '',
);

if (! $valid) {
    http_response_code(401);
    exit;
}
```

Sem o SDK, a mesma validação à mão:

```php theme={null}
$payload = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_ARARA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_ARARA_SIGNATURE'] ?? '';
$secret = getenv('ARARA_WEBHOOK_SECRET');

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
$fresh = ctype_digit($timestamp) && abs(time() - (int) $timestamp) <= 300;

if (! $fresh || ! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($payload, true);

switch ($event['event']) {
    case 'message.status_updated':
        $messageId = $event['data']['id'];   // ara_msg_...
        $status = $event['data']['status'];  // delivered, read, failed...
        break;
    case 'message.received':
        $from = $event['data']['from'];
        $body = $event['data']['body'];
        break;
}
```

Os helpers `isInboundMessageEvent()` e `isRevenueRecoveryEvent()` de `WebhookUtils` não reconhecem os eventos atuais (ver limitações): compare o campo `event` você mesmo. Detalhes do payload em [Webhooks](/webhooks). Controller Laravel pronto em [Integração Laravel](/integracoes/laravel#webhook).

## Recursos disponíveis

| Recurso | Acesso | Métodos |
| - | - | - |
| Messages | `$sdk->messages` | `send(receiver, templateName?, variables?, body?, mediaUrl?, extra?, idempotencyKey?)`, `sendBatch(templateName, messages, idempotencyKey?)`, `get($id)` |
| Templates | `$sdk->templates` | `list()`, `get($id)`, `getStatus($id)`, `create($data)`, `delete($id)`, `analytics($id?, $period)` |
| Contacts | `$sdk->contacts` | `list()`, `get($phone)`, `update($phone, $data)`, `importBatch()`, `stats()`, `listTags()`, `reactivationCandidates()`, `messages($phone)` |
| Conversations | `$sdk->conversations` | `list()`, `messages($id)`, `reply($id, $body)`, `updateStatus()`, `leadStats()`, `windowStatus($phones)` |
| Campaigns | `$sdk->campaigns` | `create($data, $idempotencyKey?)`, `list()`, `get($id)`, `cancel($id)`, `estimate()` |
| Opt-outs | `$sdk->optOuts` | `list()`, `add($phone, $reason?)`, `get($phone)`, `remove($phone)` |
| Wallet | `$sdk->wallet` | `transactions()`, `getAutoRecharge()`, `updateAutoRecharge($data)` |
| Numbers | `$sdk->numbers` | `list()`, `update()`, `delete()`, `request()`, `listRequests()`, `sync()`, `warming()` |
| Smart links | `$sdk->smartLinks` | `create()`, `update()`, `list()`, `stats()` |
| Auth | `$sdk->auth()` | `me()` (exige chave de administrador) |

## Limitações conhecidas

| Método | Problema |
| - | - |
| `WebhookUtils::isInboundMessageEvent()` | Compara com `inbound_message`; a API envia `message.received`. |
| `WebhookUtils::isRevenueRecoveryEvent()` | Compara com `revenue_recovery`; a API envia `cart.abandoned`, `payment.failed`, `pix.created`, `boleto.due`. |
| `templates->get/delete` | Precisam do id (UUID). Com o nome a API responde 400. |

<Note>
  Quem está na 1.8.1 deve atualizar para a 2.0.0 (`composer require ararahq/sdk:^2.0`). Na 1.8.1, `WebhookUtils::verifySignature()` assina só o corpo, sem o timestamp, e nunca bate com `X-Arara-Signature`; `receiver` sem o prefixo `whatsapp:` é recusado pelo SDK; `users` e `organizations` chamam rotas que não existem; e `send` não manda `Idempotency-Key`, mas o retry reenvia o `POST` em 5xx. Veja o [CHANGELOG](https://github.com/ararahq/arara-php-sdk/blob/main/CHANGELOG.md).
</Note>

## Próximos passos

<CardGroup cols={2}>
  <Card title="Integração Laravel" icon="php" href="/integracoes/laravel">
    Service provider, jobs em fila, controller de webhook.
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/introduction">
    Endpoints REST por baixo do SDK.
  </Card>
</CardGroup>

Código-fonte: [github.com/ararahq/arara-php-sdk](https://github.com/ararahq/arara-php-sdk) (MIT).


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