ClientPress can send a signed HTTP POST to any URL whenever a portal event occurs. Use this to connect your portals to Zapier, Make, n8n, OttoKit, or any custom HTTP endpoint — no vendor lock-in, no API polling required.
Configuration #
Go to Portals → Settings → Outbound Webhooks.
- Webhook URL — The HTTPS endpoint that receives events. Leave blank to disable webhooks entirely.
- Signing secret — Optional. When set, each request includes an
X-CP-Signatureheader you can use to verify the payload came from your site. - Events to send — Checkboxes for each supported event. Only checked events trigger a delivery.
Request Format #
Every webhook is an HTTP POST with a JSON body and the following headers:
Content-Type—application/jsonX-CP-Event— The event slug, e.g.task.completedX-CP-Portal-ID— The portal post ID as a stringX-CP-Delivery— A UUID4 that uniquely identifies this deliveryUser-Agent—ClientPress/{version}; https://clientpress.ioX-CP-Signature—sha256=<hmac>— only present when a signing secret is configured
Payload shape #
{
"event": "task.completed",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-18T14:22:00+00:00",
"data": { ... event-specific fields ... }
}
timestamp is always UTC in ISO 8601 format (gmdate('c')).
Signature Verification #
When a signing secret is configured, ClientPress computes:
X-CP-Signature: sha256=<hex(HMAC-SHA256(raw_json_body, secret))>
To verify in PHP:
$expected = 'sha256=' . hash_hmac( 'sha256', file_get_contents('php://input'), YOUR_SECRET );
$received = $_SERVER['HTTP_X_CP_SIGNATURE'] ?? '';
if ( ! hash_equals( $expected, $received ) ) {
http_response_code( 401 );
exit;
}
To verify in Node.js:
const crypto = require('crypto');
function verify(body, secret, header) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
Always use a constant-time comparison (hash_equals / timingSafeEqual) to prevent timing attacks.
Event Catalog #
portal.created #
Fired when a portal is created via the REST API.
{
"event": "portal.created",
"portal_id": 58,
"portal_url": "https://your-site.com/client-portal/riverside-dental/",
"timestamp": "2024-02-01T09:15:22+00:00",
"data": {
"title": "Riverside Dental",
"slug": "riverside-dental",
"status": "pending"
}
}
file.uploaded #
Fired when a file is uploaded to a portal.
{
"event": "file.uploaded",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-10T09:00:00+00:00",
"data": {
"name": "contract-q1-2024.pdf",
"original": "Contract Q1 2024.pdf",
"size": 204800,
"type": "application/pdf",
"approval_status": "none"
}
}
approval_status is "pending" when the uploader required approval, "none" otherwise.
file.approved #
Fired when an admin or designated approver approves a file.
{
"event": "file.approved",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-11T10:15:00+00:00",
"data": {
"name": "contract-q1-2024.pdf",
"original": "Contract Q1 2024.pdf"
}
}
file.rejected #
Fired when a file is rejected.
{
"event": "file.rejected",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-11T10:20:00+00:00",
"data": {
"name": "contract-draft.docx",
"original": "Contract Draft.docx",
"rejection_note": "Please use the updated template."
}
}
rejection_note is null when no reason was given.
task.created #
Fired when a task is created in any portal task list.
{
"event": "task.created",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-10T09:00:00+00:00",
"data": {
"id": 10,
"title": "Sign the contract",
"list_id": 1,
"due_date": "2024-01-20",
"assignee_ids": [7]
}
}
due_date is null when no due date is set.
task.completed #
Fired when a task is marked complete. Does not fire on re-open.
{
"event": "task.completed",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-18T14:22:00+00:00",
"data": {
"id": 10,
"title": "Sign the contract",
"list_id": 1
}
}
message.sent #
Fired when a message is posted in the portal’s 1-on-1 discussion thread.
{
"event": "message.sent",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-17T11:05:00+00:00",
"data": {
"sender_id": 7
}
}
invite.accepted #
Fired when a client follows their invitation link and creates their account.
{
"event": "invite.accepted",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-12T08:30:00+00:00",
"data": {
"user_id": 7,
"email": "jane@acme.com",
"display_name": "Jane Smith"
}
}
board.topic_posted #
Fired when a new topic is created in the portal message board.
{
"event": "board.topic_posted",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-01-15T10:00:00+00:00",
"data": {
"topic_id": 101,
"title": "Kickoff call notes",
"author_id": 1
}
}
deliverable.uploaded #
Fired when an admin or project manager uploads a new deliverable or adds a linked deliverable to a portal. Also fires when a revised version of an existing deliverable is uploaded.
{
"event": "deliverable.uploaded",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-02-10T11:00:00+00:00",
"data": {
"id": "del_66a1b2c3d4e5f",
"name": "logo-v2.pdf",
"category": "design",
"type": "application/pdf",
"size": 204800,
"linked": false,
"uploaded_by": 1
}
}
linked is true when the deliverable is an external URL rather than an uploaded file; in that case type is "" and size is 0.
When approval is not required: If the deliverable was uploaded with the “Require client approval” toggle unchecked, it is immediately
approvedat upload time. This event fires with the deliverable already in that state —deliverable.in_reviewanddeliverable.approvedwill never fire for it.
deliverable.in_review #
Fired when an admin or project manager marks a deliverable as ready for client review.
{
"event": "deliverable.in_review",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-02-10T14:30:00+00:00",
"data": {
"id": "del_66a1b2c3d4e5f",
"name": "logo-v2.pdf",
"category": "design"
}
}
deliverable.approved #
Fired when a client approves a deliverable.
{
"event": "deliverable.approved",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-02-11T09:15:00+00:00",
"data": {
"id": "del_66a1b2c3d4e5f",
"name": "logo-v2.pdf",
"category": "design",
"approved_by": 7,
"approved_at": "2024-02-11 09:15:00"
}
}
deliverable.revision_requested #
Fired when a client requests revisions on a deliverable. Use this to notify your team in Slack, create a task in your project management tool, or trigger an internal workflow.
{
"event": "deliverable.revision_requested",
"portal_id": 42,
"portal_url": "https://your-site.com/client-portal/acme-corp/",
"timestamp": "2024-02-10T16:45:00+00:00",
"data": {
"id": "del_66a1b2c3d4e5f",
"name": "logo-v2.pdf",
"category": "design",
"revision_note": "Please try a darker shade of blue.",
"revisions_used": 1,
"revisions_allowed": 3
}
}
revision_note is "" when the client did not add a note. revisions_used and revisions_allowed are both 0 when the revisions system is disabled in Settings.
Delivery Behaviour #
- Fire-and-forget — WordPress sends the request with
blocking: false. ClientPress does not wait for a response. This means webhooks never add latency to the portal action that triggered them. - No retries — If your endpoint is down or returns an error, the payload is silently dropped. Build your endpoint to respond quickly (under 5 seconds) and implement idempotency using the
X-CP-DeliveryUUID if needed. - SSL — TLS verification is enabled by default. To disable it (e.g. for local development), add this to your theme’s
functions.php:add_filter( 'cp_webhook_sslverify', '__return_false' );
Zapier / Make / n8n Quick-Start #
Zapier #
- Create a new Zap → trigger Webhooks by Zapier → Catch Hook.
- Copy the webhook URL Zapier gives you.
- Paste it into Portals → Settings → Webhook URL.
- Check the events you want to forward.
- Send a test event (e.g. complete a task) to capture a sample payload.
- Map the payload fields to your Zap actions.
Make (formerly Integromat) #
- Create a scenario → add a Webhooks → Custom webhook module.
- Copy the webhook URL and paste it into ClientPress Settings.
- Trigger a test event to let Make detect the payload structure automatically.
- Connect subsequent modules using the mapped fields.
n8n #
- Add a Webhook node (POST method).
- Copy the production URL and paste it into ClientPress Settings.
- Execute the workflow once manually to capture a test payload.
- Wire the output to whatever downstream nodes you need.
Extending #
To dispatch a webhook from custom code (e.g. a third-party plugin integration):
\CP\Webhooks::dispatch( 'task.completed', $portal_id, [
'id' => $task_id,
'title' => $task_title,
'list_id' => $list_id,
] );
Only events listed in Webhooks::EVENTS and enabled in Settings will actually fire — unrecognised slugs are silently ignored.
