Skip to content
CreatePricing
Documentation menu

api.char-gen.com/partner/v1

CharGen API

Build D&D content generation into your own product: images, video, audio, 3D, and structured entities, backed by a prepaid USD wallet.

The CharGen API lets your product create the same content CharGen users make on char-gen.com: images, video, audio, 3D models, animated pixel sprites, and structured D&D entities such as NPCs, settlements, dungeons, and full characters. It is a small REST API. One endpoint creates a generation, one polls it, and a handful more cover your account, the model catalog, and billing.

The base URL for every call is:

text
https://api.char-gen.com

Requests and responses are JSON. Errors use application/problem+json (RFC 9457).

Authentication#

Every endpoint except the public pricing list requires an API key in the Authorization header:

text
Authorization: Bearer live_<key id>_<secret>

Keys are minted in the developer portal. The full key is shown exactly once, when you mint it. After that, the portal only shows the key id and the last 4 characters, so copy the key into your secret manager at mint time.

A few rules for handling keys:

  • Keys start with live_ or test_. The prefix is a label for your own bookkeeping. Both kinds authenticate against the same API and bill the same wallet.
  • Keys are secrets. Keep them server-side. Never ship them in client code, mobile apps, or public repositories.
  • Each key carries scopes: generate:image, generate:video, generate:audio, generate:model3d, generate:entity, read, and billing. A request without the required scope returns 403 with an empty body.
  • If a key leaks, revoke it in the portal and mint a new one. Revocation takes effect immediately.

A missing or invalid key returns 401 with an empty body.

Your first generation#

Renders (images, video, audio, 3D models) go to POST /partner/v1/generations; structured entities (NPCs, dungeons, pantheons and 15 more) go to POST /partner/v1/entities. Both take the same body shape, two fields: type and input. The Idempotency-Key header is required on both (more on it below).

bash
export CHARGEN_API_KEY="live_..."
 
curl -s -X POST "https://api.char-gen.com/partner/v1/generations" \
  -H "Authorization: Bearer $CHARGEN_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "image",
    "input": {
      "model": "GPT Image 2",
      "prompt": "a dwarf battle cook in a cluttered tavern kitchen, oil painting",
      "size": "1024x1024"
    }
  }'

The response is 202 Accepted with a Location header pointing at the status endpoint:

json
{ "id": "0b7a9f6e-3c2d-4e8a-9c1f-2d5b8a7e4f01", "status": "queued", "priceUsd": 0.12 }

priceUsd is the amount this generation drew from your wallet. The value above is an example; the pricing table is the live source.

Then poll:

bash
curl -s "https://api.char-gen.com/partner/v1/generations/0b7a9f6e-3c2d-4e8a-9c1f-2d5b8a7e4f01" \
  -H "Authorization: Bearer $CHARGEN_API_KEY"
json
{
  "id": "0b7a9f6e-3c2d-4e8a-9c1f-2d5b8a7e4f01",
  "status": "succeeded",
  "assets": [
    {
      "url": "https://...",
      "webpUrl": "https://...",
      "thumbnailUrl": "https://...",
      "videoUrl": null
    }
  ]
}

Idempotency#

The Idempotency-Key header is required on every generation POST, to /generations and /entities alike. Any string from 1 to 200 characters works; a UUID per logical generation is the easy choice.

The key is claimed BEFORE we dispatch or bill anything, which gives you these guarantees:

  • Two concurrent requests with the same key can never double-bill. The second one returns 409 (empty body) while the first is still in flight.
  • A retry with the same key within 24 hours returns the ORIGINAL generation (202 with the original id and price) instead of creating and billing a new one. Retrying a timed-out HTTP call with the same key is always safe.
  • If a request fails before work starts (for example 402 insufficient funds), the key is released. You can retry with the same key after topping up.
  • After 24 hours a key is purged and may be reused for a fresh generation.

If a call fails with a 5xx, retry with the SAME key. You will either replay the original result or dispatch cleanly, never pay twice.

Lifecycle and statuses#

A generation moves through four public statuses, always lowercase:

StatusMeaning
queuedAccepted, waiting for a worker.
processingA worker is generating.
succeededDone. Assets and/or result are ready.
failedTerminal failure. The charge is refunded to your wallet automatically.

Details worth knowing:

  • The create response always says queued.
  • Renders (image, video, audio, model3d) report processing from the first poll onward, then succeeded or failed. The output appears in assets.
  • Entity generations (npc, settlement, and friends) report queued until a worker picks them up. On success, the structured entity is in result (a JSON object) and the automatically rendered portrait appears in assets once it finishes. assets can be empty while the portrait is still rendering, so poll until it shows up if you want the image.
  • result is omitted from the JSON entirely for renders.

Poll every few seconds. Images usually finish in well under a minute; video can take several minutes; entities typically take under a minute for the text plus the portrait render time.

What you can generate#

Renders#

type is one of image, video, audio, model3d. The input object takes:

FieldApplies toNotes
modelallRequired. Exact name from the catalog (see Models below).
promptallThe generation prompt. For Tripo Smart Segment and Tripo Parts it is the optional part hint (see 3D tools).
referenceImageUrlimage, video, model3dURL of a reference image. Required for models flagged "needs reference image".
sourceModelUrlmodel3dRequired for the 3D tools: an https link to the GLB, glTF, FBX, OBJ or STL file to process. Every other model3d model returns 400 when it is set.
referenceVideoUrlaudioURL of a reference video (for models that score to picture).
sizeimageShortcut for requestFields.Size, e.g. "1024x1024".
numImagesimageShortcut for requestFields["Number of Images"].
requestFieldsallPer-model options, listed below.

requestFields accepts ONLY these keys; anything else is silently dropped:

KeyValidationTypical use
Size<width>x<height>, e.g. 1024x1024Image dimensions
Number of Imagesinteger 1-10Image batch size
Qualityfree textModel-specific quality tier
Premium QualityLow, Medium, or HighGPT Image and Ideogram tiers
Durationnumber 1-600Audio length in seconds
Video Durationinteger 1-60Video length in seconds
Resolutionfree textVideo resolution
Aspect Ratiofree textOutput aspect ratio
Generate Audiotrue / falseVeo soundtrack toggle
Retopology Versionv3.0, v2.0, v1.03D clean up version (changes the price)
Quadtrue / false3D clean up to four-sided faces, v3.0 only (changes the price)
Face Limitinteger 100-2000003D clean up face count
Smart UVtrue3D clean up also unwraps new UVs, v3.0 only
Bake Texturestrue / false3D clean up and UV unwrap keep the textures
Rig Typeauto, biped, quadruped, hexapod, octopod, avian, serpentine, aquatic, others3D rig body type
Rig Spectripo, mixamo, actorcore, unity, unreal, vrm3D rig bone names
Output Formatglb / fbx3D animate output file (Tripo Rig returns FBX)
Animationscomma list of 1-5 names, see 3D tools3D moves (changes the price)
Animate In Placetrue3D moves stay in place
Segment Versionv1.0 / v2.03D split and fill hidden surfaces engine
Segment Granularitybalanced, simple, detailed3D split by shape, v2.0 only
Split By Connectivitytrue / false3D split by shape, v2.0 only
Part Granularitymedium, coarse, fine3D split by name and Tripo Parts
Completion Modeai_completion / quick_cap3D fill hidden surfaces (changes the price)

All values are strings, including numbers and booleans: "requestFields": {"Video Duration": "5", "Generate Audio": "true"}.

3D tools#

The 3D tools take a 3D model and return a 3D model (GLB or FBX in assets). Send them as type: "model3d" with the tool's name as model:

modelWhat it doesIts requestFields
Tripo RetopologyClean up: rebuilds a dense model with the face count you choose. Returns FBXRetopology Version, Quad, Face Limit, Smart UV, Bake Textures
Tripo Smart UVUnwraps new UVs. Takes models up to 80,000 triangles, so clean up a dense model firstBake Textures
Tripo RigAdds a skeleton. Returns FBXRig Type, Rig Spec, Output Format
Tripo AnimateAdds a skeleton and up to 5 movesRig Type, Rig Spec, Output Format, Animations, Animate In Place
Tripo SegmentSplits the model into parts by shapeSegment Version, Segment Granularity, Split By Connectivity
Tripo Smart SegmentSplits the model into the parts your hint namesPart Granularity
Tripo Mesh CompleteFills the hidden surfaces between partsCompletion Mode, Segment Version
  • input.sourceModelUrl is required. It must be an https link on a public host whose path ends in .glb, .gltf, .fbx, .obj or .stl (the query and fragment are not read). Links with a user name or password, or to local or private addresses, are refused. Only the path is checked, not the file itself. Tripo Smart Segment needs a .glb. The tools take no referenceImageUrl.
  • prompt is used only by Tripo Smart Segment and Tripo Parts, as the part hint, for example "head, arms, sword". The other tools do not send it on, but it is still checked by moderation, so leave it empty.
  • Only Retopology Version, Quad, Completion Mode and Animations are checked: a value outside their list returns 400. For the other 3D keys, a value outside the list uses the default, and a Face Limit outside 100-200000 is left out.
  • Animations takes clip names separated by commas: idle, walk, run, dive, climb, jump, slash, shoot, hurt, fall, turn, and for other body types quadruped:walk, hexapod:walk, octopod:walk, serpentine:march, aquatic:march. Blank means walk. An unknown name, or more than 5, returns 400. The price depends on the number of clips.
  • Tripo Parts makes a new model, already split into parts, from referenceImageUrl (required), with an optional hint in prompt. It takes no sourceModelUrl.
  • Each call runs one tool. To run several in order, poll each call to succeeded, then send its model file as the next call's sourceModelUrl: an asset's videoUrl, or its url when videoUrl is empty. Tripo Retopology and Tripo Rig return FBX, so Tripo Smart Segment (GLB only) cannot follow them.
bash
curl -s -X POST "https://api.char-gen.com/partner/v1/generations" \
  -H "Authorization: Bearer $CHARGEN_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "model3d",
    "input": {
      "model": "Tripo Animate",
      "sourceModelUrl": "https://example.com/models/knight.glb",
      "requestFields": {
        "Animations": "idle,walk,run",
        "Rig Spec": "unity",
        "Animate In Place": "true",
        "Output Format": "fbx"
      }
    }
  }'

Entities#

Entities use their own endpoint, POST /partner/v1/entities. type is one of the 18 entity types:

text
npc, character, monster, faction, magicitem, hazard, settlement, region, tavern,
shop, building, dungeon, quest, pantheon, species, spellbook, poetry, puzzle

Every type accepts prompt, a freeform seed, plus typed fields specific to that generator. A monster takes crLevel, monsterType, role, difficulty and more; a character takes className, level, background; a dungeon takes numberOfLevels, trapDensity and a reproducible seed. The API reference lists every field per type. All fields are optional, and anything you leave out gets a sensible default.

bash
curl -s -X POST "https://api.char-gen.com/partner/v1/entities" \
  -H "Authorization: Bearer $CHARGEN_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "monster",
    "input": {
      "prompt": "a swamp-dwelling horror that mimics drowned voices",
      "monsterType": "Undead",
      "crLevel": "9",
      "partyLevel": "5",
      "difficulty": "Deadly"
    }
  }'

Polling is the same for both families: the returned id resolves at GET /partner/v1/generations/{id}.

Each entity generation returns the structured entity JSON in result and automatically renders a portrait image, which lands in assets. The portrait is billed separately as an image render (see Pricing).

Pixel Sprites#

Pixel Sprites makes animated pixel-art sprites from one picture of a character. It also makes spell effects and items as sets of animated parts. Sprite orders have their own endpoints, but they use the same wallet, the same Idempotency-Key rules and the same status endpoint as other generations.

EndpointScopeUse
POST /partner/v1/spritesgenerate:imageOrder sprites of a character.
POST /partner/v1/sprites/effectsgenerate:imageOrder a spell effect or an item.
POST /partner/v1/sprites/quotegenerate:imageGet the price of a character order. Nothing is charged and no picture is downloaded.
POST /partner/v1/sprites/effects/quotegenerate:imageGet the price of an effect or item order. Nothing is charged.
GET /partner/v1/sprites/cataloguereadList the moves, game moves, facings, styles, presets, parts, elements and limits.

A quote takes the same body as its order and returns { "priceUsd": 0.24, "kind": "character" }.

Characters#

Give the picture of the character in one of these three ways. Use only one in each order:

  • sourceImageUrl: an https link to a PNG, JPEG or WebP picture, 25 MB or less. CharGen downloads it one time. If you send the same picture again, you get the same character.
  • sourceGenerationId: the id of one of your image generations. CharGen uses its first picture.
  • characterId: the characterId from an earlier sprite order. Use it to add moves to a character you already have, so that all its moves match.

Then select what to draw. One order can have all of these:

FieldWhat it orders
movesPortrait moves: idle, hello, attack, cast, victory, laugh, defeat, angry.
typedMovesMoves in your own words, each 120 characters or less. moves and typedMoves together can have 10 moves or less.
walk8true orders the walk in all 8 facings: S, SE, E, NE, N, NW, W, SW.
gameMovesGame moves and their facings, for example { "move": "sword", "facings": ["E", "W"] }. The game moves are idle, walk, run, sprint, sword, punch, cast, hurt, death, jump and roll. Each move and facing is one row, and an order can have 24 rows or less.
stylefine (the default) or chunky pixels.
detailsWhat the character holds or wears: 6 items or less, each 60 characters or less.
titleA name for the character's sprites.
bash
curl -s -X POST "https://api.char-gen.com/partner/v1/sprites" \
  -H "Authorization: Bearer $CHARGEN_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceImageUrl": "https://example.com/hero.png",
    "moves": ["idle", "attack"]
  }'
json
{
  "id": "6f1c2a8e-9b3d-4c7a-8e21-5d4b3a2f1e09",
  "status": "queued",
  "priceUsd": 0.24,
  "kind": "character",
  "characterId": "c41e7b2d-0a9f-4e6b-b8d3-7f2a1c5e9d40",
  "style": "fine"
}

The first order for a character also draws its base sprite. If you send a second order for the same character while the first one is still drawing, the API returns 409 with the code sprite_base_drawing and Retry-After: 30. Send the order again after that time.

Effects and items#

FieldNotes
kindRequired. effect or item.
presetA preset from the catalogue, for example fireball. Use preset or words, not both.
wordsWhat to draw, in your own words, when you do not use a preset.
elementfire, ice, lightning, poison, holy, dark, arcane or nature. Every part is drawn in this element.
partsThe parts to draw, by key. If you do not give parts, you get the preset's own parts.
customPartsParts in your own words: { "text": "...", "plays": "loop" }, where plays is loop, once or hold.
referenceImageUrl, referenceGenerationIdItems only. A picture to draw the item from.
characterId, styleEffects only. Match the effect to the sprites of this character.
titleA name for the effect or item.

An order can have 10 parts or less. The answer has an effectId in place of a characterId.

bash
curl -s -X POST "https://api.char-gen.com/partner/v1/sprites/effects" \
  -H "Authorization: Bearer $CHARGEN_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "effect", "preset": "fireball", "element": "fire" }'

Sprite results#

Poll GET /partner/v1/generations/{id}, as for other generations. The order shows processing while it draws. It shows succeeded when one or more rows are drawn, and failed when no row was drawn. A failed order is refunded in full.

result has one row for each move, facing or part:

json
{
  "id": "6f1c2a8e-9b3d-4c7a-8e21-5d4b3a2f1e09",
  "status": "succeeded",
  "result": {
    "kind": "character",
    "characterId": "c41e7b2d-0a9f-4e6b-b8d3-7f2a1c5e9d40",
    "style": "fine",
    "priceUsd": 0.24,
    "refundedUsd": 0.12,
    "rows": [
      {
        "move": "idle",
        "status": "done",
        "gif": "https://...",
        "gif4x": "https://...",
        "emoji": "https://...",
        "strip": "https://...",
        "frameSize": 128,
        "frameCount": 8,
        "durationsMs": [120, 120, 120, 120, 120, 120, 120, 120]
      },
      { "move": "attack", "status": "failed", "refundedUsd": 0.12 }
    ]
  },
  "assets": [
    { "url": "https://...", "webpUrl": null, "thumbnailUrl": "https://...", "videoUrl": null }
  ]
}
  • status on a row is drawing, done or failed.
  • A row that is done has its GIF (gif), a GIF 4 times larger (gif4x), a small emoji picture (emoji) and a PNG strip of all its frames (strip), with frameSize, frameCount and durationsMs. Some rows also have larger HD files (hdGif, hdGif2x, hdStrip) and chunky files (chunkyGif, chunkyGif4x, chunkyStrip).
  • Rows of the walk and of game moves have facing. Game-move rows also have plays: loop (idle, walk, run, sprint), hold (death) or once (all other game moves).
  • Typed moves show "move": "typed" and their own words in text.
  • assets has one entry for each row that is done: url is its 4x GIF and thumbnailUrl its GIF.

Sprite prices and refunds#

Each move, facing or part has its own price. The live prices are in the pricing table, and your organisation's prices come from the quote endpoints. A row that does not draw gives back its share of the price. You get this refund even if you never poll the order.

Sprite errors#

Sprite orders can return these errors, in addition to the general errors:

StatuscodeWhen
409sprite_base_drawingThe character's first order is still drawing. Retry after the Retry-After time (30 seconds).
422content_refusedContent moderation refused the words or the picture. Nothing is charged.
400, 422source_unreadableCharGen could not use the picture link. reason is invalid_url, blocked_destination, timeout, too_large, invalid_image or upstream_failure.
429rate_limitedYour organisation sent more than 600 picture links in one hour. Retry after the Retry-After time (600 seconds). To add moves to a character, use its characterId, not its link.

Models#

The model catalog is the contract: model must match a catalog name EXACTLY, including spaces and capitalisation. An unknown name returns 400 before anything is billed, with a pointer to the list.

Two ways to read the catalog:

  • GET /partner/v1/models (requires the read scope) returns every model with the price YOUR organisation pays, computed from your pinned price version.
  • The public pricing table on this page shows the current standard prices without authentication.

Each catalog row carries:

  • priceUsd: the price for a representative base configuration (image: 1024x1024, one image; video: default clip; per-1000-character pricing for some TTS models).
  • pricingNote: present when request options change the price (longer durations, premium quality, generated audio, larger sizes). If a model has a note, read it before assuming the base price.
  • requiresReferenceImage: when true, the model only works image-to-image and a request without referenceImageUrl is rejected with 400.

Four video models require an explicit duration. Calls without requestFields["Video Duration"] return 400 for: Kling 2.6 Pro, Kling O1, Kling O1 Standard, and Grok Imagine Video.

Pricing#

Your organisation has a prepaid wallet, held in US dollars. Every generation draws from it at dispatch time; the create response tells you what it drew.

  • Renders are priced per model. The exact figure comes from your price version (below) and the model's cost; the pricing table on this page lists every model.
  • Entities charge a flat per-type fee. Most types use the default fee; heavier multi-part generators (character, spellbook, pantheon, dungeon) cost slightly more. The live fees are in the table below.
  • Entity portraits: the automatic image render attached to an entity generation is billed separately, as a normal image render at your image rate.
  • Failures refund automatically. If a generation fails on our side, the amount drawn for it is credited back to your wallet. You never pay for failed work.

Price versions and grandfathering#

Prices are published as dated price versions. Your organisation is pinned to a version when you sign up, and you stay on it (you are grandfathered) until you opt in to a newer one or we migrate you with at least 30 days' email notice. Upstream cost changes never silently reprice your existing integration.

Funding the wallet#

Top up through Stripe Checkout, either in the portal billing tab or via the API:

bash
curl -s -X POST "https://api.char-gen.com/partner/v1/billing/checkout-session" \
  -H "Authorization: Bearer $CHARGEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amountUsdCents": 5000 }'
json
{ "checkoutUrl": "https://checkout.stripe.com/c/pay/...", "sessionId": "cs_..." }

Open checkoutUrl in a browser to pay. Top-ups are between $10.00 (1000 cents) and $100,000.00 (10000000 cents). The wallet is credited when the payment clears, usually within a moment of checkout completing. Where we are required to collect tax, Stripe Tax adds it at checkout; listed prices exclude taxes.

When the wallet cannot cover a generation, the API returns 402 (see Errors) and nothing is dispatched or billed.

API credits are not Gold#

CharGen consumer accounts use Gold. The API does not. API usage draws from your organisation's prepaid USD wallet, which is entirely separate from any Gold balance on a personal CharGen account, including subscriptions. Gold cannot be spent through the API and API credits cannot be spent on char-gen.com.

Rate limits#

Each API key may make up to 120 requests per minute. The bucket refills continuously, so short bursts above 2 requests/second are fine until the bucket drains. Over the limit, the API returns 429 with a Retry-After: 60 header:

json
{
  "type": "https://docs.chargen.example/errors/rate-limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Per-key rate limit exceeded. Retry after 60 seconds.",
  "code": "rate_limited"
}

Back off and retry after the indicated delay. The limit is per key, so separate keys for separate workloads also separate their budgets.

Errors#

Errors are application/problem+json objects with a machine-readable code property. Example, a wallet that cannot cover the price:

json
{
  "type": "https://docs.chargen.example/errors/insufficient-funds",
  "title": "Payment Required",
  "status": 402,
  "detail": "Insufficient partner balance",
  "code": "insufficient_funds",
  "balanceUsd": 0.52,
  "requiredUsd": 1.08
}

The full set:

StatuscodeWhen
400bad_requestInvalid input: unknown model, missing required field, out-of-range value. The detail says what to fix.
401(empty body)Missing, malformed, revoked, or expired API key.
402insufficient_fundsWallet balance below the price. Includes balanceUsd and requiredUsd. Top up and retry with the same Idempotency-Key.
403(empty body)The key lacks the required scope.
422content_refusedContent moderation refused the request (sprite orders). Nothing is charged.
404not_foundUnknown generation id, or a generation that belongs to a different organisation.
409(empty body)A request with the same Idempotency-Key is still in flight. Wait and poll, or retry shortly to replay its result.
429rate_limitedOver 120 requests/min on this key. Honour Retry-After.
500server_error, wallet_missingOur fault. Safe to retry with the SAME Idempotency-Key.

End-to-end examples#

Generate, poll until done, and surface a top-up link when funds run out.

JavaScript (fetch)#

javascript
const BASE = "https://api.char-gen.com";
const KEY = process.env.CHARGEN_API_KEY;
 
async function createGeneration() {
    const res = await fetch(`${BASE}/partner/v1/generations`, {
        method: "POST",
        headers: {
            Authorization: `Bearer ${KEY}`,
            "Idempotency-Key": crypto.randomUUID(),
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            type: "image",
            input: {
                model: "GPT Image 2",
                prompt: "a dwarf battle cook in a cluttered tavern kitchen, oil painting",
                size: "1024x1024",
            },
        }),
    });
    if (res.status === 402) {
        const problem = await res.json();
        const topUpUrl = await createTopUp(2000);
        throw new Error(
            `Balance $${problem.balanceUsd} is below $${problem.requiredUsd}. Top up: ${topUpUrl}`
        );
    }
    if (!res.ok) throw new Error(`create failed: HTTP ${res.status}`);
    const created = await res.json();
    return created.id;
}
 
async function waitForGeneration(id, timeoutMs = 300_000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        const res = await fetch(`${BASE}/partner/v1/generations/${id}`, {
            headers: { Authorization: `Bearer ${KEY}` },
        });
        if (!res.ok) throw new Error(`poll failed: HTTP ${res.status}`);
        const body = await res.json();
        if (body.status === "succeeded" || body.status === "failed") return body;
        await new Promise((r) => setTimeout(r, 3000));
    }
    throw new Error(`generation ${id} did not finish in ${timeoutMs}ms`);
}
 
async function createTopUp(amountUsdCents) {
    const res = await fetch(`${BASE}/partner/v1/billing/checkout-session`, {
        method: "POST",
        headers: {
            Authorization: `Bearer ${KEY}`,
            "Content-Type": "application/json",
        },
        body: JSON.stringify({ amountUsdCents }),
    });
    if (!res.ok) throw new Error(`top-up failed: HTTP ${res.status}`);
    const body = await res.json();
    return body.checkoutUrl;
}
 
const id = await createGeneration();
const done = await waitForGeneration(id);
console.log(done.status, done.assets);

Python (requests)#

python
import os
import time
import uuid
 
import requests
 
BASE = "https://api.char-gen.com"
KEY = os.environ["CHARGEN_API_KEY"]
AUTH = {"Authorization": f"Bearer {KEY}"}
 
 
def create_generation():
    resp = requests.post(
        f"{BASE}/partner/v1/generations",
        headers={**AUTH, "Idempotency-Key": str(uuid.uuid4())},
        json={
            "type": "image",
            "input": {
                "model": "GPT Image 2",
                "prompt": "a dwarf battle cook in a cluttered tavern kitchen, oil painting",
                "size": "1024x1024",
            },
        },
        timeout=30,
    )
    if resp.status_code == 402:
        problem = resp.json()
        top_up_url = create_top_up(2000)
        raise RuntimeError(
            f"Balance ${problem['balanceUsd']} is below ${problem['requiredUsd']}. "
            f"Top up: {top_up_url}"
        )
    resp.raise_for_status()
    return resp.json()["id"]
 
 
def wait_for_generation(generation_id, timeout_seconds=300):
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        resp = requests.get(
            f"{BASE}/partner/v1/generations/{generation_id}", headers=AUTH, timeout=30
        )
        resp.raise_for_status()
        body = resp.json()
        if body["status"] in ("succeeded", "failed"):
            return body
        time.sleep(3)
    raise TimeoutError(f"generation {generation_id} did not finish in {timeout_seconds}s")
 
 
def create_top_up(amount_usd_cents):
    resp = requests.post(
        f"{BASE}/partner/v1/billing/checkout-session",
        headers=AUTH,
        json={"amountUsdCents": amount_usd_cents},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["checkoutUrl"]
 
 
generation_id = create_generation()
result = wait_for_generation(generation_id)
print(result["status"], result.get("assets"))

Terms#

Use of the API is governed by the CharGen API Terms of Service (v1.1). Highlights: the wallet is prepaid, failed generations are auto-refunded, you may embed Output in your own product but not re-expose the raw API, and price versions are grandfathered with 30 days' notice before any migration.

Live pricing table

Current standard prices in USD, straight from the API. Your organisation's exact prices come from GET /partner/v1/models with your key.

ModelBase pricePricing notes
Amazon Nova Canvas$0.072
Anima$0.048Resolution=1.5k prices at 8 gold versus the 1k default of 4.
Chroma$0.048
CogView 4$0.108
Ernie Image$0.036
Ernie Image Turbo$0.036
Fibo 2.0$0.072
Flux.Dev$0.012
Flux Krea$0.024
Krea 2 Large$0.096
Krea 2 Medium$0.048
Krea 3$0.060Priced by Size at $0.03 per megapixel, and the same again for each reference image. Berry and Jam cost the same. Shown price is 1024x1024 with no reference (5 gold).
Flux.Pro$0.072
Flux.Pro (Ultra)$0.096
Flux.Pro (Ultra) ReduxNeeds reference image$0.084
Flux.Schnell$0.012
Flux SubjectNeeds reference image$0.048
Flux 2 Dev$0.024
Flux 2 Flash$0.012
Flux 2 Flex$0.072
Flux 2 Pro$0.048
Flux 2 Max$0.084
Hunyuan Image 2.1$0.048
Hunyuan Image 3.0$0.108
Hunyuan Image 3.0 Instruct$0.120
Ideogram 3.0$0.048Premium Quality multiplies the price: Medium x2, High x3.
Ideogram V4$0.060Premium Quality multiplies the price: Medium x2, High x3.
Ideogram V4.5$0.108Premium Quality sets the price: Low 5, Medium 9 (default), High 30. Resolution does not change the price.
Flux 3$0.084Resolution sets the price: 7 gold at 1k (default), 17 at 2k, 88 at 4k. Text-to-image and edit cost the same.
Flux 3 Klein$0.024Priced by Size: 2 gold when the longer side is 2048 or less, 5 gold above that. Text-to-image and edit cost the same. Shown price is 1024x1024.
MAI Image 2.5$0.096Priced per image by prompt length.
MAI Image 2.5 Pro$0.300Priced per image by prompt length. Supplying a reference image uses the edit route, which the provider prices higher than text-to-image (40 gold vs 25 at a short prompt).
Cosmos 3 Super$0.072
Imagen 4$0.060
Imagen 4 (Ultra)$0.084
Imagen 3$0.060
Imagen 3 Fast$0.036
Jib Mix Qwen$0.036
Juggernaut Flux$0.012
Juggernaut Flux Lightning$0.012
Juggernaut Flux Pro$0.036
Gemini Flash 2.5$0.048
GPT Image 1$0.120Priced by Premium Quality (Low/Medium/High); shown price is Medium.
GPT Image 1.5$0.072Priced by Premium Quality and Size; shown price is Medium at 1024x1024.
GPT Image 2$0.096Priced by Premium Quality (Low/Medium/High); shown price is Medium.
GPT Image 2.5 Flare$0.036Priced by Premium Quality (Low/Medium/High/XHigh/Max) and output size; shown price is Medium at 1024x1024.
GPT Image 2.5 Sunburst$0.036Priced by Premium Quality (Low/Medium/High/XHigh/Max) and output size; shown price is Medium at 1024x1024.
GPT Image 2.6 Flare$0.096Priced by Premium Quality (Low/Medium/High/XHigh/Max) and output size; shown price is Medium at 1024x1024. Still images only on this API.
GPT Image 2.6 Sunburst$0.096Priced by Premium Quality (Low/Medium/High/XHigh/Max) and output size; shown price is Medium at 1024x1024. Still images only on this API.
Grok Imagine Image$0.036
Grok Imagine Image 2.0$0.084Supplying a reference image uses the edit route, which the provider prices higher than text-to-image (9 gold vs 7). Resolution and quality do not change the price.
Kling 3.0 Image$0.048
Kontext Dev$0.024
Kontext Pro$0.048
Kontext Max$0.084
Lumina Image V2$0.084
Midjourney$0.120
MiniMax Image$0.024
Muse Image$0.024
Nano Banana Pro$0.156Sizes above 2048px double the price.
Nano Banana 2.1$0.072Resolution prices at 6 gold (1k), 9 gold (2k), or 19 gold (4k). Each reference image adds a small surcharge.
Nano Banana 2$0.096Price scales with Size.
Nano Banana 2 Fast$0.048Sizes above 2048px double the price.
Nano Banana 2 Lite$0.048
Neta Lumina$0.024
OmniGen V2$0.204
Qwen Image$0.036
Qwen Image Max$0.084
Qwen Image 2.0$0.036
Qwen Image 2.0 Pro$0.084
Qwen Image 2.1$0.036Resolution prices text-to-image at 3/6/11 gold for 1k/1.5k/2k. A reference image uses the edit route, which also adds gold per extra reference (5 gold at 1k with one image).
Qwen Image 3$0.060
Qwen Image 3.0 Pro$0.072Sizes above 2,097,152 total pixels (e.g. 1536x1536) price at 12 gold versus 6 below that.
Recraft V3$0.060
Recraft V4$0.060
Recraft V4 Pro$0.300
Recraft V4.1 Flash$0.024
Reve V1Needs reference image$0.048
Reve V2.1$0.396
Riverflow 2.0 Pro$0.144
RunDiffusion Photo Flux$0.024
Sana$0.012Price scales with Size (per megapixel).
Seedream 3.0$0.036
Seedream 3.1$0.036
Seedream 4.0$0.048
Seedream 4.5$0.048
Seedream 5.0 Lite$0.048
Seedream 5.0 Flash$0.048
Seedream 5.0 Pro$0.084Resolution=2k prices at 13 gold versus the 1k default of 7.
Wan 2.2 Image$0.036
Wan 2.2 Realism$0.036
Wan 2.5 Image$0.048
Wan 2.6 Image$0.048
Wan 2.7 Image$0.048
Wan 2.7 Image Pro$0.096
Z Image Turbo$0.012
Z Image Base$0.012

Entity generation fees

Flat fee per generation. The automatic portrait render is billed separately at the image rate.

Entity typeAPI type stringFee
NPCnpc$0.040
Charactercharacter$0.060
Character classcharacterclass$0.040
Monstermonster$0.040
Factionfaction$0.040
Magic itemmagicitem$0.040
Hazardhazard$0.040
Settlementsettlement$0.040
Regionregion$0.040
Taverntavern$0.040
Shopshop$0.040
Buildingbuilding$0.040
Dungeondungeon$0.080
Questquest$0.040
Pantheonpantheon$0.080
Speciesspecies$0.040
Spellbookspellbook$0.060
Poetrypoetry$0.040
Puzzlepuzzle$0.040
Worldworld$0.040

Pixel Sprites

Priced for each move or part you order. A move or part that does not draw is refunded.

What you orderPrice
A move (preset or in your own words)$0.120
The walk in 8 directions$0.960
A game move, for each facing$0.300
Effect or item, first part$0.120
Effect or item, each part after the first$0.096