Webhooks

jekcms emits outbound webhooks for the most important lifecycle events across content, commerce, licensing, and support. Point them at any HTTPS endpoint — n8n, Zapier, Make, a Lambda, a Cloudflare Worker — and jekcms delivers a signed JSON POST every time the event fires.

This page is the API-side reference. For the admin UI (adding webhooks, viewing delivery logs), see [Webhook management](/docs/automation/webhooks).

Events

| Event | Fires when | |-------|-----------| | post.published | A post transitions from any state to published | | post.updated | An already-published post is edited and saved | | post.deleted | A post is moved to trash or hard-deleted | | post.restored | A trashed post is restored | | comment.approved | A comment is manually or automatically approved | | comment.rejected | A comment is rejected as spam | | order.completed | A billing order transitions to paid | | order.refunded | A previously paid order is refunded | | license.activated | A license key is successfully activated on a domain | | license.deactivated | A license is deactivated (user action or expiry) | | support.ticket_created | A new support ticket is opened | | support.ticket_replied | Any party (customer or agent) replies to a ticket |

Subscribe to as many as you want per webhook URL — pick one event per webhook for cleanest downstream logic.

Common payload shape

Every payload has this envelope:

{
  "event": "post.published",
  "timestamp": "2026-04-21T14:32:17Z",
  "signature": "sha256=a1b2c3...",
  "data": { /* event-specific, see below */ }
}
  • event — string matching the table above
  • timestamp — ISO 8601, always UTC
  • signature — HMAC-SHA256 of the raw request body, prefixed sha256=
  • data — event-specific object

Event payloads

post.published, post.updated, post.deleted, post.restored

{
  "post": {
    "id": 123,
    "title": "My post title",
    "slug": "my-post-title",
    "excerpt": "First 160 chars...",
    "url": "https://yoursite.com/my-post-title",
    "status": "published",
    "published_at": "2026-04-21T14:32:17Z"
  },
  "author": { "id": 1, "name": "Jane Doe", "email": "jane@example.com" },
  "categories": ["Nutrition"],
  "tags": ["protein", "cutting"]
}

comment.approved, comment.rejected

{
  "comment": {
    "id": 456,
    "post_id": 123,
    "author_name": "Alex",
    "author_email": "alex@example.com",
    "content": "Great write-up.",
    "status": "approved"
  },
  "post": { "id": 123, "title": "...", "url": "..." }
}

order.completed, order.refunded

{
  "order": {
    "id": 789,
    "reference": "JEK-2026-00789",
    "amount_cents": 4900,
    "currency": "USD",
    "status": "paid",
    "paid_at": "2026-04-21T14:32:17Z"
  },
  "customer": { "id": 42, "email": "buyer@example.com", "name": "Buyer Name" },
  "items": [{ "sku": "pro-annual", "name": "Pro Annual", "price_cents": 4900 }]
}

license.activated, license.deactivated

{
  "license": {
    "key": "JEK-XXXX-XXXX-XXXX",
    "tier": "pro",
    "domain": "example.com",
    "activated_at": "2026-04-21T14:32:17Z",
    "expires_at": "2027-04-21T00:00:00Z"
  },
  "customer": { "id": 42, "email": "buyer@example.com" }
}

support.ticket_created, support.ticket_replied

{
  "ticket": {
    "id": 555,
    "subject": "Can't upload images",
    "status": "open",
    "priority": "normal",
    "url": "https://yoursite.com/admin/support/555"
  },
  "message": { "id": 9001, "author_type": "customer", "body": "..." },
  "customer": { "id": 42, "email": "buyer@example.com" }
}

Signature verification

Every webhook includes a signature field computed as:

signature = "sha256=" + hex( HMAC_SHA256( secret, raw_json_body ) )
  • Secret: the webhook's secret, shown once when you create the webhook in Automation → Webhooks
  • Input: the exact raw request body bytes, before any parsing or re-serialization

Node.js verification example:

const crypto = require('crypto');
const expected = 'sha256=' + crypto
  .createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(rawBody)  // Buffer or string — must be the raw body
  .digest('hex');
if (expected !== payload.signature) {
  return res.status(401).end();
}

Always use a constant-time compare (crypto.timingSafeEqual in Node, hmac.compare_digest in Python) in production. A naive === leaks timing signal on long-running endpoints.

Retry behavior

If your endpoint returns a non-2xx response, jekcms retries the delivery up to 3 times with exponential back-off:

| Attempt | Delay from prior attempt | |---------|--------------------------| | 1 | (initial) | | 2 | 1 second | | 3 | 10 seconds | | 4 | 60 seconds |

After 4 total failures, the delivery is marked failed and no further retries are attempted. It stays visible in Automation → Webhooks → Delivery log for 30 days.

HTTP response semantics

  • 2xx — delivery accepted, no retry
  • Non-2xx (4xx, 5xx, timeout) — delivery retried per the schedule above
  • 410 Gone — treated as a permanent signal to stop retrying this delivery and auto-disable the webhook. Use this when the endpoint is decommissioned.

The delivery timeout is 10 seconds. Endpoints that take longer should ACK immediately and do async work off the request thread.

Be the first to know

New features, release notes & CMS guides — a couple of emails a month, no spam.