Skip to content

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.

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’s Pliic — both can coexist on the same page without conflict.

Terminal window
npm install @pliic/sdk
import { Pliic } from '@pliic/sdk';

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.
  • userToken identifies 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 Origin header automatically with every request — your page’s domain must be in the app’s allowed domains (General tab), just like the embedded widget.
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.

// List (with search and status filter)
const list = await client.suggestions.list({ search: 'dark mode', status: 'open' });
// Create
const 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);
// Vote
await client.suggestions.vote(created.id);
// Comments
const comments = await client.suggestions.comments(created.id, { page: 1 });
const comment = await client.suggestions.addComment(created.id, { body: 'I agree!' });
// Check duplicates before creating
const { duplicates, has_duplicates } = await client.suggestions.checkDuplicates({
title: 'Dark mode',
});
// List
const tickets = await client.tickets.list({ page: 1 });
// Create
const 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);
// Reply
await 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.

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 user
const 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.

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/expired userToken, or domain not in the allowed domains list.
  • 422 — invalid validation data (e.g. empty title).
  • 403 — calling update/delete on 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 covers tickets.resolve on a ticket that doesn’t belong to the identified user or that’s no longer open/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.