DraftFilterDeveloper API

Developer reference

Webhooks

Score and rewrite jobs accept an optional webhookUrl on creation (POST /v1/score-jobs, POST /v1/rewrite-jobs). When the job reaches a terminal succeeded or failed state, DraftFilter posts a signed, metadata-only notification to that URL. The webhook never carries customer text or results; fetch results from the job endpoints (or a results-included listing) after the notification arrives.

Requirements and behavior:

text
webhookUrl: https required, must be a public host
events: job.succeeded, job.failed (cancellations are not delivered)
retries: up to 3 attempts per event with backoff (1s, then 5s) on non-2xx or errors
timeout: about 10 seconds per attempt
delivery config is not part of the idempotent request identity

The webhook URL must resolve to a public address. URLs that target loopback, private, link-local, or cloud-metadata addresses are rejected at request time, and the host is re-resolved and re-checked at delivery time so a name cannot be rebound to an internal address after acceptance.

Signing Secret

Each job gets its own signing secret. It is returned exactly once as webhookSecret in the job creation response and is never shown again, stored, or logged. Replaying the same idempotencyKey returns the same job and the same secret.

text
POST /v1/score-jobs
{"items": [...], "webhookUrl": "https://example.com/hooks/draftfilter"}

202 Accepted
{"jobId": "job_000042", "state": "queued", "webhookSecret": "whsec_..."}

Payload

The request body is JSON metadata about the terminal state:

text
{
  "event": "job.succeeded",
  "jobId": "job_000042",
  "kind": "score",
  "state": "succeeded",
  "itemCount": 3,
  "resultSummary": {"succeededItems": 3, "failedItems": 0},
  "errorSummary": null,
  "createdAt": "2026-07-08T00:00:00Z",
  "completedAt": "2026-07-08T00:00:12Z"
}

Failed jobs use "event": "job.failed", a null resultSummary, and a public errorSummary with code, message, and retryable.

Headers on every delivery:

text
Content-Type: application/json
X-DraftFilter-Event: job.succeeded | job.failed
X-DraftFilter-Signature: t=<unix seconds>,v1=<hex hmac-sha256>

Verifying Signatures

The signature is an HMAC-SHA256 over "{t}." + raw request body, keyed with the job's webhookSecret. Always verify against the raw body bytes before parsing JSON, compare digests in constant time, and reject stale timestamps to prevent replay:

python
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300

def verify_draftfilter_webhook(secret: str, signature_header: str, body: bytes) -> bool:
    parts = dict(part.split("=", 1) for part in signature_header.split(",") if "=" in part)
    timestamp, provided = parts.get("t"), parts.get("v1")
    if timestamp is None or provided is None or not timestamp.isdigit():
        return False
    if abs(int(time.time()) - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    expected = hmac.new(
        secret.encode("ascii"),
        f"{timestamp}.".encode("ascii") + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, provided)

Respond with any 2xx status to acknowledge delivery. Non-2xx responses and connection errors are retried with backoff; after the final attempt the event is dropped (the job itself is unaffected and remains fetchable).