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
| Event | When it fires |
|---|---|
progress | Each time the job progress percentage updates |
completed | The job finished successfully — result URL is included |
failed | The 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:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the JSON body |
X-Webhook-Event | The 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:
- Compute
SHA-256of your raw API key to get the signing secret - Compute
HMAC-SHA256of the raw request body using that signing secret - Compare the result to the
X-Webhook-Signatureheader (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 "", 200URL requirements
- Must use
https://orhttp://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:
| Status | Meaning |
|---|---|
pending | Webhook has not been sent yet |
delivered | Webhook was delivered successfully (2xx response) |
failed | All delivery attempts failed (non-2xx or network error) |
Best practices
- Verify signatures — always verify the
X-Webhook-Signatureheader 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
200status 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
jobIdto deduplicate.