Construa seu helper nativo com o SDK PHP
Este guia é a receita de ponta a ponta pra quem decidiu integrar o Pliic nativamente, sem o widget embutido. Ele nasceu da primeira integração real feita assim (o helper de ajuda do próprio time do Pliic) e reúne os problemas que apareceram nesse processo. Se você só precisa da referência rápida de cada chamada do SDK, veja SDK PHP; aqui o foco é o fluxo completo e as decisões que evitam bug de segurança e dado vazado entre usuários.
O que você vai construir
Seção intitulada “O que você vai construir”Um painel de ajuda dentro do seu próprio produto: o usuário abre sugestões, vota, comenta e abre tickets de suporte sem sair do seu app e sem ver nenhuma marca do Pliic. Por trás, o seu backend fala com a API do Pliic pelo SDK PHP. Do ponto de vista do seu usuário, é só mais uma tela do seu sistema.
A regra de ouro: a chave secreta nunca vai pro browser
Seção intitulada “A regra de ouro: a chave secreta nunca vai pro browser”A chave sk_live_... dá acesso total à API do seu app. Ela só pode existir no seu servidor. Isso significa que o seu frontend nunca fala direto com a API do Pliic: ele fala com rotas do seu próprio backend, e é o seu backend que chama o SDK.
Na prática, isso vira um proxy fino. Um grupo de rotas autenticadas pela sessão do seu usuário, com controllers que só traduzem a requisição em uma chamada do SDK:
// Todas as rotas abaixo já passaram pela autenticação do SEU app// (o middleware de sessão de sempre) e por um throttle próprio,// separado do rate limit da API do Pliic.Route::middleware(['auth', 'throttle:60,1']) ->prefix('help') ->group(function () { Route::get('suggestions', [HelpSuggestionsController::class, 'index']); Route::post('suggestions', [HelpSuggestionsController::class, 'store']); Route::post('suggestions/{id}/vote', [HelpSuggestionsController::class, 'vote']); Route::get('tickets/{id}', [HelpTicketsController::class, 'show']); Route::post('tickets/{id}/replies', [HelpTicketsController::class, 'reply']); });class HelpSuggestionsController{ public function __construct(private PliicClient $pliic) {}
public function index(Request $request): JsonResponse { $user = $request->user(); // o usuário autenticado no SEU sistema
$result = $this->pliic->suggestions->list([ 'status' => $request->query('status'), 'search' => $request->query('search'), 'user_id' => (string) $user->id, 'user_email' => $user->email, ]);
return response()->json($result); }}O controller não recebe sk_live_... do cliente, não repassa a chave em nenhum lugar visível e não deixa o usuário escolher de quem ele está agindo em nome. Quem é o usuário vem sempre da sessão autenticada do seu app, nunca de um campo do formulário.
Identidade do usuário
Seção intitulada “Identidade do usuário”Toda escrita no SDK (create, vote, addComment, reply) aceita um user com a identidade da pessoa no seu sistema:
$user = [ 'id' => (string) $request->user()->id, 'name' => $request->user()->name, 'email' => $request->user()->email,];
$this->pliic->suggestions->create([ 'user' => $user, 'title' => $request->input('title'), 'description' => $request->input('description'),]);O Pliic cria ou reaproveita o app user correspondente sozinho. Aqui tem duas armadilhas que só aparecem quando o helper já está em produção com usuários de verdade.
A identidade do Pliic é email-first. Se dois dos seus usuários, em ambientes diferentes ou por qualquer coincidência, tiverem o mesmo id mas emails diferentes, o Pliic não tem como saber que são pessoas diferentes só pelo id. Por isso, em toda leitura que carrega contexto de usuário (suggestions->list, suggestions->get, tickets->list, tickets->get), envie user_id e user_email juntos sempre que o seu usuário tiver um e-mail. Mandar só user_id funciona, mas é a rota mais frágil: qualquer colisão de id vira dado de outra pessoa aparecendo pro usuário errado.
Use um app separado por ambiente. Um app de staging apontando pra chave de produção (ou vice-versa) é o jeito mais comum de poluir os números reais com dado de teste, e o inverso, dado de produção aparecendo em ambiente de dev, é pior ainda. Crie um app dedicado (ou pelo menos uma chave de sandbox) por ambiente e trate a variável de ambiente da chave como qualquer outro segredo por ambiente.
Posse de ticket
Seção intitulada “Posse de ticket”Buscar um ticket pelo id sem contexto de usuário devolve o ticket, seja ele de quem for. Isso é intencional no SDK, porque a chamada existe pra sua equipe conseguir olhar qualquer ticket do app; mas dentro do helper, onde é o próprio usuário final olhando a tela, você precisa restringir a consulta ao dono:
public function show(Request $request, int $id): JsonResponse{ $user = $request->user();
try { $ticket = $this->pliic->tickets->get($id, [ 'user_id' => (string) $user->id, 'user_email' => $user->email, ]); } catch (NotFoundException) { abort(404); }
return response()->json($ticket);}Desde a versão 1.0.1 do SDK, passar user_id/user_email em tickets->get() faz a API responder 404 quando o ticket pertence a outra pessoa, exatamente o mesmo status de “não existe”. Trate os dois casos do mesmo jeito no seu catch: não dá pra diferenciar “não existe” de “não é seu” sem abrir uma brecha pro seu usuário descobrir, por tentativa e erro, quais ids de ticket existem no sistema de outra pessoa.
Quem esquece desse parâmetro e confia só no id da URL entrega qualquer ticket do app pra qualquer usuário autenticado no seu helper. É o vazamento mais fácil de introduzir nessa integração e o mais fácil de não perceber em teste manual, porque o seu próprio usuário de teste raramente tenta acessar o ticket de outra pessoa.
Erros com elegância
Seção intitulada “Erros com elegância”Toda chamada do SDK que falhar lança uma exceção tipada, todas filhas de Pliic\Exceptions\ApiErrorException. Mapeie pra respostas amigáveis no seu proxy em vez de deixar a exceção estourar como erro genérico:
use Pliic\Exceptions\NotFoundException;use Pliic\Exceptions\ValidationException;use Pliic\Exceptions\RateLimitException;use Pliic\Exceptions\ApiErrorException;
try { $result = $this->pliic->suggestions->create($payload);} catch (ValidationException $e) { // 422: erro de validação, os campos vêm em $e->errors() return response()->json(['errors' => $e->errors()], 422);} catch (NotFoundException $e) { // 404: sugestão/ticket não existe (ou não é do usuário, no caso de tickets->get) abort(404);} catch (RateLimitException $e) { // 429: limite de chamadas por hora do plano estourado return response()->json(['message' => 'Muitas requisições, tente novamente em instantes.'], 429);} catch (ApiErrorException $e) { // pega o resto: 401/403 de configuração da chave, e qualquer 5xx // (incluindo 503 de manutenção do Pliic) cai aqui, porque o SDK // não tem uma exceção dedicada pra status de servidor report($e);
return response()->json(['message' => 'Não foi possível completar a ação agora.'], 502);}Uma segunda camada de elegância: esconda a própria UI do helper quando a chave não está configurada, em vez de deixar o usuário cair numa tela quebrada. Um flag simples baseado na presença da variável de ambiente resolve:
// Em algum lugar compartilhado (um Gate, um provider de props, etc.)$helperEnabled = filled(config('services.pliic.secret'));Shapes e enums
Seção intitulada “Shapes e enums”Esses são os campos e enums que mais confundem quem está lendo a resposta da API pela primeira vez. Todos vêm direto da especificação OpenAPI do Pliic (/api/v1/openapi.json).
| Campo / enum | Onde aparece | Detalhe |
|---|---|---|
status da sugestão | Suggestion.status | pending, under_review, planned, in_progress, done, declined |
status do ticket | Ticket.status | open, pending, resolved, closed |
type do ticket | Ticket.type | bug, feature_request, question, billing, other |
priority do ticket | Ticket.priority | low, normal, high, urgent |
description vs body | Criar sugestão vs ler sugestão | Você envia description em suggestions->create(), mas a resposta (e qualquer leitura posterior) devolve o mesmo texto no campo body. Nomes diferentes pra mesma informação, dependendo se você está escrevendo ou lendo. |
author_type | SuggestionComment.author_type | app_user ou member. É o enum de quem escreveu um comentário de sugestão. |
sender_type | TicketMessage.sender_type | user ou agent. É o enum equivalente pra quem escreveu uma mensagem de ticket, com nome e valores diferentes de author_type. Não misture os dois ao renderizar o autor de um item. |
vote_count vs votes_count | Suggestion.vote_count vs a resposta de suggestions->vote() | O total de votos de uma sugestão vem como vote_count quando você lista ou busca a sugestão, mas a resposta imediata de vote() traz o mesmo número em votes_count. Se o seu frontend atualiza o contador otimisticamente, confira qual dos dois campos você está lendo. |
author do ticket | Ticket.author | Desde a v1.0.1, todo ticket traz { external_id, name } do autor. Use isso pra exibir “aberto por Fulano” na sua UI, mas não use como substituto do parâmetro user_id/user_email de tickets->get(): o author é só exibição, quem garante a posse é o parâmetro na chamada. |
author da sugestão | Suggestion.author | Mesmo shape { external_id, name } do Ticket.author, também só pra exibição (“sugerido por Fulano”). Não filtra nem restringe nada sozinho: quem faz isso continua sendo o user_id/user_email que você já envia em suggestions->list()/suggestions->get(). |
Enquanto não existe um fake oficial do Pliic, teste o helper injetando um HttpClientInterface fake no PliicClient:
use Pliic\HttpClient\ApiResponse;use Pliic\HttpClient\HttpClientInterface;
class FakePliicHttpClient implements HttpClientInterface{ public function __construct(private array $responses) {}
public function request(string $method, string $url, array $headers, ?string $body = null): ApiResponse { // devolve a resposta programada pro path chamado nesse teste, // sem bater na rede de verdade [$status, $payload] = $this->responses[$method.' '.$url] ?? [200, []];
return new ApiResponse($status, json_encode($payload)); }}
$pliic = new PliicClient('sk_live_test', 'https://pliic.com', new FakePliicHttpClient([ 'GET https://pliic.com/api/v1/tickets/7' => [404, ['message' => 'Not found']],]));Um fake oficial (Pliic\Testing) está a caminho para eliminar essa parte artesanal (acompanhe pela issue #191); assim que ele sair, este guia ganha uma seção própria com o pacote pronto.
Construindo com IA? Copie este prompt
Seção intitulada “Construindo com IA? Copie este prompt”O prompt abaixo encoda a receita inteira deste guia. Cole no Claude Code, Cursor ou equivalente, ajuste os trechos entre colchetes e você deve sair com um helper que já nasce sem os erros mais comuns dessa integração.
Quero construir um painel de ajuda nativo no meu app [NOME DO APP/FRAMEWORK],usando o SDK oficial PHP do Pliic (pliic/pliic-php) para expor sugestões etickets de suporte dentro da minha própria interface, sem o widget embutidodo Pliic.
Regras obrigatórias da integração:
1. A chave secreta do Pliic (sk_live_...) fica só no meu backend, nunca no frontend. Todo acesso passa por rotas de proxy no meu próprio app (ex.: /help/*), autenticadas pela sessão do meu usuário, com controllers finos que só traduzem a requisição em uma chamada do SDK.
2. Em toda escrita (suggestions->create, suggestions->vote, suggestions->addComment, tickets->create, tickets->reply), monte o objeto `user` a partir do usuário autenticado na MINHA sessão (id, name, email), nunca a partir de um campo enviado pelo cliente.
3. A identidade do Pliic é email-first. Em toda LEITURA com contexto de usuário (suggestions->list, suggestions->get, tickets->list, tickets->get), envie user_id E user_email juntos sempre que o usuário tiver e-mail. Nunca confie só em user_id: colisão de id entre ambientes é um problema real.
4. Use um app do Pliic (ou pelo menos uma chave) separado por ambiente: dev/staging nunca deve escrever no app de produção.
5. Ao buscar um ticket específico (tickets->get), sempre passe user_id/user_email do usuário autenticado. Desde a v1.0.1 do SDK, isso faz a API responder 404 quando o ticket é de outra pessoa. Trate esse 404 exatamente como "não existe" no meu proxy: nunca deixe o usuário diferenciar "não existe" de "não é seu".
6. Mapeie as exceções do SDK (todas filhas de Pliic\Exceptions\ApiErrorException) para respostas amigáveis: ValidationException (422, use $e->errors()), NotFoundException (404), RateLimitException (429), e um catch genérico de ApiErrorException para o resto (401/403 de configuração, e qualquer 5xx, já que o SDK não tem exceção dedicada para status de servidor).
7. Esconda a UI do helper (ou desative a rota) quando a chave do Pliic não estiver configurada no ambiente, em vez de deixar o usuário cair numa tela quebrada.
8. Use os nomes de campo corretos da API (derive da especificação OpenAPI em /api/v1/openapi.json, não invente): - Suggestion.status: pending, under_review, planned, in_progress, done, declined - Ticket.status: open, pending, resolved, closed - Ticket.type: bug, feature_request, question, billing, other - Ticket.priority: low, normal, high, urgent - Ao CRIAR uma sugestão o campo é `description`, mas ao LER a sugestão de volta o mesmo texto vem no campo `body`. - SuggestionComment usa `author_type` (app_user/member); TicketMessage usa `sender_type` (user/agent). Não são o mesmo enum, não misture. - Suggestion.vote_count é o total; a resposta de suggestions->vote() traz o mesmo número em votes_count (nome diferente). - Ticket.author ({ external_id, name }) é só para exibição, não substitui o parâmetro de posse em tickets->get().
Monte as rotas, os controllers, o tratamento de erro e os testes (com umHttpClientInterface fake) seguindo essas regras. Me avise se alguma delasconflitar com a estrutura do meu projeto antes de gerar o código.Um MCP oficial do Pliic está no roadmap como a próxima evolução dessa experiência: em vez de colar este prompt, a ideia é que o próprio assistente de IA consulte o Pliic diretamente pelas ferramentas do MCP. Até lá, este prompt e o guia acima são o caminho mais curto pra um helper nativo correto.