Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mayatech.mintlify.app/llms.txt

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

Webhooks let your backend react to Aurous Labs events without polling. Register an endpoint, subscribe to one or more event types, and we POST a signed payload as soon as the event fires.

Quick start

1

Register an endpoint

Send a POST /v1/webhook_endpoints with your HTTPS receiver URL. The response carries a one-time secret — store it; you cannot read it again.
curl -X POST https://api.aurous-labs.com/v1/webhook_endpoints \
  -H "X-Api-Key: $AUROUS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.acme.dev/aurous-webhook",
    "events": ["image.completed", "image.failed"]
  }'
Response (the only time secret is non-null):
{
  "id": "we_01HXMQ7Z3K8Y2NABCDEFGHJKMN",
  "object": "webhook_endpoint",
  "url": "https://api.acme.dev/aurous-webhook",
  "events": ["image.completed", "image.failed"],
  "secret_preview": "whsec_8jK...wxyz",
  "secret": "whsec_8jKpQ4nXabc1234abcdef567890",
  "is_active": true,
  "consecutive_failures": 0,
  "last_success_at": null,
  "last_failure_at": null,
  "metadata": {},
  "created_at": "2026-05-04T01:00:00Z",
  "updated_at": "2026-05-04T01:00:00Z"
}
2

Verify the signature on every delivery

Every delivery carries an Aurous-Webhook-Signature header of the form t=<unix_sec>,v1=<hex>. Reconstruct the canonical signed payload (${t}.${raw_body}) and HMAC-SHA256 it with your stored secret. See the verifier examples.Aurous Labs uses Unix seconds (a 10-digit timestamp) for the t= parameter, matching the Stripe webhook convention.
3

Return 2xx to acknowledge

Acknowledge within 10 seconds. Any non-2xx, timeout, connection refused, or TLS error counts as a failed delivery and triggers a retry on the exponential-backoff schedule.

Event taxonomy (v1.0)

These eight event types ship in v1.0. Subscribe to a subset via the events array, or pass ["*"] to subscribe to every type at create time (the wildcard is expanded server-side at create time — new event types added in a later version do NOT auto-subscribe).
Event typeFires when
image.completedImage generation reached succeeded. Payload contains the rendered URLs.
image.failedImage generation failed (provider error, moderation, etc.).
image.cancelledCustomer called POST /v1/images/:id/cancel.
video.completedVideo generation reached succeeded.
video.failedVideo generation failed.
video.cancelledCustomer cancelled the video generation.
usage.balance_lowTeam credits dropped to or below balance_low_threshold (1h debounced).
webhook.endpoint_disabledAnother endpoint on this team auto-disabled after sustained failures.
image.expired, video.expired, image.moderation_rejected, and video.moderation_rejected are planned for v1.1 and not emitted today.

Payload shape

Every delivery body is a single JSON object — the AurousEvent envelope:
{
  "id": "evt_01HXMQ7Z3K8Y2NABCDEFGHJKMN",
  "object": "event",
  "type": "image.completed",
  "created_at": "2026-05-04T01:00:00Z",
  "synthetic": false,
  "data": {
    "id": "img_01HXMQ7Z3K8Y2NABCDEFGHJKMN",
    "object": "image",
    "status": "succeeded",
    "...": "the same shape returned by GET /v1/images/:id"
  }
}
synthetic: true appears on the envelope (never inside data) when the delivery was triggered via POST /v1/webhook_endpoints/:id/test. Receivers can filter test fires from production traffic with a single field check.

Headers

Every delivery carries the following request headers:
HeaderExamplePurpose
Aurous-Webhook-Signaturet=1714867200,v1=8d3f...c4a1HMAC-SHA256 of ${t}.${raw_body} with your endpoint secret. Verify on every request.
Aurous-Event-Idevt_01HXMQ7Z3K8Y2NABCDEFGHJKMNSame id as the envelope. Use for de-dup on your side.
Aurous-Event-Typeimage.completedSame type as the envelope. Lets you route without parsing the body.
Aurous-Version2026-05-15API version that minted the event. Add a guard if your handler is version-locked.
User-AgentAurous-Labs-Webhooks/1.0Helps you allow-list our traffic at your edge.
Content-Typeapplication/jsonAlways JSON.

Signature format

Aurous-Webhook-Signature: t=<unix_sec>,v1=<hex> Where:
  • <unix_sec> is the moment we minted the signature (a 10-digit Unix timestamp in seconds — the Stripe convention).
  • <hex> is the lowercase hex of HMAC-SHA256(secret, "${t}.${raw_body}").
The t value goes INSIDE the HMAC payload, so an attacker cannot replay a captured body with a tweaked timestamp. We recommend rejecting any delivery whose t is older than 5 minutes — adjust the tolerance if your receiver uses a lossy queue.

Retries and dead-letter

Failed deliveries (non-2xx, timeout, connect refused, or TLS error) are retried on this schedule:
AttemptDelay before retry
1initial delivery
25 seconds
330 seconds
42 minutes
510 minutes
After attempt 5, the delivery is marked is_terminal: true and the row is flagged with is_dead_letter: true. We stop retrying. If consecutive_failures >= 20 AND there has been no successful delivery in the last 24 hours, we auto-disable the endpoint:
  • is_active flips to false.
  • We email the team owner.
  • Other active endpoints on the team receive a webhook.endpoint_disabled event so peer integrations can react.
Re-enable via PATCH /v1/webhook_endpoints/:id with { "is_active": true } once you’ve fixed the receiver. The counter resets to 0 on re-enable.

Receiver cookbook

The contract is symmetric: we sign with the active secret only; you verify against the active secret first, then the previous secret as a 24h fallback. This receiver-side fallback is what makes secret rotation zero-downtime.

Node.js (Express)

import express from 'express';
import crypto from 'crypto';

const app = express();
const ACTIVE = process.env.AUROUS_WEBHOOK_SECRET;
const PREV = process.env.AUROUS_WEBHOOK_SECRET_PREV; // set ONLY for 24h after rotation

function verify(rawBody, sigHeader) {
  if (!sigHeader) return false;
  const parts = Object.fromEntries(
    sigHeader.split(',').map((p) => p.split('=')),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;
  // `t` is Unix seconds — compare in seconds.
  const nowSec = Math.floor(Date.now() / 1000);
  if (Math.abs(nowSec - Number(t)) > 5 * 60) return false; // 5min replay window
  const payload = `${t}.${rawBody}`;

  const tryWith = (secret) =>
    secret && crypto.timingSafeEqual(
      Buffer.from(crypto.createHmac('sha256', secret).update(payload).digest('hex')),
      Buffer.from(v1),
    );
  return tryWith(ACTIVE) || tryWith(PREV);
}

// Important: receive raw bytes — JSON.parse later, AFTER signature verify.
app.post(
  '/aurous-webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const raw = req.body.toString('utf8');
    if (!verify(raw, req.headers['aurous-webhook-signature'])) {
      return res.status(401).end();
    }
    const event = JSON.parse(raw);
    if (event.synthetic) return res.status(200).end(); // ignore test fires
    // ... handle event ...
    res.status(200).end();
  },
);

Python (Flask)

import hmac, hashlib, os, time, json
from flask import Flask, request, abort

ACTIVE = os.environ["AUROUS_WEBHOOK_SECRET"]
PREV = os.environ.get("AUROUS_WEBHOOK_SECRET_PREV")  # set ONLY for 24h after rotation

def verify(raw, sig_header):
    if not sig_header:
        return False
    parts = dict(p.split("=", 1) for p in sig_header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        return False
    # `t` is Unix seconds — compare in seconds.
    if abs(int(time.time()) - int(t)) > 5 * 60:
        return False  # 5min replay window
    payload = f"{t}.{raw.decode('utf-8')}"

    def try_with(secret):
        if not secret:
            return False
        expected = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, v1)

    return try_with(ACTIVE) or try_with(PREV)

app = Flask(__name__)

@app.post("/aurous-webhook")
def aurous_webhook():
    raw = request.get_data()  # raw bytes — verify BEFORE json.loads
    if not verify(raw, request.headers.get("Aurous-Webhook-Signature")):
        abort(401)
    event = json.loads(raw)
    if event.get("synthetic"):
        return "", 200  # ignore test fires
    # ... handle event ...
    return "", 200

Manual curl verification

Useful for one-off debugging: capture a delivery body + signature, then verify locally.
# Save the body to body.json and the header value to a variable
SIG="t=1714867200,v1=8d3f...c4a1"
T=$(echo "$SIG" | sed -E 's/.*t=([^,]+).*/\1/')
V1=$(echo "$SIG" | sed -E 's/.*v1=([^,]+).*/\1/')

# Recompute over t.body
PAYLOAD="${T}.$(cat body.json | tr -d '\n')"
EXPECTED=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$AUROUS_WEBHOOK_SECRET" | awk '{print $2}')

# Constant-time compare (or eyeball)
if [ "$EXPECTED" = "$V1" ]; then echo "OK"; else echo "MISMATCH"; fi

Rotating the secret

Hit POST /v1/webhook_endpoints/:id/rotate_secret to mint a new plaintext. The response carries the new secret exactly once. The contract during rotation:
  • Sender (Aurous Labs): signs every new delivery with the active secret only.
  • Receiver (you): for the next 24 hours, store the previous secret as PREV and verify against ACTIVE first, then fall back to PREV (as shown in the cookbook).
Why? A delivery in flight at the moment of rotation may arrive seconds later, signed with the old secret. The 24h dual-validate window guarantees zero downtime for receivers under realistic clock skew + retry windows. After 24 hours, drop PREV. We never sign with it again.
curl -X POST https://api.aurous-labs.com/v1/webhook_endpoints/we_01HXMQ7Z3K8Y2NABCDEFGHJKMN/rotate_secret \
  -H "X-Api-Key: $AUROUS_KEY"
# → returns { "secret": "whsec_NEW_PLAINTEXT", ... } exactly once

Test firing

Hit POST /v1/webhook_endpoints/:id/test with an event_type to enqueue a real, signed synthetic delivery. The envelope carries synthetic: true so receivers can filter test fires from production traffic.
curl -X POST https://api.aurous-labs.com/v1/webhook_endpoints/we_01HXMQ7Z3K8Y2NABCDEFGHJKMN/test \
  -H "X-Api-Key: $AUROUS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "event_type": "image.completed" }'
# → 202 { "object": "webhook_test_fire", "endpoint_id": "we_...", "event_id": "evt_...", "enqueued": true }
Test fires use an isolated rate-limit bucket (webhooks_test, 30 / minute) so they never contend with normal webhook traffic.

Inspecting deliveries

Walk the per-attempt log via GET /v1/webhook_endpoints/:id/deliveries:
curl -G https://api.aurous-labs.com/v1/webhook_endpoints/we_01HXMQ7Z3K8Y2NABCDEFGHJKMN/deliveries \
  -H "X-Api-Key: $AUROUS_KEY" \
  --data-urlencode "limit=20"
Each row carries the attempt number, response status (or null for transport errors), error class (http_4xx / http_5xx / timeout / connect_refused / tls_error / connect_error), and the first 1KB of the response body if any. Cursor-paged via ?starting_after=<dlv_id>.