radardata / sdk-php
SDK oficial do RadarData para PHP e Laravel: consultas, Verify, Motor de Analises e webhooks.
Requires
- php: ^8.2
- ext-json: *
- guzzlehttp/guzzle: ^7.8
Requires (Dev)
- laravel/pint: ^1.18
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
SDK oficial do RadarData para PHP 8.2+, com
integração pronta para Laravel. Embrulha a API pública (/api/v2): consultas,
Verify (verificação de identidade), Motor de Análises, mensagens (SMS,
WhatsApp, Messenger e Instagram) e webhooks.
A API continua disponível para quem preferir chamá-la direto. Este pacote existe para você não precisar.
composer require radardata/sdk-php
Começando
use RadarData\RadarData; $radar = new RadarData('seu-token-de-api'); $saldo = $radar->catalogo()->saldo(); echo $saldo->emReais(); // 1.234,56 echo $saldo->disponivelCentavos; // 100000
O token é o token de API, criado no painel, e não o da sua sessão do
painel. Ele carrega escopos (consultas, verify, analises, leitura,
webhooks): uma chamada fora do escopo volta SemEscopo.
Laravel
O pacote se registra sozinho. Publique a config se quiser editá-la:
php artisan vendor:publish --tag=radardata-config
RADARDATA_TOKEN=seu-token RADARDATA_WEBHOOK_SECRET=o-segredo-que-assina-seus-webhooks # Só se você usa Verify solto, sem análise: cada fluxo tem o seu segredo. RADARDATA_VERIFY_WEBHOOK_SECRET=o-segredo-do-fluxo
use RadarData\Laravel\RadarDataFacade as RadarData; $sessao = RadarData::verify()->criarSessao('kyc-cnpj', ['cnpj' => '11222333000181']);
Seus proprios fluxos de analise
Da para montar o pipeline por codigo, em vez de desenha-lo no painel: voce
manda a definicao inteira num array e recebe um fluxo publicado, pronto para
rodar com analises()->criar().
$definicao = [ 'entrada' => ['placa' => ['tipo' => 'texto', 'obrigatorio' => true]], 'etapas' => [ ['chave' => 'nacional', 'tipo' => 'consulta', 'servico' => 'consulta-nacional', 'documento' => '{{entrada.placa}}', 'opcional' => true, 'paralelo' => true], ], 'regras' => [], 'saidas' => [['chave' => 'nacional', 'de' => 'nacional']], ]; // Quanto cada analise vai custar, sem gravar nada. echo $radar->fluxos()->validar($definicao)['preco']; $radar->fluxos()->publicar('laudo', 'Laudo', 'VEICULO', $definicao); // E a partir daqui e um produto como outro qualquer. $radar->analises()->criar('laudo', ['placa' => 'ABC1D23']);
Duas coisas que valem saber antes:
- O preco nao vai no pedido. Ele e calculado pelas bases que o fluxo
consulta, com a tabela da sua conta, e cobra o caminho mais caro. Chame
validar()antes de publicar para ver o numero. - Escopo
fluxos, separado deanalises. Publicar fluxo escolhe o que e consultado e que regra reprova alguem. Use um token de provisionamento aqui, e deixe o token do dia a dia so comanalises.
Publicar e idempotente pelo conteudo: mandar a mesma definicao de novo nao cria versao nova, entao da para chamar isto a cada deploy.
Consultas
$resultado = $radar->consultas()->executar('cpf', '12345678909');
Gasta saldo. O retorno vem como array porque cada serviço devolve um corpo diferente e a lista cresce: tipar tudo aqui faria o SDK desatualizar a cada serviço novo sem ninguém perceber.
Consulta demorada responde que está em andamento e avisa pelo webhook
consulta.concluida. O resultado sai pelo consulta_id:
$r = $radar->consultas()->resultado($consultaId); // $r['meta']['status']: em_andamento | concluida | erro | expirada; o dado em $r['data']
Erro terminal chega aqui como resposta, em $r['error'], e não como exceção.
Documentos do processo
processo-documentos-publicos e processo-autos são consultas demoradas. O
resultado traz documentos, cada um com um id para baixar. O download não
cobra: o valor foi o da consulta.
$r = $radar->consultas()->resultado($consultaId); foreach ($r['data']['documentos'] as $d) { $arquivo = $radar->consultas()->baixarDocumento($d['id']); // conteudo, tipo, nome file_put_contents($arquivo['nome'], $arquivo['conteudo']); }
Os autos pedem o certificado digital da sua empresa no campo certificado:
$radar->consultas()->executar('processo-autos', '0018063-19.2013.8.26.0002', ['certificado' => $certificadoId]);
Certificado digital
Escopo certificados, que só o administrador da conta concede, e só com token
de produção. O arquivo e a senha não voltam em nenhuma resposta, e cada
certificado serve só à sua empresa.
$c = $radar->certificados()->cadastrar('/caminho/escritorio.pfx', getenv('SENHA_DO_PFX'), 'Escritório SP', [ // Só para tribunais que pedem código do aplicativo autenticador. ['tribunal' => 'TRF2', 'sistemas' => ['EPROC-TRF2'], 'secret_2fa' => getenv('SEGREDO_2FA')], ]); $certificadoId = $c['id']; $radar->certificados()->listar(); $radar->certificados()->remover($certificadoId);
Mandar o mesmo arquivo de novo devolve o certificado que já existe. O envio não é repetido sozinho em erro.
Para saber o que a sua conta pode consultar, e por quanto:
foreach ($radar->catalogo()->servicos() as $s) { echo "{$s->slug}: {$s->nome} ({$s->precoCentavos} centavos)\n"; }
Verify
$sessao = $radar->verify()->criarSessao( fluxo: 'kyc-cnpj', // uuid do fluxo ou o alias dele input: ['cnpj' => '11222333000181'], opcoes: ['reference_id' => 'pedido-4471', 'redirect_url' => 'https://seusite.com/ok'], ); // Mande a pessoa para cá: echo $sessao->url;
O resultado chega por webhook. Dá para consultar também:
$sessao = $radar->verify()->sessao($sessao->id); if ($sessao->concluida() && $sessao->aprovada()) { echo $sessao->valorCentavos; // quanto a sessão custou para você }
custoCentavos continua existindo com o mesmo número, mas está depreciado: use
valorCentavos.
Criar sessão não é idempotente. Se der timeout, não chame de novo às cegas: você pagaria duas sessões. Mande um
reference_idseu e procure por ele antes de criar outra.
Análises
Assíncrono: criar devolve o id, o resultado chega por webhook.
$analise = $radar->analises()->criar( 'kyc-pf', ['cpf' => '12345678909'], chaveIdempotencia: 'cadastro-90321', // um id seu: repetir não cobra de novo ); $estado = $radar->analises()->mostrar($analise['id']);
Documentos
Produtos que leem documentos recebem o arquivo antes da análise. Você envia,
guarda o id e manda na entrada:
$identidade = $radar->documentos()->enviar('/tmp/rg.jpg', 'identidade'); $contrato = $radar->documentos()->enviar($recursoAberto, 'contrato_social', 'contrato.pdf'); $radar->analises()->criar('abertura-de-conta', [ 'cpf' => '52998224725', 'cnpj' => '55016784000138', 'documentos' => [ ['id' => $identidade['id'], 'tipo' => 'identidade'], ['id' => $contrato['id'], 'tipo' => 'contrato_social'], ], ]);
JPEG, PNG ou PDF, até 10 MB. Os tipos estão em Documentos::TIPOS. Documento
que não entra em análise nenhuma expira em 24 horas. O envio não se repete
sozinho: a API não tem idempotência ali.
Quando a decisão é sua
Quando a análise para esperando o seu time (pendencia.tipo = manual no
evento analise.pendente), responda:
$r = $radar->analises()->decidir( $id, 'aprovar', // ou 'recusar', que exige o motivo motivo: 'Confirmado com o titular.', responsavel: 'ana@sualoja.com.br', // alguns produtos exigem ); // Se o próximo passo é a prova de vida, o link já vem aqui: $link = $r['pendencia']['url'] ?? null;
Até a 0.3.0 este método mandava
decisaoe a API recusava toda chamada com 422. A partir da 0.4.0 mandaacao, que é o contrato da API.
Mensagens
SMS, WhatsApp, Messenger e Instagram saem pelo mesmo método, mudando o canal.
SMS
Escopo mensagens, que não vem ligado por padrão: marque ao criar o token.
Cobra por mensagem aceita; o que falha volta para o saldo.
$r = $radar->mensagens()->enviar( '+5511999999999', 'Seu código de acesso é 482913.', 'pedido-48213-codigo', // repetir com a mesma chave não manda de novo ); $r['mensagem']['status']; // "aceita": saiu para a operadora $r['meta']['saldo']; // em centavos
- Até 160 caracteres. Acento sai antes do envio e emoji recusa a
mensagem (
EntradaInvalida,codigo=texto_invalido), sem cobrar. aceitanão é entrega confirmada: para código de acesso, ofereça reenviar.envio_incerto(ErroDoServidor, 504): não confirmamos se saiu e não foi cobrado. Reenviar com chave nova pode fazer a pessoa receber duas.limite_destinoelimite_diario(MuitasTentativas): 5 por número por hora e 1.000 por conta por dia.- O aviso chega pelos webhooks
mensagem.aceitaemensagem.falhou. - A resposta se perdeu? O erro traz a chave usada em
$e->chaveIdempotencia. Repita com ela:Conflito(idempotency_em_processamento) quer dizer que a primeira ainda está saindo; espere e repita com a mesma chave. Chave nova pode fazer a pessoa receber duas. Para código de acesso, passe sempre uma chave sua.
$pagina = $radar->mensagens()->listar(1, 25); // sem o texto $uma = $radar->mensagens()->obter($id); // com o texto, por 30 dias
O mesmo método, com o canal whatsapp. O número da empresa é conectado no painel,
pelo cadastro da própria Meta, e o envio sai pelo principal ou pelo que você indicar.
// Responder dentro de 24 horas da última mensagem da pessoa: texto livre, sem cobrança. $radar->mensagens()->enviar('+5511999999999', 'Seu pedido saiu para entrega.', canal: 'whatsapp'); // Fora da janela, só modelo aprovado na Meta. É o que é cobrado. $radar->mensagens()->enviar('+5511999999999', null, 'pedido-48213-aviso', 'whatsapp', modelo: ['nome' => 'pedido_enviado', 'idioma' => 'pt_BR', 'parametros' => ['Maria', '48213']], ); $modelos = $radar->mensagens()->modelos(); // os do número principal
- Texto livre até 4096 caracteres, com emoji e acento.
- Fora da janela sem modelo:
EntradaInvalidacomcodigo=fora_da_janela. - Sem número conectado:
Conflitocomcanal_nao_conectado. - A Meta cobra a conversa direto na conta de WhatsApp do cliente.
- Eventos:
mensagem.recebida(comtexto,deenomede quem escreveu, para você responder),mensagem.entregue,mensagem.lidaemensagem.falhou. O conteúdo some da Radar Data em 30 dias.
Messenger e Instagram
O mesmo método, com o canal messenger ou instagram. A página do Facebook e o
perfil profissional do Instagram são conectados no painel, pelo login da Meta. Aqui
só se responde: a conversa começa quando a pessoa escreve para a página ou para
o perfil, e a mensagem dela chega no webhook mensagem.recebida.
Para responder, o de da mensagem recebida (o id da pessoa naquele canal) vai no
$para, e o conexao (a página ou o perfil que recebeu) vai no de:
use RadarData\RadarData; use RadarData\Recursos\Mensagens; use RadarData\Webhooks\Assinatura; Route::post('/radardata/webhook', function (Request $request, RadarData $radar) { $evento = Assinatura::exigir($request->getContent(), $request->header('X-RadarData-Signature'), config('radardata.webhook_secret')); $recebida = $evento['mensagem'] ?? []; if (($evento['event'] ?? null) === 'mensagem.recebida' && in_array($recebida['canal'] ?? null, ['messenger', 'instagram'], true)) { $radar->mensagens()->enviar( $recebida['de'], 'Oi! Já vamos te atender.', 'resposta-'.$recebida['id'], // reenvio do webhook não responde duas vezes canal: $recebida['canal'], de: $recebida['conexao'], ); } return response()->noContent(); });
- Janela de 24 horas: só dá para responder até 24 horas depois da última
mensagem da pessoa. Fora dela,
EntradaInvalidacomcodigo=fora_da_janela. Não há modelo como no WhatsApp: quem precisa voltar a falar é a pessoa. - Atendimento humano, até 7 dias: quando quem responde é uma pessoa da sua
equipe, passe a etiqueta
atendimento_humanoe a janela vai a 7 dias. É regra da Meta: não use para mensagem automática.
$radar->mensagens()->enviar($recebida['de'], 'Conferi o seu pedido: sai amanhã.', canal: Mensagens::CANAL_MESSENGER, de: $recebida['conexao'], etiqueta: Mensagens::ETIQUETA_ATENDIMENTO_HUMANO, );
- Texto até 2.000 caracteres no Messenger e 1.000 no Instagram. Não há
modelo. - Cobrança: mensalidade por página ou perfil conectado, a cada 30 dias, debitada
do saldo. A mensagem não é cobrada (
preco= 0). - Mensalidade que não renovou por falta de saldo:
SaldoInsuficientecomcodigo=mensalidade_em_aberto, e o webhookcanal.suspensoavisa na hora (com aconexao). A cobrança é refeita sozinha depois da recarga. - Sem página ou perfil conectado:
Conflitocomcanal_nao_conectado; acesso revogado na Meta:Conflitocomcanal_sem_acesso. - Na mensagem recebida,
deé o id da pessoa enomevemnull. Nas enviadas,paravem mascarado (***1234) econexaodiz por qual página ou perfil saiu. - Eventos:
mensagem.recebida,mensagem.entregue(só Messenger),mensagem.lida,mensagem.falhouecanal.suspenso.
Eventos
O seu sistema conta o que aconteceu e a configuração do lado da RadarData decide que análises acordam. Evento sem gatilho configurado não é erro.
$r = $radar->eventos()->enviar('pedido.criado', 'pedido-'.$pedido->id, ['valor' => 15000], segredo: env('RADARDATA_EVENTOS_SECRET'), // quando a origem exige assinatura );
Operações (COAF)
Para factoring: cada operação registrada é conferida, e a que precisa de olhar
humano volta com revisar.
$op = $radar->operacoes()->registrar( ['cliente_documento' => '11222333000181', 'sacado_documento' => '52998224725', 'valor' => 150000], 'operacao-'.$titulo, // repetir não cobra de novo ); $pendentes = $radar->operacoes()->listar(revisar: true); $radar->operacoes()->decidir($op['id'], 'ARQUIVAR', 'Operação conferida com o cliente.');
Monitoramentos judiciais
Cobrança adiantada a cada 30 dias. A novidade chega pelo webhook
monitoramento_judicial.evento, com o conteúdo, que some da RadarData em 30 dias.
$m = $radar->monitoramentosJudiciais()->criar('monitoramento-documento', ['documento' => '12345678909', 'nome' => 'Fornecedor principal']); $eventos = $radar->monitoramentosJudiciais()->eventos($m['id']); // ['data' => [...], 'meta' => ['paginas' => ...]] $radar->monitoramentosJudiciais()->cancelar($m['id']); // vale até o fim do período pago
O aviso de diário oficial traz conteudo.pagina_pdf quando o diário informa a
página:
if ($evento['conteudo']['pagina_pdf'] ?? null) { $pdf = $radar->monitoramentosJudiciais()->paginaDoDiario($evento['conteudo']['pagina_pdf']); }
No catálogo, $servico->monitoramento diz quais serviços se usam assim, e
$servico->rota, onde.
Webhooks: conferir que o aviso é nosso
use RadarData\Webhooks\Assinatura; Route::post('/radardata/webhook', function (Request $request) { $evento = Assinatura::exigir( $request->getContent(), // o corpo CRU $request->header('X-RadarData-Signature'), config('radardata.webhook_secret'), ); // Deduplique: reenviamos em caso de falha. $entrega = $request->header('X-RadarData-Delivery'); return response()->noContent(); });
Três coisas que costumam dar errado aqui, e valem a leitura:
Use o corpo cru. $request->getContent(), não o array decodificado.
Reserializar muda espaços e ordem das chaves, o HMAC muda junto e a conferência
falha sem motivo aparente. É o erro mais comum de qualquer integração deste
tipo, e há um teste neste repositório que o demonstra.
Deduplique pelo X-RadarData-Delivery. Reenviamos quando a entrega falha,
então o mesmo evento pode chegar duas vezes.
A assinatura prova origem e conteúdo, não a hora. O timestamp e o id de entrega viajam em cabeçalho, fora do corpo assinado, então ela não protege contra o reenvio de um aviso antigo e autêntico por quem o tenha interceptado. A dedup acima é a defesa prática.
Erros
Cada situação tem a sua classe, para você não comparar strings:
| Classe | Quando |
|---|---|
NaoAutenticado |
401, token ausente, inválido ou revogado |
SaldoInsuficiente |
402, a conta não tem saldo para a operação, ou a mensalidade da página ou do perfil está em aberto (mensalidade_em_aberto) |
SemEscopo |
403, o token não tem o escopo daquela rota |
NaoEncontrado |
404 |
Conflito |
409, a mesma chave ainda em andamento ou usada em outro pedido |
EntradaInvalida |
422, veja ->corpo para os campos |
MuitasTentativas |
429 |
ErroDoServidor |
5xx, falha nossa |
FalhaDeRede |
sem resposta: timeout, DNS, conexão recusada |
Todas estendem ErroRadarData, que traz ->codigo, ->status e ->corpo.
FalhaDeRede é separado de propósito: ali o SDK não sabe se a operação
aconteceu. Numa chamada que gasta saldo isso importa, e o caminho é consultar
pela mesma chave de idempotência, não repetir às cegas.
Repetição automática, e por que ela não é global
O SDK repete em 429 e 5xx somente nas chamadas em que manda
Idempotency-Key: consultas, análises, mensagens, eventos e operações. A mesma
chave faz a API devolver a resposta anterior em vez de cobrar de novo. Não
repete o que a mesma chave devolveria igual (limite_destino, limite_diario,
teto_excedido, envio_incerto, envio_recusado), e todo erro traz a chave
usada em $e->chaveIdempotencia.
Criar sessão de Verify não tem idempotência na API, então o SDK não repete ali, qualquer que seja a configuração. Repetir criaria uma segunda sessão faturável, e um SDK que cobra duas vezes do seu cliente é pior do que um SDK que devolve erro.
A chave é gerada automaticamente quando você não passa uma. Guarde a sua se quiser poder reconsultar o resultado depois de um timeout:
$chave = \RadarData\Recursos\Consultas::chave(); $radar->consultas()->executar('cpf', '12345678909', chaveIdempotencia: $chave);
Apontando para outro ambiente
use RadarData\{Configuracao, RadarData}; $radar = new RadarData(new Configuracao( token: 'seu-token', base: 'https://api.radardata.dev.local/api/v2', timeout: 10, ));
Desenvolvimento
composer install
vendor/bin/phpunit
vendor/bin/pint
# contrato contra uma api de verdade, com token de SANDBOX
RADARDATA_CONTRATO_BASE=http://127.0.0.1:8000/api/v2 RADARDATA_CONTRATO_TOKEN=... vendor/bin/phpunit tests/ContratoTest.php
Licença
MIT.