> Content index: https://quillly.com/docs/llms.txt
> Canonical page: https://quillly.com/docs/webhooks

---
title: Webhook Publishing: Payload, Bearer Token and Events
description: Send Quillly items to your own code with a webhook: the JSON payload, the Bearer token, the publish, update, delete and ping events, and a sample receiver.
keywords: webhook, payload, Bearer token
published: 2026-09-24
updated: 2026-09-24
url: https://quillly.com/docs/webhooks
word_count: 1253
---

# Webhook publishing

> Send Quillly items to your own code with a webhook: the JSON payload, the Bearer token, the publish, update, delete and ping events, and a sample receiver.

Canonical: https://quillly.com/docs/webhooks
Published: 2026-09-24

A webhook sends each item in a section to a URL you control, as JSON, when the item is published, changed, unpublished or deleted. Use it to feed a custom CMS, a static site build or an automation tool such as Zapier.

**Before you start**, you need:

- **A section in Quillly**, such as **Blog**. See [Content types and URLs](https://quillly.com/docs/content-types).

- **An HTTPS endpoint** that accepts a `POST` with a JSON body and answers within 15 seconds.

![The Publishing pane of the Blog settings dialog with Webhook picked, the Webhook URL and Bearer token fields, and the Test connection button](https://quillly.com/serve/v1/019c64a2-a62f-7793-aa68-2c78316d3309/images/8b3953e78b991e861b7f13facef7dc9d58b8983a.webp)

## Set up the webhook

### 1. Open the section's Publishing settings

Under **Content**, open the section, such as **Blog**. Click the gear icon at the top of the section, then **Publishing**.

[Open your blog settings](https://quillly.com/dashboard/blog)

### 2. Pick Webhook and enter your URL

Under **Publish to**, click **Webhook**. Enter your endpoint in **Webhook URL**, such as `https://acme.com/api/quillly`.

### 3. Add a Bearer token

In **Bearer token (optional)**, enter a long random string that your endpoint also knows. Quillly then sends it on every request in the [Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Authorization), as `Authorization: Bearer <token>`, and your endpoint can refuse requests without it. The token is optional, but without it anyone who finds the URL can post to it.

### 4. Save, then test the connection

Click **Save**. Open the gear icon again, go to **Publishing**, and click **Test connection**. Quillly sends a `ping` event, and any `2xx` answer passes with **Webhook responded HTTP 200**.

If you picked **Your existing platform** when you [added the website](https://quillly.com/docs/add-website), the **Connect your platform** screen offers **Webhook** for your **Blog** with the same two fields.

## The request

Quillly sends every event the same way:

- **Method:** `POST` to your **Webhook URL**.

- **Headers:** `Content-Type: application/json`, plus `Authorization: Bearer <token>` when you set a token.

- **Body:** a JSON object with `event`, `site` and, except for `ping`, `item`.

- **Timeout:** 15 seconds for content events, 10 seconds for `ping`.

## Events

![The five webhook events Quillly sends, publish, update, update with status draft, delete and ping, and what your endpoint answers to each](https://quillly.com/serve/v1/019c64a2-a62f-7793-aa68-2c78316d3309/images/6c44a3d515c8eabf343d7df2e8d6b0739bd66605.webp)

| `event` | When Quillly sends it | `item.status` |
| --- | --- | --- |
| `publish` | An item in the section goes live, and Quillly has no `externalId` for it yet. | `published` |
| `update` | A change is saved to a published item that has an `externalId`. | `published` |
| `update` | An item with an `externalId` is unpublished or archived. There's no separate unpublish event. | `draft` |
| `delete` | An item with an `externalId` is deleted in Quillly. | `draft` |
| `ping` | You click **Test connection**. There's no `item`. | none |

The `externalId` is the ID your endpoint returns for the item, as the next sections show. Until Quillly has one, every change to a published item arrives as another `publish`, and unpublishing or deleting the item sends nothing.

## The payload

This is an `update` event, built by Quillly's own code with sample data:

```json
{
  "event": "update",
  "site": {
    "websiteId": "0196f1c2-3b4d-7e8f-a1b2-c3d4e5f60718",
    "domain": "acme.com"
  },
  "item": {
    "id": "0196f2a4-7c1e-7b52-9d3a-2f8e6c41a0b7",
    "externalId": "post_1284",
    "title": "Choosing trail running shoes",
    "slug": "choosing-trail-shoes",
    "html": "<h2 id=\"heading-0-fit-comes-first\">Fit comes first</h2><p>Try shoes on in the <strong>afternoon</strong>, when your feet are largest.</p>",
    "markdown": "## Fit comes first\n\nTry shoes on in the **afternoon**, when your feet are largest.",
    "excerpt": "How to pick trail shoes that fit, grip and last.",
    "featuredImageUrl": "https://acme.com/images/trail-shoes.jpg",
    "status": "published",
    "metaTitle": "Choosing Trail Running Shoes: Fit, Grip and Wear",
    "metaDescription": "How to pick trail shoes that fit, grip and last.",
    "tags": ["trail running shoes", "running gear"]
  }
}
```

| Field | What it holds |
| --- | --- |
| `event` | `publish`, `update`, `delete` or `ping`. |
| `site.websiteId` | Your website's ID in Quillly. |
| `site.domain` | Your website's domain, such as `acme.com`. |
| `item.id` | The item's ID in Quillly. It never changes, even when the slug does. |
| `item.externalId` | The ID your endpoint returned earlier. Missing on the first `publish`. |
| `item.title`, `item.slug` | The item's title and its current slug. |
| `item.html` | The body as HTML, the way Quillly renders it, without the title. |
| `item.markdown` | The same body as Markdown. |
| `item.excerpt` | The item's excerpt, or its meta description when it has no excerpt. |
| `item.featuredImageUrl` | The featured image's URL. |
| `item.status` | `published`, or `draft` when the item was unpublished, archived or deleted. |
| `item.metaTitle`, `item.metaDescription` | The SEO title and meta description. |
| `item.tags` | The item's target keywords. |

Fields without a value are left out, so check for them before you use them. A `delete` event carries only `id`, `externalId`, `title`, `slug`, `status` and an empty `html` and `markdown`. The payload doesn't name the section, so give each section its own URL if one endpoint serves several.

## What your endpoint returns

Answer with any `2xx` [status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status) to accept the event. Anything else counts as a failure.

For `publish` and `update`, you can also return JSON with two optional fields:

```json
{
  "url": "https://acme.com/blog/choosing-trail-shoes",
  "externalId": "post_1284"
}
```

- `url` becomes the item's live address in Quillly.

- `externalId` is stored with the item and sent back with every later event for it. Use your own ID, or return Quillly's `item.id`.

> [!WARNING]
> Return an `externalId` on the first `publish`. Without one, Quillly can't tell your endpoint when the item is unpublished or deleted, and every change arrives as a new `publish`.

## Failures and retries

Quillly sends each event once. If your endpoint times out or answers with an error, nothing is retried, the item stays published in Quillly, and the dashboard shows no error. The next change you save to the item sends it again. Keep a log on your side, and run **Test connection** after you change the URL or the token.

## Example receiver

This [Express](https://expressjs.com/) server checks the token, then writes each item to a Markdown file named after Quillly's ID, so a changed slug still finds the same file. It returns that ID as the `externalId`, so `update` and `delete` events arrive.

```js
import express from 'express';
import { mkdir, writeFile, rm } from 'node:fs/promises';
import path from 'node:path';

const TOKEN = process.env.QUILLLY_WEBHOOK_TOKEN;
const DIR = path.resolve('content/blog');

const app = express();
// Posts carry both HTML and Markdown, so allow a large body.
app.use(express.json({ limit: '5mb' }));

app.post('/api/quillly', async (req, res) => {
  if (!TOKEN || req.get('authorization') !== `Bearer ${TOKEN}`) {
    return res.status(401).json({ error: 'unauthorized' });
  }

  const { event, item } = req.body;
  if (event === 'ping') return res.json({ ok: true });

  const id = String(item.id).replace(/[^\w-]/g, '');
  const file = path.join(DIR, `${id}.md`);

  if (event === 'delete') {
    await rm(file, { force: true });
    return res.json({ ok: true });
  }

  // publish and update: write the latest version.
  // status is "draft" when the item was unpublished or archived.
  await mkdir(DIR, { recursive: true });
  const frontMatter = [
    '---',
    `title: ${JSON.stringify(item.title)}`,
    `slug: ${item.slug}`,
    `status: ${item.status}`,
    `description: ${JSON.stringify(item.metaDescription ?? '')}`,
    '---',
  ].join('\n');
  await writeFile(file, `${frontMatter}\n\n${item.markdown}\n`);

  return res.json({
    url: `https://acme.com/blog/${item.slug}`,
    externalId: id,
  });
});

app.listen(3000);
```

Answer quickly. If your build takes longer than 15 seconds, save the item, answer `200`, and build afterwards.

> [!NOTE]
> Quillly doesn't show a saved Bearer token again. If you later save any other change in this section's settings, enter the token again first, or Quillly clears it and stops sending the `Authorization` header.

## Common questions

### Does Quillly sign the payload?

No. The [Bearer token](https://datatracker.ietf.org/doc/html/rfc6750) is the only proof that a request came from Quillly, so use a long random token and an HTTPS URL.

### Is there an event for unpublishing?

No. Unpublishing or archiving an item sends `update` with `item.status` set to `draft`, as long as the item has an `externalId`.

### How do I tell a docs page from a blog post?

The payload doesn't say which section an item came from. Give each section its own webhook URL, such as `/api/quillly/blog` and `/api/quillly/docs`.

## Related

- [Publish into WordPress, Ghost, Webflow or Shopify](https://quillly.com/docs/publish-to-cms): the same settings for a platform Quillly supports directly.

- [Content types and URLs](https://quillly.com/docs/content-types): the five content types and where each one lives.

- [Add a website](https://quillly.com/docs/add-website): the three ways to set up a site, including your existing platform.
