# How Ghost Webhooks Work and How to Create Custom Webhooks

> Explore Ghost webhooks explained. Learn how they function and get step-by-step guidance to create your own custom webhooks for seamless integrations.

- Repository: [Ghost/Ghost](https://github.com/TryGhost/Ghost)
- Tags: how-to-guide
- Published: 2026-05-18

---

How Ghost Webhooks Work and How to Create Custom Webhooks

**Ghost implements event-driven webhooks through a three-layer architecture where Admin API endpoints register definitions in the database, and a WebhookTrigger service delivers signed JSON payloads to target URLs when events occur.**

Ghost webhooks provide a real-time notification system that alerts external services when content changes or members interact with your site. According to the TryGhost/Ghost source code, this system processes events through distinct API, model, and trigger layers to ensure reliable delivery of webhook notifications.

## Architecture of the Ghost Webhook System

Ghost organizes its webhook infrastructure into three main components that handle registration, persistence, and execution.

### Admin API Layer

The entry point for webhook management resides in [`ghost/core/core/server/api/endpoints/webhooks.js`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/api/endpoints/webhooks.js). This controller exposes endpoints for creating, editing, and deleting webhook records while enforcing permission checks. When you register a new webhook, this layer validates the input schema and delegates storage to the model layer.

### Model Layer

Webhook definitions persist in the `webhooks` database table via [`ghost/core/core/server/models/webhook.js`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/models/webhook.js). This Bookshelf model emits change events whenever records are added, modified, or removed, ensuring the system remains synchronized with your integration requirements.

### Trigger Layer

The execution engine lives in [`ghost/core/core/server/services/webhooks/webhook-trigger.js`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/services/webhooks/webhook-trigger.js). The **WebhookTrigger** service listens for Ghost events (such as `member.added`), retrieves matching webhook definitions via `WebhookTrigger#getAll`, constructs JSON payloads, and delivers them to target URLs. This service also handles automatic cleanup and respects custom integration limits.

## How the WebhookTrigger Service Processes Events

When a Ghost event fires, the trigger service executes a specific lifecycle:

1. **Event Matching**: The service queries the `webhooks` table for entries where the `event` column matches the fired event name.
2. **Payload Construction**: For each match, Ghost builds a JSON payload containing the event type and relevant model data (such as member details).
3. **Request Signing**: If the webhook includes a `secret`, the service generates an HMAC-SHA256 signature and attaches it as the `X-Ghost-Signature` header.
4. **HTTP Delivery**: The service POSTs the payload to the `target_url`.
5. **Status Tracking**: After delivery, Ghost updates `last_triggered_at` and `last_triggered_status` via `WebhookTrigger#update`.
6. **Auto-Cleanup**: If the endpoint returns HTTP 410 (Gone), `WebhookTrigger#onError` automatically destroys the webhook record to prevent further delivery attempts.

The service also enforces the **custom-integrations limit**—when limits are reached, only internal webhooks fire.

## Creating Custom Webhooks via the Admin API

To register a custom webhook, send a `POST` request to `/admin/webhooks/` with the following required fields:

- **`name`**: Human-readable identifier for the webhook
- **`event`**: Ghost event identifier (e.g., `member.added`, `post.published`)
- **`target_url`**: HTTPS endpoint that will receive the POST request
- **`integration_id`**: UUID of the integration that owns this webhook
- **`secret`** *(optional)*: String used to sign requests via HMAC-SHA256

The validation logic in [`ghost/core/core/server/api/endpoints/utils/validators/input/webhooks.js`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/api/endpoints/utils/validators/input/webhooks.js) enforces that `integration_id` must be present when using session authentication; otherwise, it throws a `ValidationError`.

### Example: Registering a Webhook with Node.js

```javascript
import fetch from 'node-fetch';

const ADMIN_API_KEY = 'YOUR_ADMIN_API_KEY';
const GHOST_URL = 'http://localhost:2368';

await fetch(`${GHOST_URL}/admin/webhooks/`, {
  method: 'POST',
  headers: {
    'Authorization': `Ghost ${ADMIN_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    webhooks: [{
      name: 'Member Signup Handler',
      event: 'member.added',
      target_url: 'https://api.example.com/ghost-webhooks/members',
      secret: 'whsec_super_secret_key',
      integration_id: '5f29d1c5c7c3f6001f8b4567'
    }]
  })
});

```

## Webhook Payload Structure

When Ghost triggers your webhook, it sends a JSON payload with this structure:

```json
{
  "event": "member.added",
  "data": {
    "member": {
      "id": "5f29d1c5c7c3f6001f8b4567",
      "email": "user@example.com",
      "name": "Jane Doe",
      "subscribed": true,
      "created_at": "2023-07-21T15:30:00.000Z"
    }
  }
}

```

This matches the internal payload generation logic where Ghost converts the Bookshelf model to JSON and wraps it in an event-specific structure.

## Verifying Webhook Signatures

When you provide a `secret`, Ghost sends requests with the header:

```

X-Ghost-Signature: sha256=abc123def456..., t=1658421234567

```

Verify this signature in your receiver to confirm the payload originated from your Ghost instance:

```javascript
import crypto from 'crypto';
import express from 'express';

const app = express();
app.use(express.json());

app.post('/ghost-webhooks/members', (req, res) => {
  const signature = req.headers['x-ghost-signature'];
  const secret = 'whsec_super_secret_key'; // Same secret used during creation
  
  if (!signature) {
    return res.status(400).send('Missing signature');
  }
  
  const [hashPart, tsPart] = signature.split(', ');
  const receivedHash = hashPart.split('=')[1];
  const timestamp = tsPart.split('=')[1];
  
  const expectedHash = crypto
    .createHmac('sha256', secret)
    .update(`${JSON.stringify(req.body)}${timestamp}`)
    .digest('hex');
  
  if (crypto.timingSafeEqual(Buffer.from(receivedHash), Buffer.from(expectedHash))) {
    console.log('Verified Ghost webhook:', req.body);
    res.sendStatus(200);
  } else {
    res.status(401).send('Invalid signature');
  }
});

```

## Managing Webhooks Programmatically

Beyond creation, you can manage webhooks through the Admin API:

**Delete a webhook:**

```javascript
await fetch(`${GHOST_URL}/admin/webhooks/${WEBHOOK_ID}/`, {
  method: 'DELETE',
  headers: {
    'Authorization': `Ghost ${ADMIN_API_KEY}`
  }
});

```

Ghost also maintains a webhooks table component in the admin interface at [`apps/admin-x-settings/src/components/settings/advanced/integrations/webhooks-table.tsx`](https://github.com/TryGhost/Ghost/blob/main/apps/admin-x-settings/src/components/settings/advanced/integrations/webhooks-table.tsx) for manual management.

## Summary

- Ghost webhooks use a three-tier architecture: Admin API endpoints ([`webhooks.js`](https://github.com/TryGhost/Ghost/blob/main/webhooks.js)), a Bookshelf model ([`webhook.js`](https://github.com/TryGhost/Ghost/blob/main/webhook.js)), and a trigger service ([`webhook-trigger.js`](https://github.com/TryGhost/Ghost/blob/main/webhook-trigger.js)).
- Custom webhooks require `POST /admin/webhooks/` with `name`, `event`, `target_url`, and `integration_id` fields.
- The **WebhookTrigger** service automatically signs payloads with HMAC-SHA256 when a `secret` is configured, and destroys webhooks that return HTTP 410.
- Always verify the `X-Ghost-Signature` header in production to ensure payload authenticity.

## Frequently Asked Questions

### What events can trigger Ghost webhooks?

Ghost webhooks support any internal event emitted by the platform, including `member.added`, `post.published`, `post.updated`, and `subscriber.added`. The specific event name must match exactly what Ghost emits internally—for example, `member.added` fires when a new member completes signup.

### How do I verify a webhook is actually from Ghost?

When creating the webhook with a `secret` parameter, Ghost generates an HMAC-SHA256 hash of the JSON payload concatenated with a timestamp, formatted as `sha256=HASH, t=TIMESTAMP` in the `X-Ghost-Signature` header. Your receiving service must reconstruct this hash using the shared secret and compare it using constant-time comparison to prevent timing attacks.

### What happens if my webhook endpoint returns a 410 error?

Ghost automatically destroys webhooks that receive an HTTP 410 (Gone) response. The `WebhookTrigger#onError` method in [`ghost/core/core/server/services/webhooks/webhook-trigger.js`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/services/webhooks/webhook-trigger.js) handles this cleanup to prevent Ghost from repeatedly attempting delivery to defunct endpoints.

### Can I create webhooks without an integration ID?

No—when using session authentication, the validator in [`ghost/core/core/server/api/endpoints/utils/validators/input/webhooks.js`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/api/endpoints/utils/validators/input/webhooks.js) requires the `integration_id` field. You must create a custom integration in Ghost Admin first, then use that integration's ID when registering webhooks. This associates the webhook with your specific integration and enforces permission boundaries.