Skip to content
H3 Studio

Callbacks

Set callback_url on a request and we will POST you every status change, rather than making you poll.

The handshake

The first time a URL is used, we send it a challenge:

{ "challenge": "9f2c…" }

Echo the value back, unchanged, within three seconds:

{ "challenge": "9f2c…" }

Until that succeeds, no notifications are sent. This proves the URL is yours and is awake — it stops the API being turned into an amplifier for sending traffic at someone else.

Notifications

Each delivery carries the same body the query endpoint returns:

{
  "task": {
    "id": "424010985738629",
    "status": "succeeded",
    "content": { "url": "https://cdn.h3.studio/…" },
    "usage": { "total_seconds": 5, "output_seconds": 5, "input_seconds": 0, "input_image_count": 0 }
  }
}

Verifying the signature

Two headers come with every POST:

HeaderMeaning
X-H3-TimestampUnix seconds when it was signed
X-H3-Signaturesha256= + HMAC-SHA256 over "{timestamp}." + body
import hmac, hashlib

def verify(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Compare with a constant-time function, not ==. Reject anything with a timestamp more than a few minutes old, which is what stops a captured request being replayed later.

Retries and duplicates

A non-2xx response or a timeout is retried with exponential backoff: 5 s, 10 s, 20 s, 40 s, 80 s, 160 s, then we stop.

Delivery is deduplicated on (task, status), so a task that reaches succeeded once notifies once. Still, write your handler to be idempotent: a network failure after your server committed but before we saw the 200 is indistinguishable, from our side, from a failure before it.

Return 2xx as soon as you have durably recorded the event. Do the slow work afterwards — a handler that transcodes before responding will time out and get the same event again.