Overview
Push, not poll
Register an HTTPS endpoint and Inksong will POST signed JSON to it every time one of your documents completes or fails. Manage your webhook subscriptions in your dashboard.
One webhook URL receives all event types. Per-event filtering and workspace-scoped webhooks aren’t supported yet — tell us at hello@inksong.app if you need them.
Subscribing
Create a webhook
Easiest: /dashboard/webhooks — click Add webhook, paste your URL, copy the signing secret (shown once). Via the API:
curl -X POST https://api.inksong.app/api/v1/webhooks \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/inksong-hook","description":"primary"}'The response includes signing_secretin plaintext — only on the create response. Subsequent reads return a masked value. Rotate via POST /webhooks/{id}/rotate-secret.
HTTPS is required in production. Private addresses (loopback, RFC1918, link-local) are rejected.
Events
Payload shape
Two event types ship today — document.completed and document.failed. Every event has the same envelope:
{
"id": "evt_2f8b3c4a1e9d2c6f7e0a5b8c",
"type": "document.completed",
"created_at": "2026-05-17T12:34:56.123456+00:00",
"data": {
"document": {
"job_id": "abc123",
"original_filename": "draft.docx",
"status": "completed",
"humanized_ai_score": 18.0,
"original_ai_score": 78.0,
"processing_time_ms": 4231
}
}
}document.failed swaps scores for an error string.
Signing
Verify the signature
Every request has a Webhook-Signature header of the form:
Webhook-Signature: t=1700000000,v1=<hmac_sha256_hex>
The signed payload is f"{t}.{raw_body}" (concatenate the timestamp, a dot, and the unparsed request body), HMAC‑SHA256-keyed by the webhook’s signing secret. Verify by recomputing and comparing in constant time. Reject signatures whose t is more than five minutes off your clock to block replays.
Python (using the SDK)
from inksong import verify_signature
if not verify_signature(WEBHOOK_SECRET, request.body, request.headers["Webhook-Signature"]):
return Response(status_code=400)Node (using the SDK)
import { verifyWebhookSignature } from "@epigrams/inksong-sdk";
const ok = await verifyWebhookSignature(
process.env.INKSONG_WEBHOOK_SECRET,
rawBody,
req.headers["webhook-signature"],
);
if (!ok) return res.status(400).end();Retries
Delivery semantics
Inksong attempts delivery up to four times: the initial attempt plus three retries at 1, 5, and 30 second backoff intervals. Maximum wall-clock per delivery: ~36 seconds. Any 2xx response is treated as success. After the final failure, the delivery is marked failed and recorded in the log; we do not retry beyond that window.
Recommendation: make your receiver idempotent on event.id. If you receive the same event id twice, ignore the second one.
Delivery log
See what happened
/dashboard/webhooks shows recent deliveries for each subscription: event type, attempt count, HTTP status code from your receiver, and whether we ultimately succeeded or failed. The same data is available at GET /api/v1/webhooks/{id}/deliveries.