PlanDocket
Intermediate 15 min

Setting Up Webhooks

Configure real-time notifications for events.

Prerequisites

  • An API key with the required scopes
  • An active PlanDocket account
1

Create a Webhook Endpoint

Create an endpoint on your server that can receive POST requests.

2

Register the Webhook

Register your webhook endpoint via the API.

curl -X POST "https://your-tenant.plandocket.com/api/v1/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhook",
    "events": ["participant.added", "invoice.paid"]
  }'
3

Validate the Signature

Validate the webhook signature to ensure authenticity.

<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$secret = 'your_webhook_secret';

// Signature format: v1=<hmac>
if (!str_starts_with($signature, 'v1=')) {
    http_response_code(401);
    exit('Invalid signature format');
}

// Verify timestamp is recent (max 5 minutes)
if (!is_numeric($timestamp) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit('Timestamp expired');
}

$signedPayload = $timestamp . '.' . $payload;
$expected = hash_hmac('sha256', $signedPayload, $secret);

if (!hash_equals($expected, substr($signature, 3))) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($payload, true);
4

Read the Message

Every delivery is a JSON envelope: "id" (unique per event, identical on every retry), "event" (e.g. "invoice.paid"), "api_version", "created_at" (ISO 8601, UTC) and "data" (the object that changed). The headers X-Webhook-Id (= id, use it to ignore duplicates), X-Webhook-Delivery, X-Webhook-Attempt, X-Webhook-Timestamp and X-Webhook-Signature are sent as well. Payloads never contain passwords, tokens or file contents; ignore fields you do not know, new fields can be added at any time.

{
    "id": "6f1c2b0e-8a43-4d4e-9b8e-2f0c7d5a9e11",
    "event": "invoice.paid",
    "api_version": "2026-10",
    "created_at": "2027-04-12T08:15:30+00:00",
    "timestamp": "2027-04-12T08:15:30+00:00",
    "data": {
        "status": "paid",
        "open_amount": 0,
        "invoice_id": 311,
        "invoice_number": "RE-2027-000311",
        "contact_id": 42,
        "total": 2380,
        "currency": "EUR",
        "due_date": "2027-04-30",
        "source": "api"
    }
}
5

Answer Quickly and Handle Retries

Answer with a 2xx status within the timeout (default 30 seconds) and do the actual work afterwards. Any other answer is retried with increasing delays; "retry_count" is the number of retries after the first attempt. Because a message can arrive twice, store the X-Webhook-Id you have processed and skip repeats.

<?php
$event = json_decode($payload, true);
$webhookId = $_SERVER['HTTP_X_WEBHOOK_ID'] ?? $event['id'];

// Already processed (retry)? Answer 200 and stop.
if (alreadyProcessed($webhookId)) {
    http_response_code(200);
    exit;
}

markProcessed($webhookId);
queueForProcessing($event);   // do the real work later
http_response_code(200);
6

Choose the Events

An API key can only subscribe to events whose data it is allowed to read (for example "invoices:read" for invoice events); other events are rejected with 422 in the "events" field. GET /api/v1/webhooks/events lists every event with the required scope and whether your key may subscribe to it. Endpoints stop receiving events when their API key is switched off, expires, loses the scope or its owner is deactivated. All available events are listed below.

curl -X GET "https://your-tenant.plandocket.com/api/v1/webhooks/events" \
  -H "Authorization: Bearer YOUR_API_KEY"

All Webhook Events

This list is generated from the system, so it is always complete. The example shows the "data" part of the message.

Applications

  • application.submitted Application received Required scope: applications:read

    A company applied through the application form.

    Example payload
    {
        "application_id": 128,
        "event_id": 1,
        "company": "Holzbau Anna GmbH",
        "email": "anna@example.com",
        "status": "pending",
        "source": "admin"
    }
  • application.updated Application changed Required scope: applications:read

    The details of an application were changed.

    Example payload
    {
        "application_id": 128,
        "event_id": 1,
        "company": "Holzbau Anna GmbH",
        "email": "anna@example.com",
        "status": "pending",
        "source": "admin"
    }
  • application.approved Application approved Required scope: applications:read

    An application was accepted; the contact has been created.

    Example payload
    {
        "status": "approved",
        "contact_id": 42,
        "application_id": 128,
        "event_id": 1,
        "company": "Holzbau Anna GmbH",
        "email": "anna@example.com",
        "source": "admin"
    }
  • application.rejected Application rejected Required scope: applications:read

    An application was rejected.

    Example payload
    {
        "status": "rejected",
        "application_id": 128,
        "event_id": 1,
        "company": "Holzbau Anna GmbH",
        "email": "anna@example.com",
        "source": "admin"
    }

Contacts

  • contact.created Contact created Required scope: contacts:read

    A new contact was created (manually, by import or through the API).

    Example payload
    {
        "contact_id": 42,
        "company": "Holzbau Anna GmbH",
        "first_name": "Anna",
        "last_name": "Holz",
        "email": "anna@example.com",
        "source": "admin"
    }
  • contact.updated Contact updated Required scope: contacts:read

    The details of a contact were changed.

    Example payload
    {
        "contact_id": 42,
        "company": "Holzbau Anna GmbH",
        "first_name": "Anna",
        "last_name": "Holz",
        "email": "anna@example.com",
        "source": "admin"
    }
  • contact.deleted Contact deleted Required scope: contacts:read

    A contact was moved to the trash.

    Example payload
    {
        "contact_id": 42,
        "company": "Holzbau Anna GmbH",
        "first_name": "Anna",
        "last_name": "Holz",
        "email": "anna@example.com",
        "source": "admin"
    }

Events

  • event.created Event created Required scope: events:read

    A new event was created.

    Example payload
    {
        "event_id": 1,
        "title": "Nordmesse 2027",
        "starts_at": "2027-05-12T09:00:00+02:00",
        "ends_at": "2027-05-13T17:00:00+02:00",
        "city": "Hamburg",
        "source": "admin"
    }
  • event.updated Event changed Required scope: events:read

    Details, dates or deadlines of an event were changed.

    Example payload
    {
        "event_id": 1,
        "title": "Nordmesse 2027",
        "starts_at": "2027-05-12T09:00:00+02:00",
        "ends_at": "2027-05-13T17:00:00+02:00",
        "city": "Hamburg",
        "source": "admin"
    }
  • event.deleted Event deleted Required scope: events:read

    An event was moved to the trash.

    Example payload
    {
        "event_id": 1,
        "title": "Nordmesse 2027",
        "starts_at": "2027-05-12T09:00:00+02:00",
        "ends_at": "2027-05-13T17:00:00+02:00",
        "city": "Hamburg",
        "source": "admin"
    }

Participants

  • participant.added Participant added Required scope: participants:read

    A contact was added as a participant of an event.

    Example payload
    {
        "event_participant_id": 7,
        "event_id": 1,
        "contact_id": 42,
        "category_id": 3,
        "is_confirmed": true,
        "source": "admin"
    }
  • participant.updated Participant changed Required scope: participants:read

    Participation details were changed.

    Example payload
    {
        "event_participant_id": 7,
        "event_id": 1,
        "contact_id": 42,
        "category_id": 3,
        "is_confirmed": true,
        "source": "admin"
    }
  • participant.removed Participant removed Required scope: participants:read

    A participant was removed from an event.

    Example payload
    {
        "event_participant_id": 7,
        "event_id": 1,
        "contact_id": 42,
        "category_id": 3,
        "is_confirmed": true,
        "source": "admin"
    }
  • participant.confirmed Participant confirmed Required scope: participants:read

    A participation was confirmed.

    Example payload
    {
        "event_participant_id": 7,
        "event_id": 1,
        "contact_id": 42,
        "category_id": 3,
        "is_confirmed": true,
        "source": "admin"
    }

Invoices and credit notes

  • invoice.created Invoice created Required scope: invoices:read

    An invoice was created.

    Example payload
    {
        "invoice_id": 311,
        "invoice_number": "RE-2027-000311",
        "contact_id": 42,
        "total": 2380,
        "currency": "EUR",
        "status": "sent",
        "due_date": "2027-04-30",
        "source": "admin"
    }
  • invoice.sent Invoice sent Required scope: invoices:read

    An invoice was sent by e-mail.

    Example payload
    {
        "invoice_id": 311,
        "invoice_number": "RE-2027-000311",
        "contact_id": 42,
        "total": 2380,
        "currency": "EUR",
        "status": "sent",
        "due_date": "2027-04-30",
        "source": "admin"
    }
  • invoice.overdue Invoice overdue Required scope: invoices:read

    An invoice is still open after its due date (once per invoice).

    Example payload
    {
        "status": "overdue",
        "days_overdue": 7,
        "invoice_id": 311,
        "invoice_number": "RE-2027-000311",
        "contact_id": 42,
        "total": 2380,
        "currency": "EUR",
        "due_date": "2027-04-30",
        "source": "admin"
    }
  • invoice.paid Invoice paid in full Required scope: invoices:read

    An invoice is paid in full. Partial payments only trigger “Payment received”.

    Example payload
    {
        "status": "paid",
        "open_amount": 0,
        "invoice_id": 311,
        "invoice_number": "RE-2027-000311",
        "contact_id": 42,
        "total": 2380,
        "currency": "EUR",
        "due_date": "2027-04-30",
        "source": "admin"
    }
  • invoice.cancelled Invoice cancelled Required scope: invoices:read

    An invoice was cancelled.

    Example payload
    {
        "status": "cancelled",
        "credit_note_id": 312,
        "invoice_id": 311,
        "invoice_number": "RE-2027-000311",
        "contact_id": 42,
        "total": 2380,
        "currency": "EUR",
        "due_date": "2027-04-30",
        "source": "admin"
    }
  • credit_note.created Credit note created Required scope: invoices:read

    A credit note or cancellation invoice was created.

    Example payload
    {
        "invoice_id": 312,
        "invoice_number": "GS-2027-000012",
        "cancelled_invoice_id": 311,
        "total": -2380,
        "status": "sent",
        "contact_id": 42,
        "currency": "EUR",
        "due_date": "2027-04-30",
        "source": "admin"
    }

Payments

  • payment.received Payment received Required scope: payments:read

    A payment was recorded – including partial and online payments.

    Example payload
    {
        "payment_id": 95,
        "invoice_id": 311,
        "amount": 1190,
        "currency": "EUR",
        "method": "bank_transfer",
        "open_amount": 1190,
        "source": "admin"
    }
  • payment.refunded Payment refunded Required scope: payments:read

    A payment was refunded in full or in part.

    Example payload
    {
        "refund_id": 9,
        "refunded_amount": 1190,
        "payment_id": 95,
        "invoice_id": 311,
        "amount": 1190,
        "currency": "EUR",
        "method": "bank_transfer",
        "open_amount": 1190,
        "source": "admin"
    }

Booths and reservations

  • booth.assigned Booth assigned Required scope: booths:read

    A booth was assigned to a participant.

    Example payload
    {
        "booth_id": 17,
        "booth_number": "A01",
        "plan_id": 2,
        "event_id": 1,
        "event_participant_id": 7,
        "source": "admin"
    }
  • booth.unassigned Booth released Required scope: booths:read

    A booth was released again.

    Example payload
    {
        "booth_id": 17,
        "booth_number": "A01",
        "plan_id": 2,
        "event_id": 1,
        "event_participant_id": 7,
        "source": "admin"
    }
  • booth_reservation.requested Booth reservation requested Required scope: booths:read

    An exhibitor reserved a booth in the portal.

    Example payload
    {
        "reservation_id": 33,
        "status": "requested",
        "booth_id": 17,
        "booth_number": "A01",
        "plan_id": 2,
        "event_id": 1,
        "event_participant_id": 7,
        "source": "admin"
    }
  • booth_reservation.confirmed Booth reservation confirmed Required scope: booths:read

    A booth reservation was confirmed.

    Example payload
    {
        "reservation_id": 33,
        "status": "confirmed",
        "booth_id": 17,
        "booth_number": "A01",
        "plan_id": 2,
        "event_id": 1,
        "event_participant_id": 7,
        "source": "admin"
    }
  • booth_reservation.cancelled Booth reservation cancelled Required scope: booths:read

    A booth reservation was cancelled.

    Example payload
    {
        "reservation_id": 33,
        "status": "cancelled",
        "booth_id": 17,
        "booth_number": "A01",
        "plan_id": 2,
        "event_id": 1,
        "event_participant_id": 7,
        "source": "admin"
    }

Service orders

  • service_order.submitted Order received Required scope: service_orders:read

    An exhibitor ordered services.

    Example payload
    {
        "status": "submitted",
        "service_order_id": 54,
        "order_number": "SO-2027-0054",
        "event_participant_id": 7,
        "total": 450,
        "currency": "EUR",
        "source": "admin"
    }
  • service_order.confirmed Order confirmed Required scope: service_orders:read

    An order was confirmed.

    Example payload
    {
        "status": "confirmed",
        "service_order_id": 54,
        "order_number": "SO-2027-0054",
        "event_participant_id": 7,
        "total": 450,
        "currency": "EUR",
        "source": "admin"
    }
  • service_order.completed Order completed Required scope: service_orders:read

    An order was completed.

    Example payload
    {
        "status": "completed",
        "service_order_id": 54,
        "order_number": "SO-2027-0054",
        "event_participant_id": 7,
        "total": 450,
        "currency": "EUR",
        "source": "admin"
    }
  • service_order.cancelled Order cancelled Required scope: service_orders:read

    An order was cancelled.

    Example payload
    {
        "status": "cancelled",
        "service_order_id": 54,
        "order_number": "SO-2027-0054",
        "event_participant_id": 7,
        "total": 450,
        "currency": "EUR",
        "source": "admin"
    }

Submissions

  • submission.submitted Submission received Required scope: submissions:read

    An exhibitor submitted something (e.g. a logo or catalogue text).

    Example payload
    {
        "submission_id": 23,
        "submission_type_id": 4,
        "event_participant_id": 7,
        "status": "submitted",
        "source": "admin"
    }
  • submission.approved Submission approved Required scope: submissions:read

    A submission was approved.

    Example payload
    {
        "status": "approved",
        "submission_id": 23,
        "submission_type_id": 4,
        "event_participant_id": 7,
        "source": "admin"
    }
  • submission.rejected Submission rejected Required scope: submissions:read

    A submission was rejected or sent back for revision.

    Example payload
    {
        "status": "rejected",
        "submission_id": 23,
        "submission_type_id": 4,
        "event_participant_id": 7,
        "source": "admin"
    }

Documents and contracts

  • document.signed Document signed Required scope: documents:read

    A contract or document was signed.

    Example payload
    {
        "document_id": 61,
        "title": "Ausstellervertrag",
        "event_participant_id": 7,
        "esign_status": "signed",
        "source": "admin"
    }
  • document.declined Signature declined Required scope: documents:read

    Signing a document was declined.

    Example payload
    {
        "esign_status": "declined",
        "document_id": 61,
        "title": "Ausstellervertrag",
        "event_participant_id": 7,
        "source": "admin"
    }

Newsletter

  • newsletter.subscribed Newsletter subscribed Required scope: newsletter:read

    Someone confirmed their newsletter subscription.

    Example payload
    {
        "subscriber_id": 210,
        "email": "news@example.com",
        "language": "de",
        "source": "admin"
    }
  • newsletter.unsubscribed Newsletter unsubscribed Required scope: newsletter:read

    Someone unsubscribed from the newsletter.

    Example payload
    {
        "subscriber_id": 210,
        "email": "news@example.com",
        "language": "de",
        "source": "admin"
    }

Check-in

  • check_in.created Check-in recorded Required scope: checkins:read

    A participant was checked in at the entrance.

    Example payload
    {
        "check_in_id": 880,
        "event_participant_id": 7,
        "event_id": 1,
        "checked_in_at": "2027-05-12T08:41:12+02:00",
        "type": "scan",
        "source": "admin"
    }

Lists

  • list_entry.created List entry added Required scope: lists:read

    An entry was added to a list (e.g. booth staff).

    Example payload
    {
        "list_entry_id": 12,
        "list_template_id": 3,
        "event_participant_id": 7,
        "values": {
            "name": "Tom Zimmer",
            "funktion": "verkauf"
        },
        "source": "admin"
    }
  • list_entry.updated List entry changed Required scope: lists:read

    A list entry was changed.

    Example payload
    {
        "list_entry_id": 12,
        "list_template_id": 3,
        "event_participant_id": 7,
        "values": {
            "name": "Tom Zimmer",
            "funktion": "verkauf"
        },
        "source": "admin"
    }
  • list_entry.deleted List entry deleted Required scope: lists:read

    A list entry was deleted.

    Example payload
    {
        "list_entry_id": 12,
        "list_template_id": 3,
        "event_participant_id": 7,
        "values": {
            "name": "Tom Zimmer",
            "funktion": "verkauf"
        },
        "source": "admin"
    }