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.
Creating an endpoint
Section titled “Creating an endpoint”- Go to Settings → Webhooks
- Click Add endpoint
- 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
- 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.
Available events
Section titled “Available events”| Event | Description |
|---|---|
suggestion.created | A new suggestion was submitted. |
suggestion.status_changed | A suggestion’s status was changed. |
suggestion.commented | A comment was added to a suggestion. |
ticket.created | A new ticket was opened. |
ticket.status_changed | A ticket’s status was changed. |
ticket.replied | A public reply was added to a ticket. Internal notes never trigger this event — they never appear anywhere outside your team’s dashboard. |
survey.response.created | A user submitted a survey response. |
Payload format
Section titled “Payload format”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": { ... }}| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier for this delivery. |
event | string | Name of the event that fired. |
created_at | string (ISO 8601) | Date and time of the event in UTC. |
app | object | The 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. |
data | object | Event-specific payload data. |
Identifying who did what
Section titled “Identifying who did what”Every event’s data carries two identity objects, alongside the event-specific fields:
| Field | Description |
|---|---|
author | The 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. |
sender | Whoever 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.
Request headers
Section titled “Request headers”Each delivery includes the following HTTP headers:
| Header | Description |
|---|---|
X-Pliic-Signature | Signature in the format t=<unix timestamp>,v1=<HMAC-SHA256>. |
X-Pliic-Event | Event name (e.g. suggestion.created). |
X-Pliic-Delivery | Unique UUID for this delivery. |
Verifying the signature
Section titled “Verifying the signature”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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt 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->idThe 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), );}Delivery and retries
Section titled “Delivery and retries”If your server returns a non-2xx status or the connection times out, Pliic will automatically retry with exponential backoff:
| Attempt | Approximate delay |
|---|---|
| 1st (retry) | 5 minutes |
| 2nd | 15 minutes |
| 3rd | 45 minutes |
| 4th | 135 minutes |
| 5th | 405 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.
Recovering missed events
Section titled “Recovering missed events”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:00ZGET /api/v1/suggestions?updated_since=2024-01-15T14:30:00ZThe 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.
Testing your endpoint
Section titled “Testing your endpoint”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.
Security
Section titled “Security”- 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.