Agent skill

Box Webhooks Skill

Receive and verify Box V2 webhooks. Use when setting up Box webhook handlers, debugging BOX-SIGNATURE-PRIMARY verification, or handling events like FILE.UPLOADED, FILE.DOWNLOADED, FILE.TRASHED, FOLDER.CREATED, COMMENT.CREATED, COLLABORATION.CREATED, and SIGN_REQUEST.COMPLETED.

Install this skill

npx skills add hookdeck/webhook-skills --skill box-webhooks


When to Use This Skill

  • How do I receive Box webhooks?
  • How do I verify the BOX-SIGNATURE-PRIMARY header?
  • How do I handle FILE.UPLOADED, FOLDER.CREATED, or SIGN_REQUEST.COMPLETED events?
  • Why is my Box webhook signature verification failing?
  • How do I dedupe Box webhook retries?

This skill covers V2 webhooks (created through the API per file or folder). V1 webhooks are unsigned and Box Sign's own webhooks are separate; neither is covered.

Essential Code (USE THIS)

Box sends two signatures, BOX-SIGNATURE-PRIMARY and BOX-SIGNATURE-SECONDARY. Verify the primary one only:

  • Signature: standard base64 (padded, 44 characters) HMAC-SHA256, no prefix
  • Signed message: the raw request body bytes, then the BOX-DELIVERY-TIMESTAMP header value, with no separator
  • Key: the app's primary signature key, used as a UTF-8 string (not decoded)
const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyBox(rawBody, signature, timestamp, secret) {
  if (!secret || !signature || !timestamp) return false;
  if (!/^[A-Za-z0-9+/]{43}=$/.test(signature)) return false; // validate before comparing
  const signed = Buffer.concat([rawBody, Buffer.from(timestamp, 'latin1')]); // body first, no separator
  const expected = crypto.createHmac('sha256', secret).update(signed).digest('base64');
  const a = Buffer.from(signature, 'latin1'), b = Buffer.from(expected, 'latin1');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// verifyBox(rawBody, req.headers['box-signature-primary'], req.headers['box-delivery-timestamp'], secret)
import base64, hashlib, hmac, re

def verify_box(raw_body: bytes, signature, timestamp, secret: str) -> bool:
    if not secret or not signature or not timestamp or not re.fullmatch(r"[A-Za-z0-9+/]{43}=", signature):
        return False
    signed = b"".join([raw_body, timestamp.encode("latin-1")])  # body first, no separator
    digest = hmac.new(secret.encode(), signed, hashlib.sha256).digest()
    return hmac.compare_digest(signature.encode("ascii"), base64.b64encode(digest))

A missing or empty header, a malformed value, or a mismatch gets 401.

Setup note (same text Hookdeck shows for its Box source): Paste the primary signature key from the Box Developer Console; only the primary signature is verified, so during key rotation paste the key that signs it. Only V2 webhooks are signed.

Timestamp window: Box suggests rejecting a BOX-DELIVERY-TIMESTAMP (RFC 3339) older than 10 minutes. The examples leave that off by default, because Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck itself applies no tolerance. Set BOX_WEBHOOK_TOLERANCE_SECONDS to a positive number (for example 600) to opt in; unset or 0 is off. Box's own SDK helper (WebhookValidator) always enforces the 10 minutes and checks both keys, which is why the examples verify by hand. See references/verification.md.

HTTP method: Box's docs don't name the delivery method; they say only that "Box makes an HTTP call to the address". The examples accept GET, POST, PUT, PATCH and DELETE and verify the signature first for every method, treating a missing body as empty bytes. Hookdeck's Box source pre-selects POST without managing the method, so any other method the sender uses must also be allowed on the source.

Dedupe on id: the body's id is the same across Box's retries; BOX-DELIVERY-ID changes on every retry, so don't use it.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Common Event Types

The body is {type: "webhook_event", id, created_at, trigger, webhook, created_by, source, additional_info}. Dispatch on trigger:

TriggerDescription
FILE.UPLOADEDA file is uploaded or moved to the folder
FILE.DOWNLOADEDA file is downloaded
FILE.TRASHEDA file is moved to trash
FILE.DELETEDA file is permanently deleted
FOLDER.CREATEDA folder is created
COMMENT.CREATEDA comment is created
COLLABORATION.CREATEDA collaboration is created
SHARED_LINK.CREATEDA shared link is created
METADATA_INSTANCE.UPDATEDA metadata template instance is updated
SIGN_REQUEST.COMPLETEDA Box Sign request is completed

Box documents 46 triggers in all, including file, folder, comment, collaboration, metadata, shared-link, task, Box Sign and Doc Gen events. See references/overview.md.

For the full event reference, see Box webhook triggers.

Important Headers

HeaderDescription
BOX-SIGNATURE-PRIMARYBase64 HMAC-SHA256 made with the primary signature key (the one verified)
BOX-SIGNATURE-SECONDARYSame, made with the secondary key (ignored)
BOX-DELIVERY-TIMESTAMPRFC 3339 send time; part of the signed message
BOX-DELIVERY-IDUnique per delivery attempt; changes on retry

Environment Variables

BOX_WEBHOOK_SECRET=your_primary_signature_key   # Developer Console → Webhooks → Manage signature keys
BOX_WEBHOOK_TOLERANCE_SECONDS=0   # optional; a positive value opts in to the replay window

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 box --path /webhooks/box

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →