wonittecnologia / interage-sdk-php
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
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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
curlejson. - Respostas tipadas: cada chamada devolve objetos com propriedades
readonly, datas comoDateTimeImmutablee campos ausentes comonull. - 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 —
$apiMessagediz 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; } }
labelsrecebe 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.customInfoaceita 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 oskeydos 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: comCollisionPolicy::Overwrite, apaga o CPF do contato existente (nullnão envia nada).- Limites por contato: nome 255 caracteres, CPF 14, e-mail/empresa 255, até 10
identidades, até 20 etiquetas,
customInfoaté 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
dataou fora do JSON agora lançaUnexpectedResponseException; antes devolvia o modelo com valores vazios. 3xx também. - 503 lança
ServiceUnavailableExceptione 502SEND_FAILEDlançaSendFailedException— ambas subclasses deServerException, entãocatch (ServerException)continua pegando. Quem compara a classe exata ($e::class === ServerException::class) precisa ajustar. - Erro com corpo fora do envelope (HTML de proxy):
$apiMessagenão traz mais o corpo; ele fica em$rawBody. Transportpróprio:Requestganhou$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.