JavaScript/TypeScript SDK
@pliic/sdk is a typed, dependency-free client that wraps the Pliic widget API (/widget/*). It is for developers who want to build their own suggestions and tickets UI — a custom board, a feedback box with your own look, a support center embedded in your product — instead of embedding the prebuilt widget.
If the embedded widget already covers your needs (floating panel, feedback button), you don’t need the SDK. Use the SDK when you want full control over the UI and consume the data directly.
Installation
Section titled “Installation”Via <script> (hosted)
Section titled “Via <script> (hosted)”No build step, no npm — load the hosted script and use the PliicSDK global:
<script src="https://pliic.com/sdk/v1.js"></script><script> const { Pliic } = window.PliicSDK;</script>The global is named
PliicSDK, different from the embedded widget’sPliic— both can coexist on the same page without conflict.
Via npm
Section titled “Via npm”npm install @pliic/sdkimport { Pliic } from '@pliic/sdk';Authentication
Section titled “Authentication”Create the client with your app’s public key and, optionally, a user token:
const client = new Pliic({ appKey: 'pk_live_...', userToken: '<user jwt>', // optional baseUrl: 'https://pliic.com', // optional, default: page/script origin timeoutMs: 10000, // optional, default 10s});appKey(pk_live_...) identifies the app and team. Find it on the app’s Install tab.userTokenidentifies the end user and enables actions like voting. It’s a JWT generated on your server using the app’s secret key (sk_...) — the full generation flow is documented in User authentication. Without a token, SDK actions behave as anonymous (no voting).- The browser sends the
Originheader automatically with every request — your page’s domain must be in the app’s allowed domains (General tab), just like the embedded widget.
Initialization
Section titled “Initialization”const init = await client.init(); // { app, config, user }await client.ping();init() returns the app’s configuration and, if a valid userToken was provided, the identified user’s data. Use ping() to quickly check that credentials and domain are correct.
Suggestions
Section titled “Suggestions”// List (with search and status filter)const list = await client.suggestions.list({ search: 'dark mode', status: 'open' });
// Createconst created = await client.suggestions.create({ title: 'Dark mode', description: 'A dark theme would be great.',});// { id, title, status }
// Edit (author only, and only while no one else has engaged)await client.suggestions.update(created.id, { title: 'App-wide dark mode',});
// Delete (same author-only rule)await client.suggestions.delete(created.id);
// Voteawait client.suggestions.vote(created.id);
// Commentsconst comments = await client.suggestions.comments(created.id, { page: 1 });const comment = await client.suggestions.addComment(created.id, { body: 'I agree!' });
// Check duplicates before creatingconst { duplicates, has_duplicates } = await client.suggestions.checkDuplicates({ title: 'Dark mode',});Tickets
Section titled “Tickets”// Listconst tickets = await client.tickets.list({ page: 1 });
// Createconst ticket = await client.tickets.create({ subject: "Can't log in", body: 'I get a 500 error when signing in.', type: 'bug',});// { id, ticket_number, subject, status }
// Get a ticket (with messages)const { ticket: full, messages } = await client.tickets.get(ticket.id);
// Replyawait client.tickets.reply(ticket.id, { body: "I'm still having the issue." });
// Mark as resolved (only while the ticket is open or pending)await client.tickets.resolve(ticket.id);// { id, status: 'resolved', resolved_at }Replying to a resolved ticket reopens it automatically, no extra call needed. There’s no method to reopen or close (closed) a ticket through the SDK — those transitions stay with the team.
Surveys
Section titled “Surveys”The SDK only exposes the consumer side of surveys: fetch the survey currently active for the identified user, submit answers, and dismiss it. Creating and managing surveys lives in the app’s dashboard.
// Fetch the survey active for the current userconst survey = await client.surveys.active();// null when there's no eligible survey right now
if (survey) { // Submit answers await client.surveys.submit(survey.id, { answers: survey.questions.map((question) => ({ question_id: question.id, value: '5', })), });
// or dismiss it if the user closes it without answering await client.surveys.dismiss(survey.id);}Each question in survey.questions includes type (single_choice, rating, or text), label, options (for single_choice), rating_low_label/rating_high_label (for rating), placeholder, and is_required — enough to render the form without extra calls.
Error handling
Section titled “Error handling”Any non-2xx response throws PliicApiError, with status (HTTP code) and body (raw response body, when available):
import { Pliic, PliicApiError } from '@pliic/sdk';
try { await client.suggestions.create({ title: '' });} catch (error) { if (error instanceof PliicApiError) { console.error(error.status, error.message, error.body); }}Common errors:
- 401 — invalid
appKey, missing/expireduserToken, or domain not in the allowed domains list. - 422 — invalid validation data (e.g. empty title).
- 403 — calling
update/deleteon a suggestion that doesn’t belong to the identified user, or on one that’s no longer under the author’s control (it received a vote from someone else, a comment, an official response, or its status changed). See Editing or deleting your suggestion. The same code coverstickets.resolveon a ticket that doesn’t belong to the identified user or that’s no longeropen/pending. See Marking your ticket as resolved. - 429 or limit error — the app reached its plan limit (suggestions or tickets per month).
Requests that exceed the configured timeoutMs are also rejected as PliicApiError.
Next steps
Section titled “Next steps”- User authentication — how to generate the
userToken - Installing the widget — the no-code alternative
- Troubleshooting — domain and key errors also apply to the SDK