Skip to content

Configure webhooks

Webhooks send an HTTP POST to your server when something happens in your Topicary project, for example, when a topic is published. Use them to trigger CI/CD pipelines, sync external systems, or build custom notifications.

Before you begin

This feature requires the Enterprise capability tier (the Business ). During the beta, every project is on the free Solo tier, so this feature is locked by default: go to Settings ▸ Billing and request Enterprise access to unlock it.

Only project members with the Admin can configure this feature. Authors and Reviewers see the section, but its controls are replaced by an Admins only notice.

If you only want a Zap to fire on publish, use Connect Zapier instead: it registers and removes its own webhook when you turn the Zap on and off.

Add a webhook

  1. Go to Settings > Connections and find the Webhooks card.

  2. Click Add webhook. While no endpoint is registered, the button sits under the empty state, Add an endpoint to receive project events.

  3. Enter the endpoint URL, for example https://example.com/hooks/topicary. It must be a public address: private, loopback, and internal hosts are refused with Webhook URL must be a public HTTPS endpoint.

  4. Optionally fill in Description (optional). It is only a label for your own use.

  5. Under Events, choose what to subscribe to. All events is selected by default and is exclusive: clicking any named chip, such as Topic Published, clears it. Chip labels are the event names in title case, so Topic Status Changed is topic.status_changed.

  6. Click Add webhook again to submit the form. The button stays disabled until the URL field has something in it. Cancel discards the form.

The signing secret appears once, in a banner reading Copy this signing secret now. It will not be shown again. Click Copy, store the value with your other credentials, then click Dismiss. The list never shows the secret again, and no read returns it. If you lose it, rotate the secret rather than re-creating the endpoint.

The form refuses a save and says why, inline:

Message

Cause

Enter a valid webhook URL

The text is not a URL

Webhook URL must be a public HTTPS endpoint

The host is private, loopback, or otherwise not reachable from the internet

Select at least one event to subscribe to

Every event chip is cleared

Manage an endpoint

Each row in the Webhooks card shows the endpoint URL, an Active or Disabled badge, the description, one chip per subscribed event (All events for the wildcard), and the date it was created. Four buttons act on it:

Button

What it does

Deliveries

Expands the recent delivery attempts for this endpoint. See Check delivery history

Rotate secret

Issues a new signing secret. See Rotate the signing secret

Disable / Enable

Pauses or resumes deliveries. A disabled endpoint receives neither new deliveries nor retries

Delete

Removes the endpoint after the Delete webhook? dialog, which warns that "Deliveries to this endpoint stop immediately and its delivery history is removed."

The card cannot change an endpoint's URL or its event list. Delete the endpoint and add it again, or send a PATCH over the REST API. Listing and deleting endpoints keep working if the project later drops below the Business plan, so an endpoint that is still receiving events can always be removed.

Rotate the signing secret

  1. Go to Settings > Connections > Webhooks and find the endpoint.

  2. Click Rotate secret.

  3. Read the Rotate signing secret? dialog, which states: "The current secret stops verifying immediately. The new secret is shown once — update your endpoint with it right away."

  4. Click Rotate secret to confirm.

The new secret appears in the same one-time banner as a newly created endpoint. Copy it and update your receiver.

Rotation has no overlap window. Every delivery after it is signed with the new secret only, so a receiver still checking the old one rejects each request. Have the new value ready to deploy before you confirm.

Manage webhooks over the REST API

Everything except secret rotation is also available programmatically. You need an REST API authentication, which only a project Admin can generate.

Send a POST to /api/v1/webhooks with the endpoint URL and the events you want:

curl -X POST https://topicary.com/api/v1/webhooks \
  -H "Authorization: Bearer tk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/topicary",
    "events": ["topic.published", "topic.updated"],
    "description": "Build pipeline"
  }'

The response contains the webhook id and its secret:

{
  "data": {
    "id": "8b1c0f2e-...",
    "url": "https://example.com/hooks/topicary",
    "events": ["topic.published", "topic.updated"],
    "secret": "whsec_9f3c...",
    "is_active": true,
    "description": "Build pipeline",
    "created_at": "2026-09-08T14:30:00Z"
  }
}

Store the secret when the response arrives. Later reads never return it, and the API has no rotation endpoint: rotate from the Webhooks card instead.

Rules the endpoint enforces:

  • The URL must be a public http or https address. Private, loopback, and internal hosts are refused with url must be a public HTTPS endpoint.

  • events must contain at least one valid event type, or the wildcard "*" to subscribe to every event.

  • description is optional and is only a label for your own use.

Task

Request

List all webhooks

GET /api/v1/webhooks

Read one webhook and its recent deliveries

GET /api/v1/webhooks/{id}

Change the URL, events, or description

PATCH /api/v1/webhooks/{id}

Pause deliveries without deleting

PATCH /api/v1/webhooks/{id} with {"is_active": false}

Delete a webhook

DELETE /api/v1/webhooks/{id}

Event types

Event

Fires when

topic.created

A topic is created

topic.updated

A topic's content or metadata changes

topic.published

A topic's status becomes Published from the editor, from a topic's status badge in the topic list, from a bulk status change, or over the REST API

topic.deleted

A topic is deleted, singly or as part of a bulk delete, from the editor, the topic list, or the REST API

topic.status_changed

A topic's status changes (Draft, Review, Published)

component.created

A component is created

component.updated

A component's content changes

component.deleted

A component is deleted

map.published

A publication target is published

map.unpublished

A publication target is unpublished

Subscribe with "events": ["*"] to receive all of them.

Publishing from the editor or over the REST API fires topic.updated, topic.status_changed, and topic.published together. Changing a topic's status badge in the topic list fires topic.status_changed and topic.published only. Subscribe to topic.published alone if you want one delivery per publish.

Bulk actions in the topic list fire one event per topic, not one per batch. A bulk status change sends a topic.status_changed for every topic that moved, plus a topic.published for each when the new status is Published, and no topic.updated. A bulk delete sends one topic.deleted per topic removed. Topics with an active workflow are excluded from a bulk status change and fire nothing, and so is any topic the change did not reach.

Payload format

Every webhook delivery is a POST request with a JSON body:

{
  "event": "topic.status_changed",
  "data": {
    "id": "3f7c2a1e-8b04-4d6f-9a11-2c5e7d0b4f83",
    "status": "published"
  },
  "timestamp": "2026-09-08T14:30:00Z"
}

The data object is deliberately lean: it identifies what changed rather than restating the content. Read the full record over the REST API when you need titles, fields, or body content. The Webhook event reference lists the fields each event carries.

Request headers

Every webhook request includes the following headers:

Header

Value

Content-Type

application/json

X-Topicary-Signature

sha256=<hmac>

X-Topicary-Event

webhook

User-Agent

Topicary-Webhook/1.0

Verify signatures

Every request includes an X-Topicary-Signature header in the format sha256=<hex digest>. The hex digest is an HMAC-SHA256 signature of the raw request body using your webhook's signing secret. Verify this signature to confirm the request came from Topicary:

import crypto from "crypto";

function verifyWebhook(body, signatureHeader, secret) {
  const signature = signatureHeader.replace("sha256=", "");
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Always verify the signature before processing a webhook payload. Without verification, an attacker could send forged requests to your endpoint.

Retry behavior

If your endpoint returns a non-2xx status code or fails to respond within 10 seconds, Topicary retries the delivery. Each webhook is attempted up to 3 times total (the initial delivery plus two retries) with an exponential backoff between attempts:

Attempt

Scheduled after

Initial delivery

Immediately, when the event fires

1st retry

30 seconds after the initial failure

2nd retry

60 seconds after the 1st retry

Retries are processed by a background job, so the actual time between attempts can be slightly longer than the scheduled backoff. After the third failed attempt, the delivery stops being retried.

Redirects are followed, so a receiver that normalizes URLs (trailing slash, www, regional failover) still gets the payload. A paused webhook (is_active: false) receives neither new deliveries nor retries.

Check delivery history

In Settings > Connections > Webhooks, click Deliveries on an endpoint. The pane lists its 10 most recent attempts, newest first, and reads No deliveries yet. until one is made. Each line shows the event name, the HTTP status your endpoint returned or No response, a Delivered or Failed chip, the attempt count as 1 attempt or N attempts, and the time of the first attempt. Click Deliveries again to collapse the pane.

GET /api/v1/webhooks/{id} returns the same 10 most recent deliveries in recent_deliveries. Each entry records:

Field

Meaning

event_type

Which event was delivered

response_status

The HTTP status your endpoint returned, or null when it could not be reached

attempts

How many times the delivery has been tried, up to 3

succeeded

Whether an attempt got a 2xx response

created_at

When the first attempt was made

Delivery history is kept for 7 days, then removed automatically. Copy anything you need for longer-term auditing into your own logs.


See also

Was this page helpful?