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 created or Article deleted events?
  • 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_WEBHOOK_HEADER (default Authorization) and look it up case-insensitively.
  • Compare the whole value, exactly as typed, including any Bearer prefix. Don't strip or parse it.
  • Python: hmac.compare_digest on bytes (encode both sides, or compare SHA-256 digests); passing str raises TypeError on non-ASCII input.
  • Return 401 for a missing or mismatched header, 400 for a non-empty body that isn't a JSON object, 200 once handled. An authenticated request with an empty body gets 200.
  • 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.

EventSent When
Article publishedAn article is published
Article createdA new article is created
Article deletedAn article is deleted (payload adds Slugs, one per language)
Article settings savedArticle settings are saved (name used in the docs' sample payload)
Article workflow status updatedAn article's workflow status changes
Extension token deletedAn 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →