Skip to content
RunAPI Developer Docs
Guides
Guides

Callbacks

Securely receive and verify Task callback deliveries.

Use callbacks to receive Task lifecycle events at a public HTTPS endpoint. Verify every callback before processing its JSON body so that only deliveries signed for your account can change application state.

Configure a callback URL

Add a public HTTPS callback_url when you create a Task. The event body and lifecycle states depend on the endpoint; consult that endpoint’s API Reference for its callback payloads.

JSON
{
  "model": "flux-2-pro-text-to-image",
  "prompt": "A product photograph on a clean studio background",
  "callback_url": "https://your-domain.com/webhooks/runapi"
}

Create a Callback Secret

Open API Keys and create a Callback Secret for the account that creates the Task. Store the value in a secret manager and make it available only to your callback receiver. It is not an API key and must never be sent in a Task request.

The secret signs callbacks for that account. Rotating it changes the signature for later deliveries, so update every callback receiver immediately and keep both values available only long enough to handle in-flight deliveries.

Verify a callback signature

Each callback is an HTTP POST with Content-Type: application/json. RunAPI does not add an Authorization header to this request. Verify these headers before deserializing the body:

Header Meaning X-Callback-Id Unique identifier for this delivery attempt. X-Callback-Timestamp Unix timestamp in seconds when the delivery was signed. X-Callback-Signature Base64-encoded HMAC-SHA-256 signature.

Build the signed value exactly as follows, using the unmodified request body bytes:

TEXT
X-Callback-Id + "." + X-Callback-Timestamp + "." + raw request body

Base64-decode the Callback Secret, calculate HMAC-SHA-256 over that value, Base64-encode the result, and compare it with X-Callback-Signature using a timing-safe comparison. Do not parse and re-serialize JSON before verifying it.

JavaScript

JAVASCRIPT
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyCallback({headers, rawBody, callbackSecret}) {
  const callbackId = headers["x-callback-id"];
  const timestamp = Number(headers["x-callback-timestamp"]);
  const signature = headers["x-callback-signature"];

  if (!callbackId || !signature || !Number.isSafeInteger(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const signedContent = `${callbackId}.${timestamp}.${rawBody}`;
  const expected = createHmac("sha256", Buffer.from(callbackSecret, "base64"))
    .update(signedContent, "utf8")
    .digest();
  const received = Buffer.from(signature, "base64");

  return expected.length === received.length && timingSafeEqual(expected, received);
}

Pass the framework’s raw request-body string to rawBody; do not call JSON.stringify on parsed data.

Python

PYTHON
import base64
import hashlib
import hmac
import time


def verify_callback(headers, raw_body, callback_secret):
    callback_id = headers.get("X-Callback-Id")
    timestamp = headers.get("X-Callback-Timestamp")
    signature = headers.get("X-Callback-Signature")

    if not callback_id or not timestamp or not signature:
        return False

    try:
        timestamp = int(timestamp)
        secret = base64.b64decode(callback_secret, validate=True)
        received = base64.b64decode(signature, validate=True)
    except (ValueError, TypeError):
        return False

    if abs(time.time() - timestamp) > 300:
        return False

    signed_content = f"{callback_id}.{timestamp}.{raw_body}".encode("utf-8")
    expected = hmac.new(secret, signed_content, hashlib.sha256).digest()
    return hmac.compare_digest(expected, received)

Pass the exact raw request-body string received by your HTTP framework as raw_body.

Process deliveries safely

  • Return a 2xx response only after the callback has been accepted for processing. After a non-2xx response or transport failure, delivery is attempted up to 10 times.
  • Respond within 15 seconds. Queue slower work after verification instead of blocking the HTTP response.
  • Reject callbacks with missing signature headers, an invalid signature, or a timestamp outside the tolerance window your receiver defines.

Troubleshoot verification

  • Signature mismatch: confirm that the Callback Secret belongs to the same account as the Task, decode it with Base64, and sign the original body rather than parsed JSON.
  • Missing signature headers: create a Callback Secret before relying on callbacks for state changes.
  • Timestamp rejected: synchronize the receiver’s clock and use a tolerance appropriate for your deployment.

For setup and lifecycle guidance, return to the Task API quickstart.