API Docs

Webhooks, in practice

Push notifications for document.completed and document.failed. Signed with HMAC-SHA256. Retried on failure.

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.