Search by

wonittecnologia / interage-sdk-php

marcosendler

SDK PHP oficial da API pública de clientes da plataforma Interage+ (Wonit).

Package info

github.com/WonitTecnologia/interage-sdk-php

pkg:composer/wonittecnologia/interage-sdk-php

Statistics

Installs: 10

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-10-04 07:37 UTC

This package is auto-updated.

Last update: 2026-11-11 12:35:58 UTC


README

SDK PHP oficial da API pública de clientes da plataforma Interage+ (Wonit).

  • PHP 8.1+, sem dependências — usa só as extensões curl e json.
  • Respostas tipadas: cada chamada devolve objetos com propriedades readonly, datas como DateTimeImmutable e campos ausentes como null.
  • Mesmo contrato do SDK Go.

Instalação

composer require wonittecnologia/interage-sdk-php

Início rápido

<?php

require 'vendor/autoload.php';

use WonitTecnologia\Interage\Client;

// Domínio do seu tenant (https:// é adicionado automaticamente).
$cli = new Client('SEU-TENANT.wonit.cloud', 'sk_xxxxxxxxxxxx');

$campanhas = $cli->campaigns->list();
foreach ($campanhas->items as $c) {
    echo $c->name, ' — ', $c->status, ' — ', $c->totalContacts, PHP_EOL;
}

Configuração

new Client(string $baseUrl, string $token, ?Options $options = null)

Parâmetro Obrigatório Descrição
$baseUrl sim Domínio do tenant (ex.: SEU-TENANT.wonit.cloud). Sem scheme, o SDK usa https://.
$token sim Token de API sk_<valor> (painel administrativo → Tokens de API).
Options::$timeout não Tempo máximo de cada requisição, em segundos (padrão: 30).
Options::$insecure não Usa http:// quando a baseUrl vem sem scheme. Só para desenvolvimento local.
Options::$transport não Transporte HTTP próprio (proxy, cliente HTTP do seu framework, testes). Padrão: curl.
Options::$connectTimeout não Tempo máximo para abrir a conexão, em segundos (padrão: 10).
Options::$onResponse não Closure(ResponseInfo) chamada a cada resposta: status, cota (X-RateLimit-*) e Idempotency-Replayed.
use WonitTecnologia\Interage\Options;

$cli = new Client('SEU-TENANT.wonit.cloud', 'sk_xxx', new Options(timeout: 10));

O SDK envia o token no header Authorization: Token sk_.... A API também aceita X-API-Key: sk_... e ?token=sk_... na URL — este último só para quem não consegue mandar header, como um link de click to call em outro sistema; para esse uso, crie um token liberado somente para /pabx/telefonia/originate.

As permissões de cada rota (leitura, listagem, criação, alteração, remoção) são configuradas por token no painel. Token ausente, inválido, expirado ou revogado responde 401. 403 é token sem a rota ou sem a ação, cliente desativado ou módulo não licenciado — $apiMessage diz qual.

O SDK não segue redirecionamentos: um 3xx vira UnexpectedResponseException com o destino na mensagem — em geral, baseUrl com http:// em vez de https://.

Domínios

Propriedade Cobre
$cli->campaigns Campanhas de disparo WhatsApp
$cli->messages Instâncias, templates HSM e envio de mensagens
$cli->omni Filas, agentes e conversas
$cli->contacts Central de contatos
$cli->telephony Ramais, histórico de ligações, click-to-call e gravações
$cli->webhooks Verificação da assinatura e leitura das entregas de webhook

Campaigns — campanhas de disparo WhatsApp

Criar campanha com CSV

O CSV deve ter delimitador , ou ; e uma coluna de telefone (phone, telefone, numero, número, celular ou whatsapp). Colunas opcionais: name/nome, email, company/empresa, cpf/documento; as demais viram campos adicionais. Até 20 MB (acima, PayloadTooLargeException) e 200.000 linhas. settings aceita só delay_ms, max_retries, retry_delay_seconds, dispatch_days e dispatch_periods (até 3 horários); templateParams não aceita valor vazio.

use WonitTecnologia\Interage\Enum\CollisionPolicy;
use WonitTecnologia\Interage\Request\CreateCampaignRequest;

$criada = $cli->campaigns->create(new CreateCampaignRequest(
    name: 'Black Friday',
    instanceId: '<instance_id>',   // $cli->messages->listInstances()
    templateId: '<gupshup_id>',     // $cli->messages->listTemplates()
    collisionPolicy: CollisionPolicy::UpdateEmpty,
    fileName: 'contatos.csv',
    fileContent: file_get_contents('contatos.csv'),
    templateParams: ['10%'],
    startAt: new DateTimeImmutable('2026-11-27 09:00'),
    autoStart: true,
    replyWithoutContext: true, // mensagem sem citar o disparo também conta como resposta…
    replyWindowHours: 48,      // …se chegar em até 48h (1 a 72; padrão da API: 24)
    idempotencyKey: 'black-friday-2026', // repetir não cria outra campanha
));
// A importação é assíncrona: nasce "pending" e passa a "ready" ao terminar.
echo $criada->campaignId;
// $criada->replayed === true: a campanha já existia com esta chave

Listar, detalhar e controlar

use WonitTecnologia\Interage\Enum\CampaignStatus;

$pagina = $cli->campaigns->list(status: CampaignStatus::Running, pageSize: 50);
$campanha = $cli->campaigns->get($id);

$cli->campaigns->start($id);   // ready | paused | scheduled → running
$cli->campaigns->pause($id);   // running → paused
$cli->campaigns->cancel($id);  // → canceled (não reinicia)
$cli->campaigns->delete($id);  // não remove em execução/processamento

if (CampaignStatus::tryFrom($campanha->status) === CampaignStatus::Completed) { /* ... */ }

// Só no get(): resultado da importação do CSV
echo $campanha->import?->failedRows, ' ', $campanha->import?->errorMessage;

Messages — instâncias, templates e envio

use WonitTecnologia\Interage\Request\SendMessageRequest;
use WonitTecnologia\Interage\Request\SendTemplateRequest;

$instancias = $cli->messages->listInstances();
$templates = $cli->messages->listTemplates(status: 'APPROVED', instanceId: '<instance_id>');

// Template HSM sem atendimento (só messageId, do provedor)
$envio = $cli->messages->sendTemplate(new SendTemplateRequest(
    to: '5547999999999',
    templateId: '<gupshup_id>',
    instanceId: '<instance_id>',
    params: ['Maria'],
    idempotencyKey: 'pedido-123-boas-vindas', // opcional, recomendado
));

// Template abrindo atendimento para um agente, numa fila
$atendimento = $cli->messages->sendTemplate(new SendTemplateRequest(
    to: '5547999999999',
    templateId: '<gupshup_id>',
    instanceId: '<instance_id>',
    assignedUserId: '<user_uuid>',
    queueId: 3,
));
// internalMessageId, conversationId e protocol só vêm com assignedUserId

// Mensagem livre, dentro da sessão de 24h de uma conversa ativa
$msg = $cli->messages->sendMessage(SendMessageRequest::text('<protocolo>', 'Olá!')->withIdempotencyKey('msg-1'));
$cli->messages->sendMessage(SendMessageRequest::media('<protocolo>', 'image', 'https://.../foto.jpg', caption: 'Veja'));
$cli->messages->sendMessage(SendMessageRequest::location('<protocolo>', -26.91, -48.66, 'Loja Centro'));

// Status de entrega, pelo id interno
$status = $cli->messages->getMessageStatus($msg->internalMessageId);
echo $status->status, ' ', $status->deliveredAt?->format('d/m H:i');

Reenvio seguro (idempotencyKey). Em sendTemplate, sendMessage e campaigns->create, a chave vai no header Idempotency-Key (até 128 caracteres). Por 24 horas, repetir a mesma chave devolve a resposta original, sem novo envio — $resultado->replayed fica true. Gere uma chave por envio e use a mesma ao repetir depois de um timeout ou de UnexpectedResponseException. Mesma chave com outro conteúdo: UnprocessableEntityException (IDEMPOTENCY_KEY_REUSED); envio ainda em andamento: ConflictException.

Provedor recusou (502). SendFailedException (subclasse de ServerException); nada saiu. $e->internalMessageId() traz a mensagem gravada com a falha, quando houve registro.

Omni — filas, agentes e conversas

use WonitTecnologia\Interage\Enum\ConversationStatus;
use WonitTecnologia\Interage\Request\TransferConversationRequest;

$filas = $cli->omni->listQueues();
$agentes = $cli->omni->listAgents();
$conversas = $cli->omni->listConversations(status: ConversationStatus::Queue, channelType: 'whatsapp');

// Mensagens com mediaFileName, edited, removedAt e reactions
$historico = $cli->omni->getConversationHistory('<protocolo>', pageSize: 50);
$cli->omni->transferConversation('<protocolo>', TransferConversationRequest::toQueue(3));
$cli->omni->transferConversation('<protocolo>', TransferConversationRequest::toUser('<user_id>', queueId: 3)); // fila opcional
$cli->omni->closeConversation('<protocolo>');
$cli->omni->closeConversation('<protocolo>', classification: 'Resolvido'); // fila que exige classificação

// Arquivo de uma mensagem do histórico (id da mensagem)
$link = $cli->omni->createMessageFileTempLink($historico->items[0]->id, 3600);
echo $link->url;

Contacts — central de contatos

$pagina = $cli->contacts->list(search: 'joão', pageSize: 20);
$contato = $cli->contacts->get('<contact_uuid>');
$contato = $cli->contacts->getByPhone('5547999999999'); // NotFoundException se não existir
foreach ($contato->labels as $etiqueta) {
    echo $etiqueta->tenantSeq, ' ', $etiqueta->name, PHP_EOL;
}

getByPhone compara só os dígitos, nos canais phone e whatsapp (e sms em cadastros antigos), com ou sem o DDI 55 e o 9º dígito. Número sem nenhum dígito: BadRequestException.

Criar contatos em lote

Até 500 contatos por chamada. Cada item é processado de forma independente — o erro de um não impede os demais.

use WonitTecnologia\Interage\Enum\CollisionPolicy;
use WonitTecnologia\Interage\Request\BatchContactItem;
use WonitTecnologia\Interage\Request\BatchCreateContactsRequest;
use WonitTecnologia\Interage\Request\ContactIdentityInput;

$resultado = $cli->contacts->batchCreate(new BatchCreateContactsRequest(
    contacts: [
        new BatchContactItem(
            name: 'João da Silva',
            identities: [ContactIdentityInput::whatsapp('5511999998888', isPrimary: true)],
            email: 'joao@empresa.com',
            customInfo: ['codigo_cliente' => 'C-123'],
            labels: [1, 3], // números das etiquetas, não os nomes
        ),
    ],
    collisionPolicy: CollisionPolicy::Ignore,
));

foreach ($resultado->items as $item) {
    if ($item->status === 'error') {
        echo "Contato #{$item->index}: {$item->message}", PHP_EOL;
    }
}
  • labels recebe o número de cada etiqueta — o número sequencial exibido em Admin → Contatos → Etiquetas. A etiqueta precisa existir e estar ativa; senão o item falha e nada é gravado para aquele contato.
  • customInfo aceita só texto — a API recusa número, booleano ou objeto: números e datas vão como string ('1990-05-20'). As chaves são os key dos campos cadastrados e ativos.
  • Identidades aceitas: phone→phone; whatsapp→phone|username; telegram→chat_id| username; instagram/facebook→handle|profile_id; sip→extension.
  • cpf: '' envia CPF vazio: com CollisionPolicy::Overwrite, apaga o CPF do contato existente (null não envia nada).
  • Limites por contato: nome 255 caracteres, CPF 14, e-mail/empresa 255, até 10 identidades, até 20 etiquetas, customInfo até 10 KB.

Telephony — ramais, histórico e click-to-call

use WonitTecnologia\Interage\Request\OriginateCallRequest;

$ramais = $cli->telephony->listExtensions(search: '10');

$historico = $cli->telephony->listCallHistory(
    dateFrom: new DateTimeImmutable('2026-09-01'),
    dateTo: new DateTimeImmutable('2026-09-30'), // período de no máximo 3 meses
    callResult: 'ANSWERED',
    extension: '1001', // ramal exato; '10*' busca por prefixo
);

$ativas = $cli->telephony->listActiveCalls();

$chamada = $cli->telephony->originateCall(new OriginateCallRequest('1000', '5547999999999'));

$gravacao = $cli->telephony->createRecordingTempLink('<call_id>', 3600);
// GoneException: gravação arquivada pela retenção — não há restauração

Paginação

As listagens devolvem um Page com total, page, pageSize, items e nextCursor. Campanhas, contatos e histórico de ligações também paginam por cursor, mais indicado para percorrer listas grandes: passe o nextCursor como cursor da próxima chamada. nextCursor é null na última página; com cursor informado, a API ignora page.

$cursor = null;
do {
    $pagina = $cli->contacts->list(pageSize: 100, cursor: $cursor);
    foreach ($pagina->items as $contato) {
        echo $contato->name, PHP_EOL;
    }
    $cursor = $pagina->nextCursor;
} while ($cursor !== null);

Tratamento de erros

Toda exceção do SDK implementa WonitTecnologia\Interage\Exception\InterageException. Erros HTTP viram uma subclasse de ApiException, com $statusCode, $apiStatus, $apiMessage, $retryAfter, $data, $rateLimit e $rawBody.

Exceção Quando
BadRequestException 400 — parâmetros inválidos
UnauthorizedException 401 — token ausente, inválido, expirado ou revogado
ForbiddenException 403 — token sem a rota ou sem a ação, cliente desativado ou módulo não licenciado
NotFoundException 404 — recurso não encontrado
ConflictException 409 — conflito de estado (ramal em chamada ou acionado nos últimos 35 s, mesma idempotencyKey em andamento)
GoneException 410 — gravação arquivada pela retenção; não volta
PayloadTooLargeException 413 — CSV de campanha acima de 20 MB
UnprocessableEntityException 422 — ação não permitida no estado atual, janela de 24h expirada, instância inativa, idempotencyKey com outro conteúdo
TooManyRequestsException 429 — limite de requisições; espere $retryAfter segundos
SendFailedException 502 SEND_FAILED — provedor recusou o envio; internalMessageId()
ServiceUnavailableException 503 — indisponibilidade temporária; espere $retryAfter. Nas rotas de ação, nada foi executado
ServerException qualquer 5xx (inclui as duas acima)
TransportException falha de rede/conexão, sem resposta da API
ValidationException parâmetro inválido detectado pelo SDK, antes de chamar a API
UnexpectedResponseException 3xx, ou 2xx vazio/fora do JSON — numa ação, ela pode ter sido executada
WebhookSignatureException assinatura de webhook inválida

Se a resposta de erro não é o envelope da API (ex.: HTML de um proxy), $apiMessage fica curta e o corpo vai para $rawBody.

use WonitTecnologia\Interage\Exception\ApiException;
use WonitTecnologia\Interage\Exception\ForbiddenException;
use WonitTecnologia\Interage\Exception\NotFoundException;
use WonitTecnologia\Interage\Exception\ServiceUnavailableException;
use WonitTecnologia\Interage\Exception\TooManyRequestsException;

try {
    $campanha = $cli->campaigns->get($id);
} catch (NotFoundException) {
    // campanha não existe
} catch (ForbiddenException $e) {
    // rota/ação não liberada, cliente desativado ou módulo não licenciado: $e->apiMessage
} catch (TooManyRequestsException|ServiceUnavailableException $e) {
    sleep($e->retryAfter ?? 1);
    $campanha = $cli->campaigns->get($id);
} catch (ApiException $e) {
    error_log("{$e->statusCode} {$e->apiStatus}: {$e->apiMessage}");
}

O SDK não repete requisições sozinho — a decisão de esperar e tentar de novo fica com quem chama. Num POST de envio, repita só com a mesma idempotencyKey. 401 é sempre a credencial: repetir com o mesmo token não resolve.

Limite de requisições

Os limites valem para o cliente (tenant), somando todos os tokens dele — criar outro token não traz cota nova. Há duas cotas independentes: ação (originar chamada, enviar mensagem ou template, criar e iniciar campanha, criar contatos em lote — padrão 20 requisições recompostas ao longo de 60 s) e padrão (todas as outras — 120 por 60 s).

Cada resposta autenticada traz X-RateLimit-Limit e X-RateLimit-Remaining: receba-os em Options::$onResponse (ou em $e->rateLimit). Acima do limite, 429 com Retry-After. Há ainda um teto por IP de origem, e 20 tentativas por minuto com token inexistente bloqueiam o IP até a janela fechar (tokens que autenticaram nos últimos 10 minutos continuam passando; token expirado ou revogado não entra nessa conta). Nas rotas de ação, 503 indica que a cota não pôde ser conferida: nada foi executado e dá para repetir.

use WonitTecnologia\Interage\ResponseInfo;

$cli = new Client('SEU-TENANT.wonit.cloud', 'sk_xxx', new Options(
    onResponse: function (ResponseInfo $r): void {
        if ($r->rateLimit !== null && $r->rateLimit->remaining < 5) {
            error_log("cota quase no fim: {$r->rateLimit->remaining}/{$r->rateLimit->limit}");
        }
    },
));

Webhooks

Cada entrega é um POST com events[] (até 50, processe em ordem) assinado em X-Interage-Signature: t=<unix>,v1=<hex>. O SDK confere a assinatura (HMAC-SHA256 de "<t>.<corpo bruto>", qualquer v1 vale — durante a troca do segredo vêm dois —, tolerância de 300 s, comparação em tempo constante) e decodifica os eventos:

use WonitTecnologia\Interage\Exception\WebhookSignatureException;
use WonitTecnologia\Interage\Webhook\WebhookEvent;

$corpo = file_get_contents('php://input'); // corpo bruto: não reserialize o JSON
try {
    $lote = $cli->webhooks->parse(
        $_SERVER['HTTP_X_INTERAGE_SIGNATURE'] ?? '',
        $corpo,
        [$segredoAtual, $segredoAnterior], // o anterior só durante a troca
    );
} catch (WebhookSignatureException) {
    http_response_code(401);
    exit;
}

foreach ($lote->events as $evento) {
    if (jaProcessado($evento->id)) { // deduplique por id — Idempotency-Key muda a cada POST
        continue;
    }
    match ($evento->type) {
        WebhookEvent::MESSAGE_CREATED => tratarMensagem($evento->conversation, $evento->message),
        WebhookEvent::CONVERSATION_CLOSED => tratarEncerramento($evento->conversation),
        WebhookEvent::CALL_ENDED => tratarLigacao($evento->call),
        WebhookEvent::CALL_RECORDING_AVAILABLE => baixarGravacao($evento->recording->url),
        WebhookEvent::EXTENSION_STATUS_CHANGED => tratarRamal($evento->extension),
        default => null, // ping e eventos futuros
    };
}
http_response_code(200); // responda em menos de 5 s; processe em fila

verify() (lança exceção) e isValid() só conferem; decode() só decodifica.

Mudanças na 0.2.0

Incompatíveis (ajuste ao atualizar):

  • 2xx sem corpo, sem data ou fora do JSON agora lança UnexpectedResponseException; antes devolvia o modelo com valores vazios. 3xx também.
  • 503 lança ServiceUnavailableException e 502 SEND_FAILED lança SendFailedException — ambas subclasses de ServerException, então catch (ServerException) continua pegando. Quem compara a classe exata ($e::class === ServerException::class) precisa ajustar.
  • Erro com corpo fora do envelope (HTML de proxy): $apiMessage não traz mais o corpo; ele fica em $rawBody.
  • Transport próprio: Request ganhou $connectTimeout (com valor padrão).

Obsoletos (sempre vazios, a API não devolve mais): Campaign::$scheduledAt, Contact::$visibility, ActiveCall::$channel.

Novos: idempotencyKey/replayed, SendTemplateRequest::$queueId, toUser(..., queueId:), closeConversation(..., classification:), ConversationStatus::Coex, Campaign::$instanceId/ $autoStart/$import, Contact::$labels, mensagens do histórico com $mediaFileName/$edited/ $removedAt/$reactions, GoneException, PayloadTooLargeException, SendFailedException, ServiceUnavailableException, ApiException::$data/$rateLimit, Options::$connectTimeout/ $onResponse, $cli->webhooks e cpf: '' para limpar o CPF no lote.

Testes

composer install
composer test

Licença

MIT — ver LICENSE.