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
}| Field | Type | Description |
|---|---|---|
event | string | Event type: job.completed, job.failed, or job.cancelled |
job_id | string | Job UUID |
canvas_id | string | Canvas that was created or edited |
status | string | Terminal status: completed, failed, or cancelled |
task | string | The original prompt |
ops_count | integer | Total canvas operations applied |
timestamp | string | Event timestamp (ISO 8601) |
attempt_count | integer | Number of job execution attempts |
error | string | null | Error message if the job failed, otherwise null |
Verifying webhook signatures
Every webhook request includes two headers for signature verification:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature in the format v1=<hex> |
X-Webhook-Timestamp | Unix 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:
- Extract the timestamp from the
X-Webhook-Timestampheader - Concatenate:
{timestamp}.{raw_request_body} - Compute
HMAC-SHA256(signing_secret, concatenated_string) - 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:
| Attempt | Delay |
|---|---|
| 1st retry | 1 second |
| 2nd retry | 5 seconds |
| 3rd retry | 30 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-Signatureheader 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_idas an idempotency key. - Use HTTPS. Your
callback_urlmust use HTTPS to protect the payload in transit.