Image Creation
Pro Clubs Studio generates player card images asynchronously. You submit a request, receive an image_id immediately, then poll for the result or receive it via webhook.
Async flow overview
Image creation follows a two-step pattern:
POST /v1/images
Submit the image request. Returns 202 Accepted with an image_id and estimated completion time.
GET /v1/images/:image_id
Poll for status updates. When the image is ready, the response includes the CDN URL. Alternatively, configure a webhook to receive the result automatically.
Request lifecycle
Every image request transitions through these states:
| Status | Terminal | Description |
|---|---|---|
processing | No | Image is being generated. The progress field provides a best-effort estimate. |
completed | Yes | Image is ready. The result.image_url field contains the CDN URL. |
failed | Yes | Image generation failed. The failure object contains the error code and message. |
Progress field semantics
- Range:
0.0to1.0(inclusive). - Present only when
statusis"processing". Omitted from terminal states. - May be
nullif progress is indeterminate (e.g. queued but not yet started). - Not guaranteed to be monotonically increasing — treat it as a best-effort estimate for UI display.
Idempotency
The Idempotency-Key header is required on POST /images requests. It prevents duplicate image creation if you need to retry a request.
| Scenario | Result |
|---|---|
| Same key + same payload | Returns the original 202 response with the same image_id. No duplicate processing. |
| Same key + different payload | Returns 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD. |
| Different key + any payload | Creates a new image request as normal. |
Idempotency keys are scoped to your partner account and retained for at least 48 hours.
// First request — creates the image
const res1 = await fetch("https://api.proclubsstudio.com/v1/images", {
method: "POST",
headers: {
"X-API-Key": "sk_live_xxx",
"Idempotency-Key": "order-456-avatar",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
// => 202, image_id: "img_abc..."
// Retry with SAME key + SAME payload — returns original response
const res2 = await fetch("https://api.proclubsstudio.com/v1/images", {
method: "POST",
headers: {
"X-API-Key": "sk_live_xxx",
"Idempotency-Key": "order-456-avatar", // same key
"Content-Type": "application/json",
},
body: JSON.stringify(payload), // same payload
});
// => 202, same image_id: "img_abc..."
// Retry with SAME key + DIFFERENT payload — error!
const res3 = await fetch("https://api.proclubsstudio.com/v1/images", {
method: "POST",
headers: {
"X-API-Key": "sk_live_xxx",
"Idempotency-Key": "order-456-avatar", // same key
"Content-Type": "application/json",
},
body: JSON.stringify(differentPayload), // different payload!
});
// => 409, IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOADComplete create + poll example
Here is a full working example that creates an image and polls until it reaches a terminal state:
# Step 1: Create the image
IMAGE_RESPONSE=$(curl -s -X POST \
"https://api.proclubsstudio.com/v1/images" \
-H "X-API-Key: sk_live_xxx" \
-H "Idempotency-Key: player-123-$(date +%s)" \
-H "Content-Type: application/json" \
-d '{
"type": "player_card",
"player": {
"external_id": "player_123",
"name": "Zethxr",
"position": "ST",
"number": 18,
"skill_level": 88,
"source_image_url": "https://your-app.com/uploads/player.png"
},
"style": { "preset": "pro_clubs_card" }
}')
IMAGE_ID=$(echo $IMAGE_RESPONSE | jq -r '.data.image_id')
echo "Created: $IMAGE_ID"
# Step 2: Poll until complete
while true; do
sleep 2
STATUS_RESPONSE=$(curl -s \
"https://api.proclubsstudio.com/v1/images/$IMAGE_ID" \
-H "X-API-Key: sk_live_xxx")
STATUS=$(echo $STATUS_RESPONSE | jq -r '.data.status')
if [ "$STATUS" = "completed" ]; then
echo "Done! URL: $(echo $STATUS_RESPONSE | jq -r '.data.result.image_url')"
break
elif [ "$STATUS" = "failed" ]; then
echo "Failed: $(echo $STATUS_RESPONSE | jq -r '.data.failure.message')"
break
fi
echo "Status: $STATUS ($(echo $STATUS_RESPONSE | jq -r '.data.progress // "..."'))"
doneCredits
Image credits follow a reserve-on-acceptance model:
1 credit per successful image
Each completed image consumes 1 credit from your account balance. Credits are reserved when the request is accepted (202) and finalised when the image reaches a terminal state.
Automatic refund on platform failure
If an image fails due to a platform or provider error, the reserved credit is automatically refunded. Validation failures (before processing begins) never reserve credits.
Test keys don't consume credits
Requests made with sk_test_ keys do not deduct credits. Use them freely during development.
Monitor your credit balance and usage with the Usage endpoint.