PHPackages                             callcocam/laravel-whatsapp-cloud - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. callcocam/laravel-whatsapp-cloud

ActiveLibrary[API Development](/categories/api)

callcocam/laravel-whatsapp-cloud
================================

Núcleo reutilizável da WhatsApp Cloud API (Meta) para Laravel: envio de templates/sessão/interativo por-tenant, webhook com assinatura e eventos, e gestão de templates via Artisan.

v0.1.0(1mo ago)00MITPHPPHP ^8.3CI passing

Since Jul 8Pushed 1mo agoCompare

[ Source](https://github.com/callcocam/laravel-whatsapp-cloud)[ Packagist](https://packagist.org/packages/callcocam/laravel-whatsapp-cloud)[ RSS](/packages/callcocam-laravel-whatsapp-cloud/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (9)Versions (4)Used By (0)

Laravel WhatsApp Cloud
======================

[](#laravel-whatsapp-cloud)

Núcleo reutilizável da **WhatsApp Cloud API (Meta)** para Laravel: envio de templates / texto de sessão / interativo **por-tenant**, webhook com assinatura e eventos, e gestão de templates via Artisan.

> Extraído do núcleo Meta-only do Coordena. O pacote cuida do **transporte + protocolo + ciclo de vida de template**; o app cuida da **regra de negócio + gatilhos + textos**.

- **Envio** por intenção: `sendTemplate` / `sendSessionText` / `sendInteractive`.
- **Multi-tenant**: credenciais resolvidas por contrato (`WhatsApp::for($tenant)`), com fallback pras credenciais default do config (dev/single-tenant).
- **Webhook** GET (hub-challenge) + POST (assinatura `X-Hub-Signature-256`), disparando eventos — sem regra de negócio dentro do pacote.
- **Templates**: registry (config + runtime), comandos `whatsapp:template:*` e um **painel web** opcional (Inertia + Vue) pra criar/editar/enviar.
- **Erros terminais** da Meta mapeados (`isTerminal()`) pra você não re-tentar o que não adianta (janela de 24h fechada, template pausado, etc.).

Documentação
------------

[](#documentação)

Este README é a referência rápida. Para um passo a passo completo, escolha o guia pelo seu perfil:

GuiaPara quemO que tem dentro🤖 **[Guia para agentes de IA](docs/AGENTS.md)**Um agente (Claude Code, Copilot…) ou dev integrando o pacoteMapa de arquivos, assinaturas exatas, contratos, invariantes, as 3 armadilhas do pacote, receita de integração e checklist anti-erro👤 **[Guia do usuário](docs/GUIA-DO-USUARIO.md)**Quem vai instalar, configurar e operarRegras da Meta explicadas, onde achar cada credencial, ligar o webhook, criar templates, usar o painel e resolver os erros comuns🧪 **[Sandbox](docs/SANDBOX.md)**Quem vai testar um fluxo antes de soltarEnsaiar a conversa inteira — inclusive o handoff pro responsável — sem um celular e **antes de submeter o template à Meta**. Janela de 24h e falhas da Meta de verdade.Requisitos
----------

[](#requisitos)

- PHP `^8.3`
- Laravel `^11.0 || ^12.0 || ^13.0`

Instalação
----------

[](#instalação)

Repositório Git privado, consumido via Composer:

```
// composer.json do app
"repositories": [
    { "type": "vcs", "url": "git@github.com:callcocam/laravel-whatsapp-cloud.git" }
]
```

```
composer require callcocam/laravel-whatsapp-cloud
php artisan whatsapp:install   # publica config + migration e imprime o checklist
php artisan migrate
```

Onboarding de um projeto novo (6 passos)
----------------------------------------

[](#onboarding-de-um-projeto-novo-6-passos)

1. `composer require callcocam/laravel-whatsapp-cloud` (com o `repositories: vcs`).
2. `php artisan whatsapp:install` &amp;&amp; `php artisan migrate`.
3. `.env`: `WHATSAPP_CLOUD_APP_SECRET`, `WHATSAPP_CLOUD_VERIFY_TOKEN`, `WHATSAPP_CLOUD_GRAPH_VERSION`. Cadastre o número — use o model default `WhatsAppNumber` **ou** implemente `WhatsAppCredentials` no seu model + binde um resolver (veja [Credenciais](#credenciais-multi-tenant)).
4. Declare os templates em `config/whatsapp-cloud.php` e crie-os na Meta com `php artisan whatsapp:template:create `.
5. (Opcional) Ouça `WhatsAppMessageReceived` / `WhatsAppStatusReceived`.
6. Envie: `WhatsApp::for($tenant)->sendTemplate('chave', [...params]);`.

Uso
---

[](#uso)

### Enviar

[](#enviar)

```
use Callcocam\WhatsAppCloud\Facades\WhatsApp;
use Callcocam\WhatsAppCloud\Messages\InteractiveMessage;
use Callcocam\WhatsAppCloud\Messages\TemplateMessage;

// Template aprovado (única forma fora da janela de 24h):
WhatsApp::for($team)->sendTemplate('5548999999999', TemplateMessage::make('assignment', [
    'name' => 'Maria', 'event' => 'Congresso', 'url' => 'https://app.test/x',
]));

// Texto livre (só dentro da janela de 24h):
WhatsApp::for($team)->sendSessionText('5548999999999', 'Recebido, obrigado!');

// Pergunta com opções (lista interativa da Meta):
WhatsApp::for($team)->sendInteractive('5548999999999',
    InteractiveMessage::multiChoice('Quando você pode?', ['Sexta à noite', 'Sábado de manhã']));
```

`WhatsApp::for()` sem argumento usa as credenciais `default` do config (dev / single-tenant). Prefira injetar `WhatsAppManager` a usar a Facade em código testável.

### Tratar erros da Meta

[](#tratar-erros-da-meta)

```
use Callcocam\WhatsAppCloud\Exceptions\WhatsAppException;

try {
    WhatsApp::for($team)->sendTemplate(...);
} catch (WhatsAppException $e) {
    if ($e->isTerminal()) {
        // Terminal (janela fechada, template pausado…): logue e NÃO re-tente.
    }
    // Senão: deixe a fila re-tentar (rate limit, rede).
}
```

> `isTemporaryRestriction()` continua funcionando como **alias deprecado** de `isTerminal()` — o nome antigo dizia o oposto do que o método faz.

### Templates

[](#templates)

Declare cada chave de mensagem do app → template aprovado na Meta:

```
// config/whatsapp-cloud.php
'templates' => [
    'assignment' => [
        'name' => 'coordena_assignment',
        'language' => 'pt_BR',
        'category' => 'utility',
        'params' => ['name', 'event', 'url'], // ordem de {{1}}, {{2}}, {{3}}
    ],
],
```

Ou em runtime:

```
use Callcocam\WhatsAppCloud\Templates\MetaTemplate;

WhatsApp::registerTemplate('assignment', new MetaTemplate('coordena_assignment', 'pt_BR', 'utility', ['name', 'event', 'url']));
```

### Painel de templates (Inertia + Vue)

[](#painel-de-templates-inertia--vue)

Uma página web para **criar, listar, editar, apagar e enviar teste** de templates — o mesmo `TemplateManager` do CLI, com preview estilo WhatsApp. Fica em `/whatsapp/cloud/templates` (configurável).

É **opcional e desacoplado do núcleo**: as rotas só são registradas quando o app tem Inertia instalado (`inertiajs/inertia-laravel`). Quem só envia mensagens não carrega nada de frontend.

O **backend** (controller, rotas, `TemplateManager`) é sempre do pacote. A **página** vem em dois modos — escolha um:

ModoComoPra quem**Fallback autônoma**`vendor:publish --tag=whatsapp-cloud-inertia` — página self-contained (CSS próprio, sem design system)Apps sem design system; funciona na hora**Scaffold nativo**`php artisan whatsapp:panel:scaffold` — gera páginas **shadcn-vue** que você passa a ser donoApps com shadcn-vue + `@lucide/vue` + `vue-sonner` + `AppLayout`Nos dois casos o pacote continua dono do backend; só o **contrato de props**(`templates`, `waConfig`, `loadError`, `panelUrl`) atravessa a fronteira — então atualizar o pacote **não exige recopiar a página**.

#### Habilitar (app com Inertia + Vue + Vite)

[](#habilitar-app-com-inertia--vue--vite)

1. **Dependências** (se ainda não tiver Inertia no app):

    ```
    composer require inertiajs/inertia-laravel
    npm install @inertiajs/vue3
    php artisan inertia:middleware   # cria app/Http/Middleware/HandleInertiaRequests.php
    ```

    Registre o middleware no grupo `web` (Laravel 11+ em `bootstrap/app.php`):

    ```
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->web(append: [\App\Http\Middleware\HandleInertiaRequests::class]);
    })
    ```
2. **Publique as páginas Vue** e rebuild o Vite:

    ```
    php artisan vendor:publish --tag=whatsapp-cloud-inertia
    npm run build     # ou: npm run dev
    ```

    O publish copia os componentes para `resources/js/pages/WhatsAppCloud/`, onde o resolver dos starter kits do Laravel — `resolvePageComponent('./pages/**/*.vue')`— os encontra. É o **mesmo destino** do scaffold nativo, então os dois modos do painel caem no mesmo lugar. Nada de CDN/assets do pacote: quem compila é o Vite do app.
3. **Compartilhe o `flash`** no `HandleInertiaRequests::share()` para as mensagens de sucesso e o id da mensagem enviada aparecerem:

    ```
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'flash' => fn () => $request->session()->get('flash'),
        ]);
    }
    ```
4. **Acesse** `/whatsapp/cloud/templates` autenticado. Sem isso, a rota (grupo `auth`) redireciona pro login.

#### Configuração

[](#configuração)

```
// config/whatsapp-cloud.php
'panel' => [
    'enabled'    => env('WHATSAPP_CLOUD_PANEL_ENABLED', true),
    'prefix'     => env('WHATSAPP_CLOUD_PANEL_PREFIX', 'whatsapp/cloud/templates'),
    'name'       => 'whatsapp.cloud.panel',
    'middleware' => ['web', 'auth'],
    // Componente Inertia renderizado. Aponte para sua página nativa (scaffold)
    // ou deixe no default (fallback autônoma).
    'component'  => env('WHATSAPP_CLOUD_PANEL_COMPONENT', 'WhatsAppCloud/Templates/Index'),
    // Gate de autorização opcional: quando setado, o pacote anexa 'can:'
    // à middleware do painel (ele mexe na WABA compartilhada).
    'gate'       => env('WHATSAPP_CLOUD_PANEL_GATE'),
    // Moeda (ISO) do card de gasto estimado; null mostra o número puro.
    'currency'   => env('WHATSAPP_CLOUD_PANEL_CURRENCY'),
    'ui_token'   => env('WHATSAPP_CLOUD_PANEL_UI_TOKEN'), // defesa extra opcional
],
```

Env correspondente:

```
WHATSAPP_CLOUD_PANEL_ENABLED=true
WHATSAPP_CLOUD_PANEL_PREFIX=whatsapp/cloud/templates
# WHATSAPP_CLOUD_PANEL_COMPONENT=WhatsAppCloud/Templates/Index
# WHATSAPP_CLOUD_PANEL_GATE=manage-whatsapp-templates
# WHATSAPP_CLOUD_PANEL_CURRENCY=BRL
# WHATSAPP_CLOUD_PANEL_UI_TOKEN=um-token-secreto
```

O painel é poderoso (cria/apaga/envia numa WABA que costuma ser **compartilhada**entre times), então proteja-o:

- **Gate** (recomendado): setar `panel.gate` faz o pacote anexar `can:` à middleware. Defina o gate no app (ex.: `Gate::define('manage-whatsapp-templates', …)`).
- **`middleware`**: já vem `['web','auth']` — troque/estenda como quiser.
- **`ui_token`**: se `WHATSAPP_CLOUD_PANEL_UI_TOKEN` estiver setado, toda requisição precisa mandar o mesmo valor no header `X-WA-UI-Token` (defesa em profundidade).

Link no seu menu com o nome de rota: `route('whatsapp.cloud.panel.index')`.

#### UI nativa no seu design system (scaffold)

[](#ui-nativa-no-seu-design-system-scaffold)

Quer o painel com a **cara do seu app** (shadcn-vue) em vez da página autônoma? Rode:

```
php artisan whatsapp:panel:scaffold           # --force pra sobrescrever
```

Copia páginas **shadcn-vue** para `resources/js/pages/WhatsAppCloud/Templates/` (que você passa a ser dono), removendo o sufixo `.stub`. Depois:

1. Aponte o painel: `WHATSAPP_CLOUD_PANEL_COMPONENT=WhatsAppCloud/Templates/Index`.
2. Garanta os pré-requisitos: shadcn-vue (`@/components/ui/*`), `@lucide/vue`, `vue-sonner` e `@/layouts/AppLayout.vue`.
3. `npm run build` (e typecheck com `vue-tsc --noEmit`).

O contrato de props é o mesmo da fallback (`templates`, `waConfig`, `loadError`, `panelUrl`), e o `flash` de sucesso vem no shape `flash.toast = { type, message }`(no `send`, com `flash.sent_id`) — o Index nativo já faz a ponte pro `vue-sonner`.

#### Usando a interface

[](#usando-a-interface)

- **Listagem**: tabela com nome, idioma, categoria e status; filtros por status / categoria, busca por nome e contadores no topo.
- **Novo / Editar**: formulário completo (cabeçalho, corpo com `{{1}}`, exemplos por variável, rodapé e botões `QUICK_REPLY` / `URL` / `PHONE_NUMBER`) com **pré-visualização ao vivo** estilo bolha do WhatsApp. Nome e idioma são imutáveis ao editar.
- **Enviar teste**: dispara um template aprovado pra um número, preenchendo as variáveis do corpo.
- **Apagar**: remove **todos os idiomas** com aquele nome na WABA (há confirmação).
- **Gastos (estimado)**: card com o custo do mês atual e a quebra por categoria, lido de `conversation_analytics` da WABA. É **best-effort** — o token precisa da permissão `whatsapp_business_management`; sem ela (ou em falha), o card some e o resto do painel segue. O valor é **estimado** (a fonte oficial é o WhatsApp Manager); ajuste a moeda com `panel.currency`.

Notas: CSRF é automático (Inertia). **Criar e editar são assíncronos** — vão para análise da Meta (status `PENDING`); editar um aprovado o reseta para nova análise. A categoria pode ser reclassificada pela Meta. O painel opera no tenant `default`; o gancho para escolher o número/tenant é o `TemplatePanelController::tenant()`.

#### Integração no app consumidor (Coordena — Parte B)

[](#integração-no-app-consumidor-coordena--parte-b)

> **Nota de handoff.** A **Parte A** (este pacote) está pronta na branch `feature/template-panel`. A **Parte B** (página nativa no design system) é feita no repo do **Coordena** — plano completo em [`docs/plans/2026-07-09-painel-templates-nativo-host.md`](docs/plans/2026-07-09-painel-templates-nativo-host.md).

Commits do pacote que o Coordena consome (branch `feature/template-panel`):

- **`2091f0c`** — painel headless: `panel.component` configurável, `panel.gate`, flash normalizado (`flash.toast`) e o comando `whatsapp:panel:scaffold` + stubs nativos (shadcn-vue).
- **`61e614a`** — card de gasto estimado (`TemplateManager::costs()` + prop `costs`
    - `panel.currency`).

**Contrato de props (congelado — o backend fica no pacote):**

PropTipoDescrição`templates``array`Templates crus da Meta (`{ id, name, language, category, status, components[], rejected_reason? }`)`waConfig``{ waba_id, phone_number_id, api_version }`Credenciais **públicas** (nunca o token)`loadError``string | null`Erro ao carregar a lista`costs``object | null``{ currency, total, conversations, period:{start,end}, byCategory:[{category,cost,conversations}] }` — `null` quando indisponível`panelUrl``string`Base das rotas de mutação (o host concatena; **sem wayfinder**)Config que o Coordena publica: `panel.component = WhatsAppCloud/Templates/Index`, `panel.gate = manage-whatsapp-templates`, `panel.currency = BRL`. Fluxo de sucesso via `flash.toast = { type, message }` (`send` inclui `flash.sent_id`); erros via `errors.meta` / `errors.form`.

### Credenciais (multi-tenant)

[](#credenciais-multi-tenant)

Implemente o contrato no seu model (ou use a trait) e binde um resolver:

```
use Callcocam\WhatsAppCloud\Contracts\WhatsAppCredentials;
use Callcocam\WhatsAppCloud\Support\HasWhatsAppCredentials;

class TeamWhatsappConnection extends Model implements WhatsAppCredentials
{
    use HasWhatsAppCredentials; // lê phone_number_id, cloud_access_token, waba_id
}
```

```
// AppServiceProvider::register()
use Callcocam\WhatsAppCloud\Contracts\WhatsAppCredentialsResolver;

$this->app->bind(WhatsAppCredentialsResolver::class, fn () => new class implements WhatsAppCredentialsResolver {
    public function resolve(mixed $context): ?WhatsAppCredentials
    {
        return $context instanceof \App\Models\Team ? $context->whatsappConnection : null;
    }
});
```

Projeto novo que não quer escrever resolver: guarde os números no model default `WhatsAppNumber` e binde o `ModelCredentialsResolver` (busca por `key`).

### Webhook

[](#webhook)

O provider registra `GET|POST {prefix}` (default `webhooks/whatsapp/cloud`, no grupo `api` — sem CSRF). Aponte o webhook da Meta pra essa URL. Para registrar você mesmo, ponha `whatsapp-cloud.webhook.enabled = false` e mire o `WebhookController`.

```
// Um listener no app:
use Callcocam\WhatsAppCloud\Events\WhatsAppStatusReceived;

Event::listen(function (WhatsAppStatusReceived $event) {
    logger()->info('status', ['id' => $event->status['id'], 'status' => $event->status['status']]);
});
```

Eventos: `WhatsAppMessageReceived`, `WhatsAppStatusReceived`, `WhatsAppWebhookVerified`.

Comandos Artisan
----------------

[](#comandos-artisan)

```
php artisan whatsapp:template:list                 # lista templates da WABA + status
php artisan whatsapp:template:get            # detalha um template
php artisan whatsapp:template:create         # cria na Meta a partir de um arquivo de definição
php artisan whatsapp:template:send   ... # envia um template aprovado
```

Todos aceitam `--tenant=` (contexto pro resolver). O `create` lê `config('whatsapp-cloud.definitions_path')/.php` (ou `--path=`), um arquivo que retorna `TemplateBuilder::...->toArray()`. A pasta **não vem pronta** — `definitions_path` é `null` por padrão; crie-a e aponte o config/env.

**Não há comando de `edit` nem de `delete`**: para alterar ou apagar um template já criado, use o [painel](#painel-de-templates-inertia--vue) ou o `TemplateManager`. Re-rodar `create` com um nome+idioma que já existe falha na Meta. Detalhes do ciclo de vida (incl. a sincronia entre o arquivo de definição e o registry `templates`) no [guia de agentes](docs/AGENTS.md#9-a-pasta-de-defini%C3%A7%C3%B5es-e-a-manuten%C3%A7%C3%A3o-dos-templates)e no [guia do usuário](docs/GUIA-DO-USUARIO.md#a-pasta-de-templates-para-quem-usa-o-terminal).

Configuração
------------

[](#configuração-1)

Veja [`config/whatsapp-cloud.php`](config/whatsapp-cloud.php): `graph_version`, `driver` ([sandbox](docs/SANDBOX.md)), `app_secret`, `verify_token`, `default`(creds dev), `webhook`, `panel`([painel de templates](#painel-de-templates-inertia--vue)), `sandbox`, `model`, `templates`, `definitions_path`.

Sandbox
-------

[](#sandbox)

Testar um fluxo hoje significa mandar mensagem pra um número real e ficar com o celular na mão. O sandbox troca o fio que vai pra Meta por um simulador: você encena a conversa inteira numa tela com cara de WhatsApp — inclusive o handoff pro responsável — e os listeners do seu app rodam **de verdade**, porque as respostas entram pela rota de webhook real, assinadas com o HMAC real.

```
WHATSAPP_CLOUD_DRIVER=sandbox
```

O código do app **não muda**. E o corpo do template vem do arquivo de definição local, então dá pra fechar o texto, os botões e o fluxo **antes de submeter o template à Meta** — o que importa, porque `whatsapp:template:create` é one-way.

A janela de 24h é real (fora dela só template passa, e texto livre volta como `131047`), e as falhas da Meta podem ser injetadas — inclusive as retentáveis, que são as que exercitam o backoff da sua fila.

→ **[Guia do sandbox](docs/SANDBOX.md)**

Testes
------

[](#testes)

```
composer test          # pint --test + phpstan + pest
```

Segurança
---------

[](#segurança)

Nunca versione tokens. `cloud_access_token` fica `encrypted` no model; segredos (`app_secret`, `verify_token`, tokens) só via `.env`.

Licença
-------

[](#licença)

MIT.

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance91

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

48d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/7425411?v=4)[Claudio Campos](/maintainers/callcocam)[@callcocam](https://github.com/callcocam)

---

Top Contributors

[![webcall](https://avatars.githubusercontent.com/u/7212238?v=4)](https://github.com/webcall "webcall (24 commits)")

---

Tags

laravelmessagingwhatsappmetawhatsapp-cloud-api

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/callcocam-laravel-whatsapp-cloud/health.svg)

```
[![Health](https://phpackages.com/badges/callcocam-laravel-whatsapp-cloud/health.svg)](https://phpackages.com/packages/callcocam-laravel-whatsapp-cloud)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k31.8M163](/packages/laravel-cashier)[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k161.4k2](/packages/mike-bronner-laravel-model-caching)[forjedio/inertia-table

Backend-driven dynamic tables for Laravel + Inertia.js

272.0k](/packages/forjedio-inertia-table)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5226.7k](/packages/simplestats-io-laravel-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
