← Back to Dashboard

Webhooks

Receive real-time event notifications via HTTP callbacks.

Instead of polling for job completion, register a webhook URL and SymageDocs will POST a signed JSON payload whenever a subscribed event fires.

Events

EventFires when
job.completedA generation job finishes successfully (includes presigned download URLs)
job.failedA generation job fails
batch.completedA batch generate request completes (includes per-item download URLs)
batch.exhaustedA batch's token budget is fully consumed

Register a Webhook

POST /api/v1/webhooks Register a new webhook endpoint session

Request body:

FieldTypeRequiredDescription
urlstringyesHTTPS endpoint to receive events (max 2,048 chars)
eventsstring[]noEvent types to subscribe to (default: all three)
descriptionstringnoOptional label (max 500 chars)
curl -X POST https://symagedocs.ai/api/v1/webhooks \
  -H "x-user-id: YOUR_USER_ID" \
  -H "Content-Type: application/json" \
  -d '{{"url": "https://your-server.com/hooks", "events": ["job.completed", "job.failed"]}}'
Save the signing secret!The signing_secret is returned once at creation. Store it securely — you need it to verify payload signatures.

Manage Webhooks

GET /api/v1/webhooks List your webhooks (never includes the full secret) session
PATCH /api/v1/webhooks/{{webhook_id}} Update URL, events, description, or active status session
DELETE /api/v1/webhooks/{{webhook_id}} Disable a webhook (soft-delete) session

Inline Webhook Registration

Instead of pre-registering webhooks, pass a webhook_url parameter when creating a job or generating batch items. A temporary webhook subscription is auto-created for the relevant events.

curl -X POST https://symagedocs.ai/api/v1/generate \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{{"form_id": "irs_w2_2025", "quantity": 10, "webhook_url": "https://your-server.com/hooks"}}'

Payload Format

Every delivery is a POST with a JSON body. job.completed and batch.completed payloads include presigned download URLs:

{{
  "event": "job.completed",
  "timestamp": "2026-03-30T12:05:00+00:00",
  "webhook_id": "550e8400-...",
  "data": {{
    "job_id": "661e9500-...",
    "status": "completed",
    "form_id": "irs_w2_2025",
    "quantity": 100,
    "credits_charged": 2000,
    "completed_at": "2026-03-30T12:05:00+00:00",
    "download_urls": [
      {{"filename": "irs_w2_2025_abc123.pdf", "url": "https://s3...presigned"}},
      {{"filename": "irs_w2_2025_abc123.json", "url": "https://s3...presigned"}}
    ]
  }}
}}

Headers

HeaderDescription
X-Symagedocs-SignatureHMAC-SHA256 hex digest of the raw request body
X-Symagedocs-EventEvent type (e.g., job.completed)
X-Symagedocs-DeliveryUnique delivery ID for deduplication

Verifying Signatures

Verify the X-Symagedocs-Signature header to ensure the payload is authentic and untampered:

import hashlib, hmac

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# In your handler:
body = request.body  # raw bytes
sig = request.headers["X-Symagedocs-Signature"]
if not verify_webhook(body, sig, WEBHOOK_SECRET):
    return Response("Invalid signature", status=401)
const crypto = require("crypto");

function verifyWebhook(body, signature, secret) {{
  const expected = crypto.createHmac("sha256", secret)
    .update(body).digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(signature)
  );
}}

// In your handler:
const body = await request.text();
const sig = request.headers.get("X-Symagedocs-Signature");
if (!verifyWebhook(body, sig, WEBHOOK_SECRET)) {{
  return new Response("Invalid signature", {{ status: 401 }});
}}

Retry Policy

Failed deliveries (non-2xx response or network error) are retried with exponential backoff:

AttemptDelay after failure
1st retry10 seconds
2nd retry60 seconds
3rd retry5 minutes

After all retries are exhausted the delivery is marked as failed. Your webhook remains active and continues receiving future events.

TipReturn a 2xx response quickly (within 10 seconds). Do heavy processing asynchronously after acknowledging receipt.

Best Practices