Agent skill
Document360 Webhooks Skill
Receive and authenticate Document360 webhooks. Use when setting up a Document360 Webhook notification channel endpoint, checking the secret header you add to the channel, debugging 401s, or handling knowledge base events like Article published, Article created and Article deleted.
Install this skill
npx skills add hookdeck/webhook-skills --skill document360-webhooks
Document360's Webhook notification channel sends a JSON payload to your URL when mapped knowledge base events happen: an article is published, created or deleted, a workflow status changes, an extension token is deleted. Webhooks are not signed. You authenticate them with a secret header that you add to the channel as a key-value pair; your endpoint compares that header with the value you stored.
When to Use This Skill
- How do I receive Document360 webhooks?
- Does Document360 sign webhook payloads? (No: use a secret header you add to the channel)
- How do I check the header I set on a Document360 Webhook channel?
- How do I handle
Article published,Article createdorArticle deletedevents? - Why does my Document360 webhook endpoint return 401 or reject PUT requests?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyDocument360Request(
headers, // Node lowercases header names
secret = process.env.DOCUMENT360_WEBHOOK_SECRET, // the WHOLE header value
name = process.env.DOCUMENT360_WEBHOOK_HEADER || 'Authorization'
) {
if (!secret) return false; // never accept when unconfigured
const received = headers[name.toLowerCase()];
return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}
- The header name is yours: it is whatever key you typed in the channel. Read it from
DOCUMENT360_(defaultWEBHOOK_ HEADER Authorization) and look it up case-insensitively. - Compare the whole value, exactly as typed, including any
Bearerprefix. Don't strip or parse it. - Python:
hmac.compare_digeston bytes (encode both sides, or compare SHA-256 digests); passingstrraisesTypeErroron non-ASCII input. - Return
401for a missing or mismatched header,400for a non-empty body that isn't a JSON object,200once handled. An authenticated request with an empty body gets200. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - Accept POST and PUT: the channel's Request method dropdown offers both. In Next.js export a handler for each; in FastAPI use
methods=["POST", "PUT"].
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Anyone who captures one request can resend it. Use HTTPS, keep the value secret, and rotate it in both Document360 and your environment if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The payload's Event field carries the event's display name. Keys are display labels with spaces.
| Event | Sent When |
|---|---|
Article published | An article is published |
Article created | A new article is created |
Article deleted | An article is deleted (payload adds Slugs, one per language) |
Article settings saved | Article settings are saved (name used in the docs' sample payload) |
Article workflow status updated | An article's workflow status changes |
Extension token deleted | An extension token is deleted (payload has Extension) |
These are a subset. The full list of events you can map is in Document360's notification mapping docs (categories, Drive, workspaces, site settings and more). Return 200 for events you don't handle too; Document360 doesn't document whether or how it retries failed deliveries.
Article event payload (the docs' example):
{
"Knowledge base name": "Document360 2.0",
"Event": "Article settings saved",
"User": "Matt Sterling",
"User email": "matt.sterling@document360.co",
"Logged on (UTC)": "09/12/2025 06:15:00",
"Article name": "Getting Started",
"Article id": "e1444f0x-0xx0-000x-xxx0-0x3f2bc3713c",
"Language": "English",
"Language code": "en",
"Project version id": "55144000-0xxx-000x-0000-0x2346b3bms3",
"Project version name": "1.0"
}
Environment Variables
DOCUMENT360_WEBHOOK_HEADER=Authorization # the header key you added to the channel
DOCUMENT360_WEBHOOK_SECRET=your-secret # its exact, whole value (including any "Bearer ")
Using Hookdeck
Hookdeck's Document360 source type authenticates this the same way: you give it the header name and the exact whole value. The source pre-selects POST without managing the method, so if your channel's Request method is PUT, also allow PUT on the source.
Local Development
Document360 can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 document360 --path /webhooks/document360
Use the printed URL as the Webhook URL in the channel, and add the same header there.
Resources
- references/overview.md - What Document360 webhooks are, events and payload shape
- references/setup.md - Create the Webhook channel, add the header, map events
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests