Skip to content

Webhooks

Webhooks let your team receive automatic real-time notifications on your own server when events happen in Pliic — such as a new suggestion submitted or a status change on a ticket.

Webhooks are available on the Starter plan (up to 3 endpoints) and Pro plan (unlimited). The Free plan does not include this feature. Only members with the Admin or Owner role can manage webhooks.

  1. Go to Settings → Webhooks
  2. Click Add endpoint
  3. Fill in the fields:
    • Name — an internal label for the endpoint
    • URL — a public HTTPS address that will receive the notifications
    • Events — select which events should trigger this endpoint
  4. Click Create

After creating, a signing secret (whsec_…) is shown only once. Copy and store it securely — it cannot be retrieved later.

If the secret is lost or compromised, open the endpoint menu and choose Rotate secret. Pliic shows the new secret once and the previous one stops working immediately — update your receiver before or right after rotating.

EventDescription
suggestion.createdA new suggestion was submitted.
suggestion.status_changedA suggestion’s status was changed.
suggestion.commentedA comment was added to a suggestion.
ticket.createdA new ticket was opened.
ticket.status_changedA ticket’s status was changed.
ticket.repliedA public reply was added to a ticket. Internal notes never trigger this event — they never appear anywhere outside your team’s dashboard.
survey.response.createdA user submitted a survey response.

Each delivery is a POST request with Content-Type: application/json in the following format:

{
"id": "018e1234-5678-7abc-def0-123456789abc",
"event": "suggestion.created",
"created_at": "2024-01-15T14:30:00Z",
"app": {
"id": 42,
"public_key": "pk_live_..."
},
"data": { ... }
}
FieldTypeDescription
idstring (UUID)Unique identifier for this delivery.
eventstringName of the event that fired.
created_atstring (ISO 8601)Date and time of the event in UTC.
appobjectThe app on your team that originated the event: id (internal identifier) and public_key (the same pk_live_... used by that app’s widget), so you can correlate the event with the right app when your team has more than one.
dataobjectEvent-specific payload data.

Every event’s data carries two identity objects, alongside the event-specific fields:

FieldDescription
authorThe owner of the record: whoever opened the ticket, created the suggestion, or answered the survey. Shape { external_id, name }. external_id is the id your own system assigned to that user when you created it.
senderWhoever caused this specific event. Shape { type, external_id, name }. type is "member" when someone on your team acted from the Pliic dashboard (external_id is null — members have no external id) or "app_user" when it was the end user themselves (external_id present).

In events triggered by the record’s own author (for example, the user creating their own suggestion), author and sender point to the same person. In events triggered by someone else (your team replying to a ticket, or changing a suggestion’s status from the board), they diverge.

Example of ticket.replied answered by your team:

{
"ticket_id": 123,
"ticket_number": "TKT-0042",
"message_id": 987,
"sender_type": "agent",
"author": { "external_id": "usr_42", "name": "Ana" },
"sender": { "type": "member", "external_id": null, "name": "Support Team" }
}

A common use case: notify the author when something changes on their own record, but skip the notification when they caused the change themselves (for example, the user replying to their own ticket). Compare the two external_ids:

if (data.author.external_id !== data.sender.external_id) {
notify(data.author);
}

In survey.response.created, the respondent is always the sender (there’s no “on behalf of” flow), so author and sender always match. The event also keeps the legacy app_user_id field alongside the new author.

Each delivery includes the following HTTP headers:

HeaderDescription
X-Pliic-SignatureSignature in the format t=<unix timestamp>,v1=<HMAC-SHA256>.
X-Pliic-EventEvent name (e.g. suggestion.created).
X-Pliic-DeliveryUnique UUID for this delivery.

Verifying the signature confirms that the request was sent by Pliic and that the payload was not tampered with in transit. The header looks like:

X-Pliic-Signature: t=1750000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t is the unix timestamp of the delivery and v1 is HMAC-SHA256(secret, "{t}.{raw_body}"). Checking the timestamp protects you from replayed payloads captured earlier.

PHP (official SDK):

use Pliic\Webhook;
use Pliic\Exceptions\SignatureVerificationException;
try {
$event = Webhook::constructEvent(
$request->getContent(),
$request->header('X-Pliic-Signature'),
$endpointSecret, // whsec_...
);
} catch (SignatureVerificationException $e) {
abort(400);
}
// $event->type, $event->data, $event->id

The SDK (composer require pliic/pliic-php) compares in constant time and rejects signatures older than 5 minutes.

Using Laravel? The SDK’s Laravel integration already registers this route for you, signature check included.

PHP (manual):

function verifySignature(string $secret, string $rawBody, string $header): bool
{
if (preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m) !== 1) {
return false;
}
[, $timestamp, $signature] = $m;
if (abs(time() - (int) $timestamp) > 300) {
return false;
}
$expected = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secret);
return hash_equals($expected, $signature);
}

Node.js:

const crypto = require('crypto');
function verifySignature(secret, rawBody, header) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header);
if (!m) return false;
const [, timestamp, signature] = m;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}

If your server returns a non-2xx status or the connection times out, Pliic will automatically retry with exponential backoff:

AttemptApproximate delay
1st (retry)5 minutes
2nd15 minutes
3rd45 minutes
4th135 minutes
5th405 minutes (~6.75 hours)

After 5 failed attempts the delivery is abandoned.

Redeliveries of the same event carry the same id in the body. If your processing is not naturally idempotent, store the ids you have already handled and skip duplicates.

The full history of each delivery — status, response code, and number of attempts — is available on the endpoint’s detail page.

After 5 attempts, if the delivery is still failing, the event is abandoned — there is no 6th attempt. To avoid relying on the webhook arriving at all, use the API as a safety net:

GET /api/v1/tickets?updated_since=2024-01-15T14:30:00Z
GET /api/v1/suggestions?updated_since=2024-01-15T14:30:00Z

The updated_since parameter (ISO 8601 timestamp) filters items updated at or after that instant and returns them sorted by updated_at ascending — the opposite of these endpoints’ default “most recent first” order, built specifically for sweeping through what changed in chronological order.

Recommended pattern: store the time of the last webhook you successfully processed. When you notice unusual silence, or a delivery marked as abandoned in the endpoint’s history, call both endpoints with updated_since=<that time> and reprocess whatever comes back, using the same idempotent handling you already apply to retries. Each suggestion in these responses also now carries the author object, in the same shape used in webhook events.

On the endpoint detail page, click Send test. Pliic will deliver a webhook.ping event so you can verify that your server is receiving requests correctly before activating real events.

  • Only public HTTPS URLs are accepted. Internal, private-network, or localhost addresses are rejected.
  • The TLS certificate of the destination server is always verified.
  • Never share the signing secret or include it in public code or logs.