Skip to main content
Documentation

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:

EventTriggerDescription
image.completedImage is readyIncludes the image URL, dimensions, content type, and credits used.
image.failedImage generation failedIncludes the failure code and a human-readable error message.

Delivery headers

Each webhook delivery includes these headers for verification and routing:

HeaderExampleDescription
X-ProClubs-Signaturesha256=a1b2c3...HMAC-SHA256 signature for payload verification.
X-ProClubs-Timestamp1740000012Unix timestamp when the webhook was sent. Use for replay protection.
X-ProClubs-Eventimage.completedThe event type for quick routing.
X-Request-Idreq_abc123Unique 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:

signature = HMAC_SHA256(webhook_secret, timestamp + "." + raw_body)

Verification steps:

  1. Extract X-ProClubs-Timestamp and X-ProClubs-Signature from the request headers.
  2. Reject the request if the timestamp is older than 5 minutes (replay protection).
  3. Concatenate: timestamp + "." + raw_body
  4. Compute HMAC-SHA256 using your webhook secret.
  5. 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:

AttemptDelay after previousCumulative time
1Immediate0 min
21 minute1 min
35 minutes6 min
415 minutes21 min
560 minutes~1.5 hours
66 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.