ModaDocs

Webhooks

Receive HTTP notifications when AI design jobs complete.

When you start a design job via POST /jobs or POST /remix, you can provide a callback_url to receive an HTTP notification when the job finishes. This lets you avoid polling GET /jobs/{job_id} and instead react to completion events asynchronously.

Setting up a webhook

Include a callback_url when starting a job:

curl -X POST https://api.moda.app/v1/jobs \
  -H "Authorization: Bearer moda_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Create a modern SaaS landing page",
    "callback_url": "https://your-server.com/webhooks/moda"
  }'

When the job reaches a terminal state (completed, failed, or cancelled), Moda sends a POST request to your callback_url.

Webhook payload

The payload is a JSON object with the following fields:

{
  "event": "job.completed",
  "job_id": "990e8400-e29b-41d4-a716-446655440000",
  "canvas_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "task": "Create a modern SaaS landing page",
  "ops_count": 42,
  "timestamp": "2025-03-15T10:32:15Z",
  "attempt_count": 1,
  "error": null
}
FieldTypeDescription
eventstringEvent type: job.completed, job.failed, or job.cancelled
job_idstringJob UUID
canvas_idstringCanvas that was created or edited
statusstringTerminal status: completed, failed, or cancelled
taskstringThe original prompt
ops_countintegerTotal canvas operations applied
timestampstringEvent timestamp (ISO 8601)
attempt_countintegerNumber of job execution attempts
errorstring | nullError message if the job failed, otherwise null

Verifying webhook signatures

Every webhook request includes two headers for signature verification:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature in the format v1=<hex>
X-Webhook-TimestampUnix timestamp (seconds) when the webhook was sent

Each API key has its own unique webhook signing secret, shown once when the key is created. You can find it in the credentials banner alongside your API key in Settings > Developer > REST API. Store it securely -- it cannot be retrieved later.

The signature is computed over the string {timestamp}.{request_body} using HMAC-SHA256. To verify:

  1. Extract the timestamp from the X-Webhook-Timestamp header
  2. Concatenate: {timestamp}.{raw_request_body}
  3. Compute HMAC-SHA256(signing_secret, concatenated_string)
  4. Compare the result with the signature value after the v1= prefix

Verification example (Node.js)

import crypto from 'node:crypto';

function verifyWebhook(signingSecret, requestBody, signatureHeader, timestampHeader) {
  const message = `${timestampHeader}.${requestBody}`;
  const expected = crypto.createHmac('sha256', signingSecret).update(message).digest('hex');
  const received = signatureHeader.replace('v1=', '');

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}

Verification example (Python)

import hashlib
import hmac

def verify_webhook(
    signing_secret: str,
    request_body: bytes,
    signature_header: str,
    timestamp_header: str,
) -> bool:
    message = f"{timestamp_header}.{request_body.decode()}"
    expected = hmac.new(
        signing_secret.encode(), message.encode(), hashlib.sha256
    ).hexdigest()
    received = signature_header.removeprefix("v1=")

    return hmac.compare_digest(expected, received)

Retry behavior

If your endpoint returns a non-2xx status code or does not respond within 30 seconds, Moda retries the webhook up to 3 times with exponential backoff:

AttemptDelay
1st retry1 second
2nd retry5 seconds
3rd retry30 seconds

After all retries are exhausted, the webhook is dropped. You can still retrieve the job status via GET /jobs/{job_id}.

Best practices

  • Return 200 quickly. Process the webhook payload asynchronously to avoid timeouts. Acknowledge receipt first, then handle the event.
  • Verify signatures. Always validate the X-Webhook-Signature header before trusting the payload.
  • Check timestamps. Reject webhooks with timestamps more than 5 minutes old to prevent replay attacks.
  • Handle duplicates. In rare cases, the same event may be delivered more than once. Use job_id as an idempotency key.
  • Use HTTPS. Your callback_url must use HTTPS to protect the payload in transit.

On this page