Upgrade
PicX StudioPicX Studio
Create
Generate
Explore
Templates
Skills
Prompts
Product Video Studio
Product Image Studio
Ad Templates
Media
DirectorLive
Creditsup to 80%

Buy Credits−80%CreditsLoading…up to 80% off yearly
  • Overview
  • Docs
  • API keys
  • Playground
  • MCP
  • CLI
Getting Started
  • Quick Start
  • Authentication
  • Rate Limits
MCP
  • Overview
  • Tools Reference
  • Connect Claude Desktop
  • Connect Cursor
CLI
  • Overview
  • Command Reference
  • Recipes
API Reference
  • Generate Image
  • Edit Image
  • Generate Video
  • List Models
  • Managed Assets
  • Templates
Developer Tools
  • API Keys
  • Webhooks
  • Usage Tracking
  • Playground
  • Async Image Generation
Code Examples
  • cURL Examples
  • Python SDK
  • JavaScript SDK
DevelopersDocsDeveloper Tools

Webhooks

Receive signed notifications when asynchronous generations complete or fail.

View as Markdown

Webhooks notify your server when a generation finishes, so you do not have to poll. They apply to every asynchronous generation — video, and (since API version 2026-08-01) images submitted with a delivery target.

There are two ways to name a target, and which you can use depends on how you authenticate:

How you set it Auth to set up Signed with
Per-request URL callback_url or webhook: {url} in the generation body API key a secret derived from your API key
Registered webhook create in the console, then webhook: {id} signed-in session its own whsec_…

Webhook registration (create, list, test, delete) lives on the console's session-authenticated API under /api/webhooks, not on the public /v1 surface. An API key alone cannot register a webhook. If you provision programmatically, use callback_url or webhook: {url} on the generation request — it needs no setup and is signed and logged the same way.

Delivery inspection is different: GET /v1/generations/{id}/deliveries, GET /v1/webhooks/{id}/deliveries and POST /v1/webhooks/deliveries/{id}/redeliver are published under /v1 and work with an API key. In the SDKs that is generations.deliveries(), webhooks.deliveries() and webhooks.redeliver(), available from picx-ai 0.3.1.

For images specifically, Async Image Generation covers the whole flow end to end.

The simplest path: a per-request URL#

No registration, no console — works with an API key alone:

import { PicX } from "picx-ai";

const picx = new PicX(process.env.PICX_API_KEY);

const job = await picx.images.generate({
  prompt: "a cat on a windowsill",
  callback_url: "https://your-server.com/hooks/picx",
});

console.log(job.id); // persist this to correlate the delivery
import os
from picx import PicX

picx = PicX(os.environ["PICX_API_KEY"])

job = picx.images.generate(
    "a cat on a windowsill",
    callback_url="https://your-server.com/hooks/picx",
)
curl -X POST https://api.picxstudio.com/v1/images/generate \
  -H "Authorization: Bearer pxsk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"a cat on a windowsill","callback_url":"https://your-server.com/hooks/picx"}'

The same field works on POST /v1/images/edit and POST /v1/videos/generate.

The secret for this mode is derived from the API key that made the request — stable, distinct per key, and readable from the key's detail view in the developer console. It is not returned by any /v1 endpoint.

Bind inline through the webhook field#

Equivalent to callback_url, but recorded against the generation as an explicit binding (mode: "inline") and delivered with the strict envelope rather than the legacy flat body:

const job = await picx.video.create({
  prompt: "a cat jumping off a table",
  duration: 5,
  resolution: "480p",
  webhook: { url: "https://your-server.com/webhook" },
});
job = picx.video.create(
    prompt="a cat jumping off a table",
    duration=5,
    resolution="480p",
    webhook_url="https://your-server.com/webhook",
)

Resolution order is first match wins: webhook.id, then webhook.url, then callback_url.

Managing registered webhooks#

Registration requires a signed-in console session. Paths are under /api, not /v1.

Action Path API key?
Create POST /api/webhooks no — console only
List GET /api/webhooks no — console only
Test delivery POST /api/webhooks/{id}/test no — console only
Delete DELETE /api/webhooks/{id} no — console only

Delivery inspection is available to API keys, under /v1:

Action Path API key?
Deliveries for one generation GET /v1/generations/{id}/deliveries yes
Deliveries for one webhook GET /v1/webhooks/{id}/deliveries yes
Redeliver POST /v1/webhooks/deliveries/{id}/redeliver yes

The signing secret (whsec_…) is returned only once, when you create the webhook. Store it immediately.

Inspecting deliveries#

When a result never turns up, this is the call that tells you why — it shows every event fired for a generation and every attempt made, including inline and legacy callback_url deliveries that have no registered webhook row.

const { deliveries, total } = await picx.generations.deliveries(job.id);
for (const d of deliveries) {
  console.log(d.event_type, d.outcome, d.status_code, d.target_url);
  console.log(d.attempts); // [{ n: 1, at: ..., status: 503, ms: 812, error: null }]
}
log = picx.generations.deliveries(job.id)
for d in log.deliveries:
    print(d.event_type, d.outcome, d.status_code, d.target_url)
curl https://api.picxstudio.com/v1/generations/GENERATION_ID/deliveries \
  -H "Authorization: Bearer pxsk_your_key"

For a registered webhook, filter to just the problems:

const { deliveries } = await picx.webhooks.deliveries(webhookId, {
  outcome: "exhausted",
  limit: 50,
});

outcome accepts delivered, failed, pending_retry or exhausted; anything else is rejected with a 400 rather than quietly matching nothing. limit caps at 100.

Once you have fixed your endpoint, replay a delivery:

const result = await picx.webhooks.redeliver(deliveryId);
console.log(result.outcome, result.status_code);
result = picx.webhooks.redeliver(delivery_id)

The replay reuses the original payload and event_id, so a consumer that dedupes on X-PicX-Delivery can safely ignore one it already handled. The recorded target URL is re-validated first — a delivery whose hostname now resolves to a private address is refused with a 400 rather than fetched.

Once registered, binding a generation to a webhook by id needs only an API key:

const job = await picx.images.generate({
  prompt: "a cat",
  webhook: { id: "0b9c1e42-…", events: ["generation.completed"] },
});
job = picx.images.generate("a cat", webhook_id="0b9c1e42-…")

A bad or foreign webhook id fails the submit with 404, before credits are spent, rather than running a generation that delivers nowhere.

Delivery headers#

POST /your/webhook HTTP/1.1
Content-Type: application/json
X-PicX-Event: generation.completed
X-PicX-Delivery: evt_e7e4a0602e364e26ba574c4d0ca03027
X-PicX-Attempt: 1
X-PicX-Generation: d3bfa107-6475-4f5b-ad32-c65c3e1b0377
X-PicX-Signature: t=1787654321,v1=6f1c2a9d8e...

Event payload#

{
  "id": "evt_e7e4a0602e364e26ba574c4d0ca03027",
  "event": "generation.completed",
  "created_at": "2026-08-25T16:02:47.802789+00:00",
  "api_version": "2026-08-01",
  "webhook_id": null,
  "data": {
    "generation_id": "d3bfa107-6475-4f5b-ad32-c65c3e1b0377",
    "status": "completed",
    "type": "image",
    "model": "gemini-3.1-flash-image-preview",
    "output_url": "https://cdn.picxstudio.com/api/generated/image_6830d3ca.png",
    "error_message": null,
    "credits_used": 35
  }
}

The event name is in event, and the delivery id in id. data.type is image or video.

Two event types are sent by default — generation.completed and generation.failed — and generation.cancelled can be subscribed to explicitly. On a failure output_url is null, error_message is populated, and the credits are refunded.

api_version is bumped only on a breaking payload change, so you can pin against it.

A callback_url target receives a superset body for backwards compatibility: the envelope above plus the data fields flattened onto the top level (generation_id, status, output_url, …). Reading data.output_url works for every target type, so prefer it.

Delivery behaviour#

Property Value
Attempts 3
Backoff 1s, then 2s
Per-attempt timeout 10s
Redirects not followed
Success any 2xx

Reply 2xx promptly and move heavy work off the request — a timeout counts as a failed attempt, and after three the delivery is marked exhausted.

Deliveries are at-least-once. Dedupe on X-PicX-Delivery (or the body's id) and keep handlers idempotent.

Never treat the webhook as your only path to the result. If a delivery is missed the generation still succeeded — read it with GET /v1/generations/{id} using the id from the 202. Persist that id at submit time.

Callback URL requirements#

Validated at submit time; a bad target fails with 400 before credits are spent.

  • Scheme http or https; use https in production
  • Hostname must resolve to a public address
  • Loopback, private ranges and link-local (including the cloud metadata address) are rejected
  • Maximum 2048 characters

The guard fails closed, so a hostname that does not resolve is rejected. For local development use a tunnel (cloudflared tunnel --url http://localhost:3000) rather than localhost.

Always verify the signature before trusting a delivery. The X-PicX-Signature header has the form t={timestamp},v1={signature}: compute HMAC-SHA256 over the exact string "{timestamp}." followed by the raw request body, using your webhook secret, then compare in constant time.

Verify the signature#

Verify against the raw bytes, before any JSON parsing or re-serialisation — a framework that reformats the body will invalidate the digest.

python

import hmac, hashlib
from flask import Flask, request, abort, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret"

def verify_signature(secret: str, header: str, raw_body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp, signature = parts["t"], parts["v1"]
    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    header = request.headers.get("X-PicX-Signature", "")
    if not header or not verify_signature(WEBHOOK_SECRET, header, request.get_data()):
        abort(401)
    event = request.get_json()
    if event["event"] == "generation.completed":
        print("Ready:", event["data"]["output_url"])
    elif event["event"] == "generation.failed":
        print("Failed:", event["data"]["error_message"])
    return jsonify(ok=True)

javascript

import express from "express";
import crypto from "crypto";

const app = express();
const WEBHOOK_SECRET = "whsec_your_secret";

function verifySignature(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const signed = `${parts.t}.` + rawBody.toString();
  const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
  const a = Buffer.from(parts.v1 || "", "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.headers["x-picx-signature"] || "";
  if (!verifySignature(WEBHOOK_SECRET, header, req.body)) {
    return res.status(401).json({ error: "Invalid signature" });
  }
  const event = JSON.parse(req.body.toString());
  if (event.event === "generation.completed") {
    console.log("Ready:", event.data.output_url);
  }
  res.json({ ok: true });
});

Deliveries are retried up to 3 times with backoff on non-2xx responses. Respond quickly with a 2xx once you have accepted the event, then do heavy work asynchronously.

Previous
API Keys
Next
Usage Tracking
On this page
  • The simplest path: a per-request URL
  • Bind inline through the webhook field
  • Managing registered webhooks
  • Inspecting deliveries
  • Delivery headers
  • Event payload
  • Delivery behaviour
  • Callback URL requirements
  • Verify the signature
PicX Studio

Fuel your creativity, frame your story.

Studio

  • 1985 Snapshot
  • Templates
  • Skills
  • Tools
  • Prompts
  • Discover
  • PicX TV
  • Pricing
  • Blog

Skills

  • 1985 Flash Snapshot
  • 90s Album Snapshot
  • Storyboard to Video Workflow
  • H3 Max Director Live
  • GPT Image 2 Prompting
  • Character Continuity

Video models

  • MiniMax H3
  • Seedance 2.5
  • Seedance 2.0
  • Kling 3.0 Pro
  • FLUX 3
  • Grok Imagine
  • What is Seedance?
  • Higgsfield alternative

Image models

  • Nano Banana 2
  • Nano Banana Pro
  • GPT Image 2
  • Seedream 5 Pro
  • Nano Banana 2 vs Pro
  • What is Nano Banana?
  • Prompt Generator

E-commerce

  • AI tools for e-commerce
  • AI product photography
  • AI product video generator
  • AI UGC video ads
  • AI video ad generator
  • AI dropshipping

Free Tools

  • Background Remover
  • Image Upscaler
  • Image Compressor
  • Image to Text
  • Meme Generator
  • Instagram grid maker
  • AI face generator
  • Explore all tools

Resources

  • Compare
  • Prompt Roundups
  • Guides
  • Docs
  • API
  • CLI
  • FAQ

Company

  • About Us
  • Team
  • Careers
  • Brand
  • Partners
  • Sponsors
  • Roadmap
  • Status
  • Domain Rating
PicX Studio

© 2026 PicX Studio. All rights reserved.

Monitor your Domain Rating with FrogDR
[email protected]
  • Terms of Service
  • Privacy Policy
  • Security