ApiframeApiframe Docs

Webhooks

Receive real-time notifications when jobs complete or fail.

Instead of polling the jobs endpoint, you can provide a webhookUrl when submitting a generation request. Apiframe will send an HTTP POST to your URL when the job reaches the subscribed event.

Setting up webhooks

Include webhookUrl and webhookEvents in your generation request:

{
  "prompt": "a mountain landscape at dawn",
  "model": "midjourney",
  "webhookUrl": "https://your-server.com/api/webhook",
  "webhookEvents": ["completed", "failed"]
}

Webhook events

EventWhen it fires
progressEach time the job progress percentage updates
completedThe job finished successfully — result URL is included
failedThe job failed — error message is included

Payload format

Apiframe sends a POST request with a JSON body:

Completed event

{
  "event": "completed",
  "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "COMPLETED",
  "progress": 100,
  "result": "https://cdn.apiframe.ai/results/a1b2c3d4.png",
  "model": "midjourney",
  "creditCost": 10,
  "completedAt": "2026-03-15T10:00:32.000Z"
}

Failed event

{
  "event": "failed",
  "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "FAILED",
  "error": "Provider returned an error: content policy violation",
  "model": "midjourney"
}

Progress event

{
  "event": "progress",
  "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "PROCESSING",
  "progress": 45,
  "model": "midjourney"
}

Verifying webhook signatures

Every webhook request includes two headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the JSON body
X-Webhook-EventThe event type (completed, failed, or progress)

The signing secret is the SHA-256 hash of your API key. For example, if your API key is afk_abc123..., the signing secret is SHA256("afk_abc123...").

To verify a webhook:

  1. Compute SHA-256 of your raw API key to get the signing secret
  2. Compute HMAC-SHA256 of the raw request body using that signing secret
  3. Compare the result to the X-Webhook-Signature header (use a constant-time comparison to prevent timing attacks)

Verification examples

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

const API_KEY = "afk_your_api_key_here";
const SIGNING_SECRET = createHash("sha256").update(API_KEY).digest("hex");

function verifyWebhook(body, signature) {
  const expected =
    "sha256=" + createHmac("sha256", SIGNING_SECRET).update(body).digest("hex");
  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// In your webhook handler:
app.post("/api/webhook", (req, res) => {
  const signature = req.headers["x-webhook-signature"];
  if (!verifyWebhook(JSON.stringify(req.body), signature)) {
    return res.status(401).send("Invalid signature");
  }

  // Process the webhook...
  res.sendStatus(200);
});
import hmac
import hashlib

API_KEY = "afk_your_api_key_here"
SIGNING_SECRET = hashlib.sha256(API_KEY.encode()).hexdigest()

def verify_webhook(body: bytes, signature: str) -> bool:
    expected = "sha256=" + hmac.new(
        SIGNING_SECRET.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# In your webhook handler (Flask example):
@app.route("/api/webhook", methods=["POST"])
def webhook():
    signature = request.headers.get("X-Webhook-Signature", "")
    if not verify_webhook(request.data, signature):
        return "Invalid signature", 401

    # Process the webhook...
    return "", 200

URL requirements

  • Must use https:// or http:// protocol
  • Cannot target private or internal IP addresses (localhost, 10.x.x.x, 192.168.x.x, etc.)
  • Maximum URL length: 2048 characters

Retry behavior

Terminal events (completed, failed) are retried up to 10 times with exponential backoff starting at 5 seconds. Progress events are best-effort with a single attempt.

If all retries are exhausted, the delivery is marked as failed. The webhookStatus field on the job object reflects the delivery outcome:

StatusMeaning
pendingWebhook has not been sent yet
deliveredWebhook was delivered successfully (2xx response)
failedAll delivery attempts failed (non-2xx or network error)

Best practices

  • Verify signatures — always verify the X-Webhook-Signature header to ensure the payload is authentic and hasn't been tampered with. Use a constant-time comparison function to prevent timing attacks.
  • Respond quickly — return a 200 status within a few seconds. Process the payload asynchronously if needed.
  • Use HTTPS — always use HTTPS endpoints in production to protect your webhook payloads in transit.
  • Handle duplicates — in rare cases a webhook may be delivered more than once. Use the jobId to deduplicate.

On this page