Skip to main content
Documentation

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:

1

POST /v1/images

Submit the image request. Returns 202 Accepted with an image_id and estimated completion time.

2

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:

processing
completed
or
failed
StatusTerminalDescription
processingNoImage is being generated. The progress field provides a best-effort estimate.
completedYesImage is ready. The result.image_url field contains the CDN URL.
failedYesImage generation failed. The failure object contains the error code and message.

Progress field semantics

  • Range: 0.0 to 1.0 (inclusive).
  • Present only when status is "processing". Omitted from terminal states.
  • May be null if 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.

ScenarioResult
Same key + same payloadReturns the original 202 response with the same image_id. No duplicate processing.
Same key + different payloadReturns 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD.
Different key + any payloadCreates 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_PAYLOAD

Complete 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 // "..."'))"
done

Credits

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.