Concepts
Webhooks
Receive lead.created, verify the HMAC signature, and handle retries.
Instead of polling for new enquiries, receive them: when a lead appears in the clinic's CRM, the platform sends a signed POST to your address, with retries. This page covers the event, the headers, signature verification and delivery.
Set it up
In the clinic's panel, open Ustawienia → Integracje → Webhook, enter your endpoint's address (Adres webhooka) and choose Wygeneruj sekret podpisu. The secret starts with pp_whsec_ and, like the API key, is shown once; generating a new one invalidates the old one.
The lead.created event
There is one event today: lead.created, sent for every new lead whatever its source (the clinic's site, the embed script, the API, a manual entry or an inbound message). The body is a frozen snapshot of the lead at the moment of the event: every delivery attempt sends the same bytes.
{
"event": "lead.created",
"lead": {
"id": "rS2bIhedVSStSQLc7DL_T",
"name": "Anna Kowalska",
"phone": "+48 600 100 200",
"email": "anna.kowalska@example.com",
"message": "Proszę o kontakt w sprawie higienizacji.",
"source": "api",
"createdAt": "2026-09-30T14:25:40.000Z"
}
}source is one of form, embed, api, manual or inbound. A field the lead does not have is null.
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
X-PP-Event | The event name, e.g. lead.created. |
X-PP-Timestamp | When this attempt was sent (Unix time, seconds). |
X-PP-Signature | sha256=<hex>, the HMAC described below. |
The body is frozen, but X-PP-Timestamp and X-PP-Signature are computed again for every attempt.
Verify the signature
The signature is an HMAC-SHA256 with your pp_whsec_ secret over the string timestamp + "." + body, where timestamp is the X-PP-Timestamp header and body the raw bytes of the request. Verify before you parse the JSON: parsing and serialising again can change the bytes.
import { createHmac, timingSafeEqual } from "node:crypto";
/** true only for a fresh, correctly signed delivery */
export function verifyWebhook(opts: {
secret: string; // pp_whsec_… from the panel
rawBody: string; // the request body exactly as received
timestamp: string; // X-PP-Timestamp
signature: string; // X-PP-Signature, e.g. "sha256=3f7a…"
}): boolean {
const age = Math.abs(Date.now() / 1000 - Number(opts.timestamp));
if (!Number.isFinite(age) || age > 300) return false; // replay protection: 5 minutes
const expected = createHmac("sha256", opts.secret)
.update(`${opts.timestamp}.${opts.rawBody}`)
.digest();
const received = Buffer.from(opts.signature.replace(/^sha256=/, ""), "hex");
return expected.length === received.length && timingSafeEqual(expected, received);
}<?php
// WordPress REST route or plain PHP: read the raw body before anything parses it
$raw = file_get_contents( 'php://input' );
$timestamp = $_SERVER['HTTP_X_PP_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_PP_SIGNATURE'] ?? '';
$secret = getenv( 'PP_WEBHOOK_SECRET' );
$fresh = abs( time() - (int) $timestamp ) <= 300;
$expected = 'sha256=' . hash_hmac( 'sha256', $timestamp . '.' . $raw, $secret );
if ( ! $fresh || ! hash_equals( $expected, $signature ) ) {
http_response_code( 401 );
exit;
}
$event = json_decode( $raw, true );- Compare signatures in constant time (
timingSafeEqual,hash_equals), never with===. - Reject old timestamps (5 minutes above) and log rejected requests: they point at a wrong secret or a spoofing attempt.
- A clinic that has not generated a secret yet gets deliveries signed with an older interim scheme that you cannot verify in advance. Generate the secret, then verify every delivery.
Delivery and retries
- Answer with a 2xx status within 10 seconds. A slower or different answer counts as a failed attempt.
- After a failed attempt the delivery is retried after 1, 5, 30, 120 and 720 minutes. When the retries run out, the delivery is marked failed and not sent again.
- Answer fast: store the raw request, reply 2xx, and process it afterwards.
- Be idempotent: when your 2xx gets lost on the way (a timeout), the same snapshot comes again. Deduplicate by
lead.id.
A webhook complements the API: after an event you can fetch more with the read endpoints.