Webhooks and jobs¶
Use jobs for lists larger than the synchronous batch (POST /v1/judge/batch, up to 10 pages). A job runs
in a background worker; you poll its status or receive a signed webhook when it finishes. Reports are stored
in the job's project and are available through GET /v1/reports/{id}.
API keys need the jobs scope. Jobs are project-scoped: a job from another project returns 404 job_not_found.
Create a job¶
curl -X POST https://<host>/v1/jobs \
-H "Authorization: Bearer gsj_..." -H "Content-Type: application/json" \
-d '{"items": [{"url": "https://example.com/"}, {"text": "Pasted page text"}],
"rules": ["SP-KWS-REPEAT", "QR-INCOHERENT"],
"webhook_url": "https://example.org/hooks/gsj"}'
Body: items (each url, html or text; up to GSPAMJUDGE_JOBS_MAX, default 1000), optional rules,
explain, webhook_url, project_id (or the X-Project-ID header; an API key uses its project).
Response 202 {"job_id": "job_…", "status": "queued"}.
Limits: html/text up to 2,000,000 characters per item and 20,000,000 per job (413 item_too_large); at
most 5 queued or running jobs per project (429 too_many_active_jobs with Retry-After). A job is stopped
after 30 minutes (status: failed, error says it timed out; the webhook is still sent). Rules disabled in
the catalog are rejected up front (400 rule_disabled).
webhook_url must be https with a public host name: IP literals, localhost, internal names and URLs with
credentials are rejected (400 invalid_webhook_url). At delivery time the host is resolved again and every
address must be public; the request goes to the checked address, without following redirects.
Poll the status¶
GET /v1/jobs/{job_id}:
| Field | Meaning |
|---|---|
status |
queued → running → done or failed |
done / total |
items processed so far (with or without error) |
failed |
items that ended with an error; errors[] = {index, error} |
report_ids |
public ids of stored reports (rep_…) in item order (fetch with GET /v1/reports/{id}) |
webhook_status |
HTTP status returned by your webhook (0 = not reached) |
done means every item was processed; some may still have failed (failed > 0). failed means every item
failed or the job crashed (error). GET /v1/jobs/{job_id}/results lists {index, report_id, error, url};
GET /v1/jobs lists recent jobs of your projects. Errors and results carry the item url (when given).
Webhook¶
When a job finishes, the server sends POST webhook_url with JSON {"event": "job.finished", "job": {…}, "id": null}
(job is the same object as GET /v1/jobs/{id}; id is filled only in organization webhooks). Headers:
| Header | Value |
|---|---|
X-GSPAMJUDGE-Event |
job.finished |
X-GSPAMJUDGE-Delivery |
delivery id, the same for all retries of one delivery |
X-GSPAMJUDGE-Timestamp |
Unix time in seconds (new on each retry) |
X-GSPAMJUDGE-Signature |
sha256= + hex HMAC-SHA256 of timestamp + "." + raw body with the webhook secret |
Receivers must:
- verify the signature against the raw body and compare in constant time;
- reject the request when
|now - timestamp| > 300seconds (replay protection); - deduplicate by
X-GSPAMJUDGE-Delivery- a retry after a lost response arrives with the same id.
import hashlib, hmac, time
def valid(secret: bytes, timestamp: str, body: bytes, signature: str) -> bool:
expected = "sha256=" + hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return abs(time.time() - int(timestamp)) <= 300 and hmac.compare_digest(expected, signature)
Return any 2xx quickly. Other responses and network errors are retried 3 times with backoff (1, 2, 4 s).
The secret is set by the server operator (GSPAMJUDGE_WEBHOOK_SECRET); without it, requests with
webhook_url are rejected with 503 webhooks_unavailable. A per-project secret is planned (#101).