Pular para o conteúdo

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.

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.

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.

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.

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'));

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 / enumOnde apareceDetalhe
status da sugestãoSuggestion.statuspending, under_review, planned, in_progress, done, declined
status do ticketTicket.statusopen, pending, resolved, closed
type do ticketTicket.typebug, feature_request, question, billing, other
priority do ticketTicket.prioritylow, normal, high, urgent
description vs bodyCriar sugestão vs ler sugestãoVocê 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_typeSuggestionComment.author_typeapp_user ou member. É o enum de quem escreveu um comentário de sugestão.
sender_typeTicketMessage.sender_typeuser 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_countSuggestion.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 ticketTicket.authorDesde 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ãoSuggestion.authorMesmo 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.

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 e
tickets de suporte dentro da minha própria interface, sem o widget embutido
do 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 um
HttpClientInterface fake) seguindo essas regras. Me avise se alguma delas
conflitar 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.