Skip to content
Documentation
pozyskajpacjentaDocs

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.

POST to your address
{
  "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

HeaderValue
Content-Typeapplication/json
X-PP-EventThe event name, e.g. lead.created.
X-PP-TimestampWhen this attempt was sent (Unix time, seconds).
X-PP-Signaturesha256=<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.

TypeScript (Node)
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
<?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.