Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.moda.app/llms.txt

Use this file to discover all available pages before exploring further.

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

Setting up a webhook

Include a callback_url when starting a task:
curl -X POST https://api.moda.app/v1/tasks \
  -H "Authorization: Bearer moda_live_abc123..." \
  -H "Moda-Version: 2026-05-01" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Create a modern SaaS landing page",
    "callback_url": "https://your-server.com/webhooks/moda"
  }'
When the task reaches a terminal state (succeeded, failed, or canceled), Moda sends a POST request to your callback_url.

Event types

Closed set. Moda only fires webhooks for the event types below. Anything else your integration sees is a bug and should be reported.
EventWhen it fires
task.succeededA design task finished successfully
task.failedA design task failed (transient or dead-lettered — see data.error.retryable)
task.canceledA design task was canceled
export.succeededAn async canvas export finished successfully
export.failedAn async canvas export failed
Non-terminal states (queued, running, expired) do not fire webhooks today. If you need running/progress beats, poll GET /tasks/{task_id} or open an issue — we’ll expand the enum on concrete customer need.

Webhook payload

The payload is a JSON event envelope wrapping the canonical Task object under a data key. Same shape for every event type.
{
  "id": "evt_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
  "type": "task.succeeded",
  "created": "2026-04-15T12:01:00+00:00",
  "api_version": "2026-05-01",
  "data": {
    "id": "task_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
    "kind": "design",
    "status": "succeeded",
    "created_at": "2026-04-15T12:00:00+00:00",
    "started_at": "2026-04-15T12:00:02+00:00",
    "completed_at": "2026-04-15T12:01:00+00:00",
    "progress": null,
    "attempt": 1,
    "max_attempts": 3,
    "input": { "prompt": "Create a modern SaaS landing page" },
    "result": {
      "canvas_id": "cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
      "canvas_url": "https://app.moda.com/canvas/..."
    },
    "error": null,
    "credits": { "credits_used": 5, "credits_remaining": 12 },
    "links": {
      "self": "/v1/tasks/task_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
      "events": null,
      "cancel": null,
      "canvas": "https://app.moda.com/canvas/..."
    },
    "retry_after_ms": null
  }
}

Envelope fields

FieldTypeDescription
idstringUnique event ID (prefixed evt_…). Stable across retries of this event
typestringOne of the event types listed above
createdstringISO 8601 timestamp when the event was generated
api_versionstringCanonical Moda-Version of the data payload (currently 2026-05-01)
dataobjectCanonical Task object. See Get Task for details
For task.failed / export.failed events, inspect data.error.retryable to distinguish transient failures (worth retrying your own downstream work) from dead-lettered ones.

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

import crypto from 'node:crypto';

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

  // timingSafeEqual throws when buffer lengths differ; check first so a
  // malformed signature can't crash the handler.
  if (expected.length !== received.length) {
    return false;
  }
  return crypto.timingSafeEqual(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 task status via GET /tasks/{task_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 the envelope id (evt_…) as an idempotency key — it’s stable across retries of the same event.
  • Use HTTPS. Your callback_url must use HTTPS to protect the payload in transit.

Delivery log & manual replay

Every webhook attempt is recorded in a delivery log you can query and replay. Useful when your endpoint was down, a signature-verification bug slipped through, or you just want to confirm “did this fire?”
  • List deliveries: GET /webhook_deliveries (reference) — cursor-paginated, filterable by event type, status, API key, and date range. Response rows include the target URL, HTTP status code, response body excerpt (first 2KB), and attempt count.
  • Replay a delivery: POST /webhook_deliveries/{id}/redeliver (reference) — queues a fresh attempt with the original payload to the original URL. Returns 202 with the new delivery’s ID so you can follow it.
Retention: deliveries are kept for 90 days and then hard-deleted. Permission: replay is restricted to the user who created the API key that dispatched the original delivery, or any team admin/owner. If the original API key was deleted, only admins can replay. The log is also surfaced in the Settings → API → View API usage drawer with a Webhooks tab if you prefer a UI over the API.