Skip to main content
Webhooks push event notifications to your server when jobs complete, inputs are ready, or other events occur. Unlike WebSockets, webhooks don’t require a persistent connection.

Creating a Webhook

from comfy_cloud import ComfyCloudClient

client = ComfyCloudClient(api_key="comfyui-...")

with client:
    webhook = client.webhooks.create(
        url="https://your-server.com/comfy-webhook",
        events=["job.completed", "job.failed"],
    )
    
    # Save the secret for signature verification
    print(f"Webhook ID: {webhook.id}")
    print(f"Secret: {webhook.secret}")  # Only shown on create!
The webhook secret is only returned when creating the webhook or rotating the secret. Store it securely - you’ll need it to verify signatures.

Event Types

EventDescription
job.completedJob finished successfully
job.failedJob failed with error
job.cancelledJob was cancelled
job.*All job events
input.readyInput file processed
input.failedInput upload failed
input.*All input events
model.readyModel upload complete
model.failedModel upload failed
model.*All model events
archive.readyArchive ZIP ready
archive.failedArchive creation failed
archive.*All archive events
account.low_balanceAccount balance is low
*All events

Webhook Payload

{
  "event": "job.completed",
  "delivery_id": "dlv_abc123",
  "data": {
    "id": "job_xyz789",
    "status": "completed",
    "outputs": [
      {
        "id": "out_123",
        "type": "image",
        "download_url": "https://storage.comfy.org/..."
      }
    ],
    "tags": ["my-app", "batch-1"],
    "created_at": "2024-01-15T10:00:00Z",
    "completed_at": "2024-01-15T10:00:45Z"
  }
}

Signature Verification

All webhooks are signed with HMAC-SHA256. Always verify signatures to ensure requests are from ComfyUI Cloud.

Headers

HeaderDescription
X-Comfy-Signature-256HMAC-SHA256 signature
X-Comfy-TimestampUNIX timestamp (seconds)

Python Verification

from comfy_cloud.helpers import verify_webhook_signature, WebhookVerificationError

@app.post("/comfy-webhook")
async def handle_webhook(request: Request):
    payload = await request.body()
    signature = request.headers.get("X-Comfy-Signature-256")
    timestamp = request.headers.get("X-Comfy-Timestamp")
    
    try:
        verify_webhook_signature(
            payload=payload,
            signature=signature,
            timestamp=timestamp,
            secret=WEBHOOK_SECRET,
        )
    except WebhookVerificationError as e:
        raise HTTPException(400, str(e))
    
    # Process the webhook
    data = await request.json()
    if data["event"] == "job.completed":
        job_id = data["data"]["id"]
        outputs = data["data"]["outputs"]
        # Handle completed job...
    
    return {"status": "ok"}

Parse and Verify Together

from comfy_cloud.helpers import parse_webhook

@app.post("/comfy-webhook")
async def handle_webhook(request: Request):
    payload = await request.body()
    
    webhook = parse_webhook(
        payload=payload,
        signature=request.headers.get("X-Comfy-Signature-256"),
        timestamp=request.headers.get("X-Comfy-Timestamp"),
        secret=WEBHOOK_SECRET,
    )
    
    print(f"Event: {webhook.event}")
    print(f"Data: {webhook.data}")
    print(f"Timestamp: {webhook.timestamp}")
    
    return {"status": "ok"}

Manual Verification

The signature is computed as:
HMAC-SHA256(secret, timestamp + "." + body)
Example in Python without the SDK:
import hmac
import hashlib
import time

def verify_signature(payload: bytes, signature: str, timestamp: str, secret: str):
    # Check timestamp is recent (prevent replay attacks)
    ts = int(timestamp)
    if abs(time.time() - ts) > 300:  # 5 minute tolerance
        raise ValueError("Timestamp too old")
    
    # Compute expected signature
    signed_payload = f"{timestamp}.".encode() + payload
    expected = hmac.new(
        secret.encode(),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()
    
    # Constant-time comparison
    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid signature")

Managing Webhooks

List Webhooks

webhooks = client.webhooks.list()
for wh in webhooks.webhooks:
    print(f"{wh.id}: {wh.url} -> {wh.events}")

Rotate Secret

If your secret is compromised:
webhook = client.webhooks.rotate_secret(webhook_id)
new_secret = webhook.secret  # Update your server with this

Delete Webhook

client.webhooks.delete(webhook_id)

Retry Policy

Failed deliveries are retried with exponential backoff:
AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
Webhooks are considered failed if:
  • Your server returns a non-2xx status code
  • Connection timeout (30 seconds)
  • DNS resolution fails

Best Practices

Never trust webhook payloads without verifying the signature. This prevents attackers from spoofing events.
Return a 2xx response within 30 seconds. Do heavy processing asynchronously after acknowledging receipt.
Use delivery_id to deduplicate. Retries may cause the same event to be delivered multiple times.
Always use HTTPS endpoints. HTTP webhooks are rejected in production.

Per-Job Webhooks

You can also specify a webhook URL when creating a job:
job = client.jobs.create(
    workflow={...},
    webhook_url="https://your-server.com/job-webhook",
)
This webhook receives events only for that specific job, using your account’s default webhook secret.