Przejdź do treści

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:

  1. verify the signature against the raw body and compare in constant time;
  2. reject the request when |now - timestamp| > 300 seconds (replay protection);
  3. 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).