Webhooks
Receive real-time HTTP notifications when image requests reach a terminal state. Webhooks are optional but strongly recommended as an alternative to polling.
Events
The following webhook events are delivered to the webhook URL configured in your partner account settings:
| Event | Trigger | Description |
|---|---|---|
image.completed | Image is ready | Includes the image URL, dimensions, content type, and credits used. |
image.failed | Image generation failed | Includes the failure code and a human-readable error message. |
Delivery headers
Each webhook delivery includes these headers for verification and routing:
| Header | Example | Description |
|---|---|---|
X-ProClubs-Signature | sha256=a1b2c3... | HMAC-SHA256 signature for payload verification. |
X-ProClubs-Timestamp | 1740000012 | Unix timestamp when the webhook was sent. Use for replay protection. |
X-ProClubs-Event | image.completed | The event type for quick routing. |
X-Request-Id | req_abc123 | Unique delivery ID for tracing and deduplication. |
Payload examples
image.completed
{
"event": "image.completed",
"image_id": "img_01HSXYZ9Y5T8S5H0R9F3A2M7Q1",
"status": "completed",
"result": {
"image_url": "https://cdn.proclubsstudio.com/partner/img_01HSXYZ9Y5T8S5H0R9F3A2M7Q1.png",
"width": 1024,
"height": 1280,
"content_type": "image/png",
"expires_at": "2026-05-21T11:30:12Z"
},
"usage": {
"credits_used": 1
},
"metadata": {
"partner_user_id": "user_789"
},
"timestamp": "2026-02-19T11:30:12Z"
}image.failed
{
"event": "image.failed",
"image_id": "img_01HSXYZ9Y5T8S5H0R9F3A2M7Q1",
"status": "failed",
"failure": {
"code": "IMAGE_PROCESSING_FAILED",
"message": "Unable to detect player in source image"
},
"metadata": {
"partner_user_id": "user_789"
},
"timestamp": "2026-02-19T11:30:08Z"
}Signature verification
All webhook deliveries are signed using HMAC-SHA256. You should always verify the signature before processing the payload.
Algorithm
The signature is computed as:
Verification steps:
- Extract
X-ProClubs-TimestampandX-ProClubs-Signaturefrom the request headers. - Reject the request if the timestamp is older than 5 minutes (replay protection).
- Concatenate:
timestamp + "." + raw_body - Compute HMAC-SHA256 using your webhook secret.
- Compare with the provided signature using constant-time comparison.
import crypto from "node:crypto";
function verifyWebhookSignature(
rawBody: string,
signature: string,
timestamp: string,
webhookSecret: string
): boolean {
// Reject if timestamp is older than 5 minutes
const timestampAge = Math.abs(Date.now() / 1000 - parseInt(timestamp, 10));
if (timestampAge > 300) {
return false;
}
// Compute expected signature
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", webhookSecret)
.update(payload)
.digest("hex");
const expectedSig = `sha256=${expected}`;
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSig)
);
}
// Express middleware example
app.post("/webhooks/proclubsstudio", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-proclubs-signature"] as string;
const timestamp = req.headers["x-proclubs-timestamp"] as string;
const event = req.headers["x-proclubs-event"] as string;
const isValid = verifyWebhookSignature(
req.body.toString(),
signature,
timestamp,
process.env.WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
const payload = JSON.parse(req.body.toString());
switch (event) {
case "image.completed":
console.log("Image ready:", payload.result.image_url);
break;
case "image.failed":
console.log("Image failed:", payload.failure.message);
break;
}
res.status(200).json({ received: true });
});Retry policy
If your webhook endpoint returns a non-2xx status code or times out, Pro Clubs Studio will retry delivery with exponential backoff:
| Attempt | Delay after previous | Cumulative time |
|---|---|---|
| 1 | Immediate | 0 min |
| 2 | 1 minute | 1 min |
| 3 | 5 minutes | 6 min |
| 4 | 15 minutes | 21 min |
| 5 | 60 minutes | ~1.5 hours |
| 6 | 6 hours | ~7.5 hours |
After all retry attempts are exhausted, the webhook delivery is marked as delivery_failed. Every attempt is logged and visible in your Partner Console.
Test vs live webhooks
Webhooks behave the same in both environments. The key difference is an additional field in the payload:
Live webhooks
Delivered for real image completions. The payload uses your production data and CDN URLs.
Test webhooks
Delivered to the same webhook_url. The payload includes "environment": "test" so you can distinguish test from live events.
Tip: Use test keys during development to verify your webhook handler without consuming credits. Your endpoint will receive real HTTP requests with test payloads.
Best practices
Return 200 quickly
Acknowledge the webhook with a 200 response as fast as possible. Process the payload asynchronously (e.g. via a queue) to avoid timeouts.
Handle duplicates
Webhook deliveries may be retried. Use the image_id and X-Request-Id to deduplicate events on your side.
Always verify signatures
Never skip signature verification in production. This protects against forged webhook deliveries and replay attacks.