Setting Up Webhooks
Configure real-time notifications for events.
Prerequisites
- An API key with the required scopes
- An active PlanDocket account
Create a Webhook Endpoint
Create an endpoint on your server that can receive POST requests.
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"]
}'
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);
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"
}
}
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);
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.submittedApplication received Required scope:applications:readA 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.updatedApplication changed Required scope:applications:readThe 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.approvedApplication approved Required scope:applications:readAn 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.rejectedApplication rejected Required scope:applications:readAn 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.createdContact created Required scope:contacts:readA 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.updatedContact updated Required scope:contacts:readThe 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.deletedContact deleted Required scope:contacts:readA 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.createdEvent created Required scope:events:readA 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.updatedEvent changed Required scope:events:readDetails, 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.deletedEvent deleted Required scope:events:readAn 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.addedParticipant added Required scope:participants:readA 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.updatedParticipant changed Required scope:participants:readParticipation details were changed.
Example payload
{ "event_participant_id": 7, "event_id": 1, "contact_id": 42, "category_id": 3, "is_confirmed": true, "source": "admin" } -
participant.removedParticipant removed Required scope:participants:readA 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.confirmedParticipant confirmed Required scope:participants:readA 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.createdInvoice created Required scope:invoices:readAn 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.sentInvoice sent Required scope:invoices:readAn 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.overdueInvoice overdue Required scope:invoices:readAn 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.paidInvoice paid in full Required scope:invoices:readAn 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.cancelledInvoice cancelled Required scope:invoices:readAn 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.createdCredit note created Required scope:invoices:readA 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.receivedPayment received Required scope:payments:readA 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.refundedPayment refunded Required scope:payments:readA 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.assignedBooth assigned Required scope:booths:readA 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.unassignedBooth released Required scope:booths:readA 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.requestedBooth reservation requested Required scope:booths:readAn 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.confirmedBooth reservation confirmed Required scope:booths:readA 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.cancelledBooth reservation cancelled Required scope:booths:readA 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.submittedOrder received Required scope:service_orders:readAn 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.confirmedOrder confirmed Required scope:service_orders:readAn 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.completedOrder completed Required scope:service_orders:readAn 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.cancelledOrder cancelled Required scope:service_orders:readAn 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.submittedSubmission received Required scope:submissions:readAn 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.approvedSubmission approved Required scope:submissions:readA submission was approved.
Example payload
{ "status": "approved", "submission_id": 23, "submission_type_id": 4, "event_participant_id": 7, "source": "admin" } -
submission.rejectedSubmission rejected Required scope:submissions:readA 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.signedDocument signed Required scope:documents:readA contract or document was signed.
Example payload
{ "document_id": 61, "title": "Ausstellervertrag", "event_participant_id": 7, "esign_status": "signed", "source": "admin" } -
document.declinedSignature declined Required scope:documents:readSigning a document was declined.
Example payload
{ "esign_status": "declined", "document_id": 61, "title": "Ausstellervertrag", "event_participant_id": 7, "source": "admin" }
Newsletter
Check-in
-
check_in.createdCheck-in recorded Required scope:checkins:readA 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.createdList entry added Required scope:lists:readAn 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.updatedList entry changed Required scope:lists:readA 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.deletedList entry deleted Required scope:lists:readA 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" }