Documentation Index
Fetch the complete documentation index at: https://docs.topify.ai/llms.txt
Use this file to discover all available pages before exploring further.
Webhooks let you receive real-time notifications when action workflows reach checkpoints or complete. They are required for the execute workflow used by new_content actions.
Register webhook
Register a new webhook endpoint to receive event notifications.
Request body
| Field | Type | Required | Description |
|---|
url | string | Yes | HTTPS endpoint URL that will receive webhook events |
events | string[] | Yes | Array of event types to subscribe to |
Valid events
| Event | Description |
|---|
action.checkpoint | Workflow paused at an approval checkpoint (outline, draft, or final review) |
action.completed | Workflow finished successfully |
action.failed | Workflow encountered an error |
curl -X POST "https://topify-customer-api-production.up.railway.app/api/public/v1/webhooks" \
-H "X-API-Key: tk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/topify",
"events": ["action.checkpoint", "action.completed", "action.failed"]
}'
resp = client.post("/webhooks", json={
"url": "https://example.com/webhooks/topify",
"events": ["action.checkpoint", "action.completed", "action.failed"],
})
const resp = await fetch(`${BASE}/webhooks`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example.com/webhooks/topify",
events: ["action.checkpoint", "action.completed", "action.failed"],
}),
});
Response (201 Created)
{
"success": true,
"data": {
"id": "wh_a1b2c3d4-...",
"url": "https://example.com/webhooks/topify",
"events": ["action.checkpoint", "action.completed", "action.failed"],
"is_active": true,
"secret": "whsec_k7m9p2...",
"created_at": "2026-04-08T12:00:00Z"
}
}
The secret field is only returned once at creation time. Store it securely — you will need it to verify webhook signatures.
Error responses
| Status | Cause |
|---|
400 | Missing url, non-HTTPS URL, empty events array, or invalid event type |
403 | API key has read-only access |
409 | A webhook with this URL already exists |
List webhooks
Returns all registered webhooks for your account.
curl "https://topify-customer-api-production.up.railway.app/api/public/v1/webhooks" \
-H "X-API-Key: tk_live_..."
resp = client.get("/webhooks")
const resp = await fetch(`${BASE}/webhooks`, { headers });
Response
{
"success": true,
"data": [
{
"id": "wh_a1b2c3d4-...",
"url": "https://example.com/webhooks/topify",
"events": ["action.checkpoint", "action.completed", "action.failed"],
"is_active": true,
"created_at": "2026-04-08T12:00:00Z"
}
]
}
The secret field is not included in list responses. It is only returned when the webhook is first created.
Delete webhook
DELETE /webhooks/{webhook_id}
Permanently remove a webhook. Events will no longer be delivered to the endpoint.
Path parameters
| Param | Type | Description |
|---|
webhook_id | string | The webhook ID |
curl -X DELETE "https://topify-customer-api-production.up.railway.app/api/public/v1/webhooks/{webhook_id}" \
-H "X-API-Key: tk_live_..."
resp = client.delete(f"/webhooks/{webhook_id}")
const resp = await fetch(`${BASE}/webhooks/${webhookId}`, {
method: "DELETE",
headers,
});
Response
{
"success": true,
"data": {
"id": "wh_a1b2c3d4-...",
"message": "Webhook deleted successfully"
}
}
Error responses
| Status | Cause |
|---|
403 | API key has read-only access |
404 | Webhook not found |
When an event fires, Topify sends a POST request to your registered URL with the following JSON body.
action.checkpoint payload
{
"event": "action.checkpoint",
"timestamp": "2026-04-08T21:19:13.463Z",
"data": {
"action_id": "a1b2c3d4-...",
"workflow_id": "w1x2y3z4-...",
"checkpoint": "outline",
"content": "## Article Outline\n\n1. Introduction to project management tools...",
"options": [
{ "action": "approve", "label": "Approve & Write" },
{ "action": "edit", "label": "Edit Outline" },
{ "action": "regenerate", "label": "Regenerate" },
{ "action": "reject", "label": "Cancel" }
]
}
}
action.completed payload
{
"event": "action.completed",
"timestamp": "2026-04-08T21:25:00.000Z",
"data": {
"action_id": "a1b2c3d4-...",
"workflow_id": "w1x2y3z4-...",
"result": {
"title": "Best Project Management Tools in 2026",
"content": "..."
}
}
}
action.failed payload
{
"event": "action.failed",
"timestamp": "2026-04-08T21:22:00.000Z",
"data": {
"action_id": "a1b2c3d4-...",
"workflow_id": "w1x2y3z4-...",
"error": "LLM generation timed out after 120 seconds"
}
}
Payload fields
| Field | Type | Description |
|---|
event | string | Event type (action.checkpoint, action.completed, action.failed) |
timestamp | string | ISO 8601 timestamp of when the event occurred |
data.action_id | string (UUID) | The action that triggered the event |
data.workflow_id | string (UUID) | The active workflow ID |
data.checkpoint | string | Checkpoint name (only for action.checkpoint) |
data.content | string | Content to review (only for action.checkpoint) |
data.options | object[] | Available response actions (only for action.checkpoint) |
data.result | object | Final output (only for action.completed) |
data.error | string | Error message (only for action.failed) |
Verifying signatures
Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature of the raw request body, computed using the webhook secret returned at creation time.
Always verify this signature before processing the payload to ensure requests are authentic.
import hmac
import hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your webhook handler:
signature = request.headers["X-Webhook-Signature"]
is_valid = verify_webhook(request.body, signature, WEBHOOK_SECRET)
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(body, signature, secret) {
const expected = createHmac("sha256", secret)
.update(body)
.digest("hex");
return timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
// In your webhook handler:
const signature = req.headers["x-webhook-signature"];
const isValid = verifyWebhook(req.rawBody, signature, WEBHOOK_SECRET);
Your endpoint should return a 200 status code within 10 seconds. If your endpoint fails to respond or returns a non-2xx status, Topify will retry up to 3 times with exponential backoff.