Skip to main content

Webhooks

EVENTSwebhooks:managev3.2.0STABLE

Webhooks

Webhooks let TechWriter push real-time notifications to your server the moment something happens — a document is published, a comment is posted, a deployment fails. Instead of polling the API for changes, you register an HTTPS endpoint and TechWriter delivers a signed JSON payload within milliseconds of each event.

Every delivery includes an HMAC-SHA256 signature so you can cryptographically confirm the payload came from TechWriter and hasn't been tampered with. The SDK ships a WebhookVerifier helper that handles the constant-time comparison and replay-window check for you.

Pick your language on the right to see the full flow: registering an endpoint, verifying signatures, and dispatching events. All snippets assume your signing secret is in the TW_WEBHOOK_SECRET environment variable.

Event catalog

Subscribe to any combination of these events when you register a webhook. Use ["*"] to receive everything.

Event typeResourceWhen it fires & notes
document.publishedDocumentA document transitions to Published status. Fired once per publish — not on re-publishes unless content changed.
document.updatedDocumentA draft or published document is saved. High-frequency. Consider filtering on data.object.status.
document.archivedDocumentA document is moved to the archive. The document is soft-deleted. Content is still readable via API.
document.deletedDocumentA document is permanently deleted. Hard delete. Content cannot be recovered after this event.
comment.createdCommentA new inline or document-level comment is posted. Includes author_id, body, and optional mention list.
comment.resolvedCommentA comment thread is marked resolved. data.object.resolved_by contains the resolver's user ID.
project.createdProjectA new project (workspace root) is created. Useful for provisioning downstream resources automatically.
project.deletedProjectA project and all its documents are deleted. Cascade — listen for this if you mirror project structure.
user.invitedUserA new user is invited to the workspace. Fires before the user accepts. data.object.status = pending.
user.role_changedUserA user's role or group membership changes. Old and new role are both included in data.object.
deployment.createdDeploymentA new portal deployment is triggered. Use this to kick off downstream CDN invalidations.
deployment.failedDeploymentA portal deployment fails to complete. data.object.reason contains a human-readable failure summary.

Delivery headers

Every POST request TechWriter sends includes these headers. Verify the signature first — then use the others for routing and idempotency.

HeaderFormat / exampleDescription
X-TechWriter-Signaturet=<unix>,v1=<hex>Timestamp and HMAC-SHA256 of the raw payload. Always verify this before processing.
X-TechWriter-Eventdocument.publishedThe event type string. Identical to event.type in the body — useful for routing in proxies.
X-TechWriter-Deliveryevt_01HW8N4Z3R8C9XUnique delivery ID for this attempt. Stable across retries — use it for idempotency checks.
X-TechWriter-Version2026-04-01Payload schema version. Pin to this in your handler to avoid surprise breaks on schema updates.
Content-Typeapplication/jsonAlways JSON. Read the raw body bytes before parsing — signature verification requires the raw form.
User-AgentTechWriter-Webhook/1.0Identifies TechWriter as the sender. Use in firewall rules to allow-list inbound requests.

Retry schedule

TechWriter retries on any non-2xx response or connection timeout. The delivery ID stays constant across all attempts — use it as your idempotency key.

AttemptWait before retryTriggerNotes
1stImmediateNon-2xx or connection refusedFirst delivery attempt fires as soon as the event is emitted.
2nd5 secondsNon-2xx or timeout on attempt 1Short delay — catches brief restarts and cold-start latency.
3rd30 secondsNon-2xx or timeout on attempt 2After this point the event is considered degraded.
4th5 minutesNon-2xx or timeout on attempt 3Long pause — allows maintenance windows to complete.
5th30 minutesNon-2xx or timeout on attempt 4Final attempt. Failure triggers a webhook.delivery_failed notification.

Webhook configuration

Options accepted by tw.webhooks.create() and the REST endpoint. All names are consistent across every SDK language.

OptionTypeRequiredDescription
urlstringrequiredHTTPS endpoint that receives POST requests. Plain HTTP is rejected at registration.
eventsstring[]requiredOne or more event types to subscribe to. Pass ["*"] to receive all events in the workspace.
secretstringrequiredSigning secret used to generate the X-TechWriter-Signature header. Store alongside your API key.
activebooleanoptionalWhether deliveries are enabled. Defaults to true. Set to false to pause without deleting.
descriptionstringoptionalA human-readable label shown in the dashboard. Useful when you run multiple endpoints.
metadataobjectoptionalUp to 16 arbitrary key-value pairs for your own bookkeeping. Not included in the payload.
timeoutnumberoptionalResponse timeout in seconds. Default 30. Increase for handlers that fan out to slow downstream services.

Security checklist

Webhooks are an inbound attack surface. These seven rules cover the most common failure modes seen in production integrations.

RuleWhy it matters
Always verify the signatureAny server on the internet can POST to your endpoint. An unverified handler processes attacker-crafted data.
Reject replayed events (|t − now| > 5 min)A valid but old payload can trigger duplicate side-effects. The timestamp in the signature protects against replay.
Return 200 before processingRespond immediately so TechWriter doesn't time out and retry. Process the event asynchronously in a background job.
Use X-TechWriter-Delivery for idempotencyRetries send the same delivery ID. Store it in a seen-events set and skip duplicates to stay idempotent.
Rotate secrets without downtimeThe dashboard supports two active secrets during rotation. Accept both, then revoke the old one after deploys.
Never 500 on unknown event typesTechWriter will retry on non-2xx. Unknown types should be logged and acknowledged with 200 to avoid retry storms.
Scope the webhooks:manage permission tightlyThe scope that creates webhooks can also read signing secrets. Restrict it to the service that owns the endpoint.

Where to go next

Was this section helpful?

1. Register a webhook

import { TechWriterClient } from ;

const tw = new TechWriterClient({ apiKey: process.env.TECHWRITER_API_KEY });

const webhook = await tw.webhooks.create({
  url: ,
  events: [, , ],
  secret: process.env.TW_WEBHOOK_SECRET,
  active: true
});

console.log(, webhook.id);

2. Verify the signature

import { WebhookVerifier } from ;
import express from ;

const app = express();
const verifier = new WebhookVerifier(process.env.TW_WEBHOOK_SECRET);

"color: ">#8b949e">// Use raw body — JSON.parset verify correctly.
app.post(, express.raw({ type:  }), (req, res) => {
  try {
    const event = verifier.constructEvent(
      req.body,                                   "color: ">#8b949e">// raw Buffer
      req.headers[]       "color: ">#8b949e">// "t=...,v1=..."
    );
    "color: ">#8b949e">// event is verified — safe to process
    handleEvent(event);
    res.sendStatus(200);
  } catch (err) {
    console.error(, err.message);
    res.sendStatus(400);
  }
});

3. Handle events

async function handleEvent(event) {
  switch (event.type) {
    case : {
      const doc = event.data.object;
      await searchIndex.upsert({ id: doc.id, title: doc.title });
      break;
    }
    case : {
      await searchIndex.delete(event.data.object.id);
      break;
    }
    case : {
      const { authorId, body } = event.data.object;
      await notifyMentions(authorId, body);
      break;
    }
    default:
      "color: ">#8b949e">// Unrecognised event — log and ignore, never 500.
      console.log(, event.type);
  }
}

Pro tip

Respond with 200 immediately, then process the event in a background job. If your handler calls a slow downstream service synchronously, TechWriter will time out at 30 s and retry — leading to duplicate processing and retry storms during outages.