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:
https://api.char-gen.comRequests 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:
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_ortest_. 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, andbilling. A request without the required scope returns403with 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).
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:
{ "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:
curl -s "https://api.char-gen.com/partner/v1/generations/0b7a9f6e-3c2d-4e8a-9c1f-2d5b8a7e4f01" \
-H "Authorization: Bearer $CHARGEN_API_KEY"{
"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 (
202with 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
402insufficient 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:
| Status | Meaning |
|---|---|
queued | Accepted, waiting for a worker. |
processing | A worker is generating. |
succeeded | Done. Assets and/or result are ready. |
failed | Terminal failure. The charge is refunded to your wallet automatically. |
Details worth knowing:
- The create response always says
queued. - Renders (image, video, audio, model3d) report
processingfrom the first poll onward, thensucceededorfailed. The output appears inassets. - Entity generations (npc, settlement, and friends) report
queueduntil a worker picks them up. On success, the structured entity is inresult(a JSON object) and the automatically rendered portrait appears inassetsonce it finishes.assetscan be empty while the portrait is still rendering, so poll until it shows up if you want the image. resultis 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:
| Field | Applies to | Notes |
|---|---|---|
model | all | Required. Exact name from the catalog (see Models below). |
prompt | all | The generation prompt. For Tripo Smart Segment and Tripo Parts it is the optional part hint (see 3D tools). |
referenceImageUrl | image, video, model3d | URL of a reference image. Required for models flagged "needs reference image". |
sourceModelUrl | model3d | Required 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. |
referenceVideoUrl | audio | URL of a reference video (for models that score to picture). |
size | image | Shortcut for requestFields.Size, e.g. "1024x1024". |
numImages | image | Shortcut for requestFields["Number of Images"]. |
requestFields | all | Per-model options, listed below. |
requestFields accepts ONLY these keys; anything else is silently dropped:
| Key | Validation | Typical use |
|---|---|---|
Size | <width>x<height>, e.g. 1024x1024 | Image dimensions |
Number of Images | integer 1-10 | Image batch size |
Quality | free text | Model-specific quality tier |
Premium Quality | Low, Medium, or High | GPT Image and Ideogram tiers |
Duration | number 1-600 | Audio length in seconds |
Video Duration | integer 1-60 | Video length in seconds |
Resolution | free text | Video resolution |
Aspect Ratio | free text | Output aspect ratio |
Generate Audio | true / false | Veo soundtrack toggle |
Retopology Version | v3.0, v2.0, v1.0 | 3D clean up version (changes the price) |
Quad | true / false | 3D clean up to four-sided faces, v3.0 only (changes the price) |
Face Limit | integer 100-200000 | 3D clean up face count |
Smart UV | true | 3D clean up also unwraps new UVs, v3.0 only |
Bake Textures | true / false | 3D clean up and UV unwrap keep the textures |
Rig Type | auto, biped, quadruped, hexapod, octopod, avian, serpentine, aquatic, others | 3D rig body type |
Rig Spec | tripo, mixamo, actorcore, unity, unreal, vrm | 3D rig bone names |
Output Format | glb / fbx | 3D animate output file (Tripo Rig returns FBX) |
Animations | comma list of 1-5 names, see 3D tools | 3D moves (changes the price) |
Animate In Place | true | 3D moves stay in place |
Segment Version | v1.0 / v2.0 | 3D split and fill hidden surfaces engine |
Segment Granularity | balanced, simple, detailed | 3D split by shape, v2.0 only |
Split By Connectivity | true / false | 3D split by shape, v2.0 only |
Part Granularity | medium, coarse, fine | 3D split by name and Tripo Parts |
Completion Mode | ai_completion / quick_cap | 3D 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:
model | What it does | Its requestFields |
|---|---|---|
Tripo Retopology | Clean up: rebuilds a dense model with the face count you choose. Returns FBX | Retopology Version, Quad, Face Limit, Smart UV, Bake Textures |
Tripo Smart UV | Unwraps new UVs. Takes models up to 80,000 triangles, so clean up a dense model first | Bake Textures |
Tripo Rig | Adds a skeleton. Returns FBX | Rig Type, Rig Spec, Output Format |
Tripo Animate | Adds a skeleton and up to 5 moves | Rig Type, Rig Spec, Output Format, Animations, Animate In Place |
Tripo Segment | Splits the model into parts by shape | Segment Version, Segment Granularity, Split By Connectivity |
Tripo Smart Segment | Splits the model into the parts your hint names | Part Granularity |
Tripo Mesh Complete | Fills the hidden surfaces between parts | Completion Mode, Segment Version |
input.sourceModelUrlis required. It must be anhttpslink on a public host whose path ends in.glb,.gltf,.fbx,.objor.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 Segmentneeds a.glb. The tools take noreferenceImageUrl.promptis used only byTripo Smart SegmentandTripo 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 ModeandAnimationsare checked: a value outside their list returns400. For the other 3D keys, a value outside the list uses the default, and aFace Limitoutside 100-200000 is left out. Animationstakes clip names separated by commas:idle,walk,run,dive,climb,jump,slash,shoot,hurt,fall,turn, and for other body typesquadruped:walk,hexapod:walk,octopod:walk,serpentine:march,aquatic:march. Blank meanswalk. An unknown name, or more than 5, returns400. The price depends on the number of clips.Tripo Partsmakes a new model, already split into parts, fromreferenceImageUrl(required), with an optional hint inprompt. It takes nosourceModelUrl.- Each call runs one tool. To run several in order, poll each call to
succeeded, then send its model file as the next call'ssourceModelUrl: an asset'svideoUrl, or itsurlwhenvideoUrlis empty.Tripo RetopologyandTripo Rigreturn FBX, soTripo Smart Segment(GLB only) cannot follow them.
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:
npc, character, monster, faction, magicitem, hazard, settlement, region, tavern,
shop, building, dungeon, quest, pantheon, species, spellbook, poetry, puzzleEvery 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.
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.
| Endpoint | Scope | Use |
|---|---|---|
POST /partner/v1/sprites | generate:image | Order sprites of a character. |
POST /partner/v1/sprites/effects | generate:image | Order a spell effect or an item. |
POST /partner/v1/sprites/quote | generate:image | Get the price of a character order. Nothing is charged and no picture is downloaded. |
POST /partner/v1/sprites/effects/quote | generate:image | Get the price of an effect or item order. Nothing is charged. |
GET /partner/v1/sprites/catalogue | read | List 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: anhttpslink 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: thecharacterIdfrom 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:
| Field | What it orders |
|---|---|
moves | Portrait moves: idle, hello, attack, cast, victory, laugh, defeat, angry. |
typedMoves | Moves in your own words, each 120 characters or less. moves and typedMoves together can have 10 moves or less. |
walk8 | true orders the walk in all 8 facings: S, SE, E, NE, N, NW, W, SW. |
gameMoves | Game 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. |
style | fine (the default) or chunky pixels. |
details | What the character holds or wears: 6 items or less, each 60 characters or less. |
title | A name for the character's sprites. |
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"]
}'{
"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#
| Field | Notes |
|---|---|
kind | Required. effect or item. |
preset | A preset from the catalogue, for example fireball. Use preset or words, not both. |
words | What to draw, in your own words, when you do not use a preset. |
element | fire, ice, lightning, poison, holy, dark, arcane or nature. Every part is drawn in this element. |
parts | The parts to draw, by key. If you do not give parts, you get the preset's own parts. |
customParts | Parts in your own words: { "text": "...", "plays": "loop" }, where plays is loop, once or hold. |
referenceImageUrl, referenceGenerationId | Items only. A picture to draw the item from. |
characterId, style | Effects only. Match the effect to the sprites of this character. |
title | A name for the effect or item. |
An order can have 10 parts or less. The answer has an effectId in place of a characterId.
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:
{
"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 }
]
}statuson a row isdrawing,doneorfailed.- 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), withframeSize,frameCountanddurationsMs. 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 haveplays:loop(idle, walk, run, sprint),hold(death) oronce(all other game moves). - Typed moves show
"move": "typed"and their own words intext. assetshas one entry for each row that is done:urlis its 4x GIF andthumbnailUrlits 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:
| Status | code | When |
|---|---|---|
| 409 | sprite_base_drawing | The character's first order is still drawing. Retry after the Retry-After time (30 seconds). |
| 422 | content_refused | Content moderation refused the words or the picture. Nothing is charged. |
| 400, 422 | source_unreadable | CharGen could not use the picture link. reason is invalid_url, blocked_destination, timeout, too_large, invalid_image or upstream_failure. |
| 429 | rate_limited | Your 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 thereadscope) 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: whentrue, the model only works image-to-image and a request withoutreferenceImageUrlis rejected with400.
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:
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 }'{ "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:
{
"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:
{
"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:
| Status | code | When |
|---|---|---|
| 400 | bad_request | Invalid 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. |
| 402 | insufficient_funds | Wallet 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. |
| 422 | content_refused | Content moderation refused the request (sprite orders). Nothing is charged. |
| 404 | not_found | Unknown 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. |
| 429 | rate_limited | Over 120 requests/min on this key. Honour Retry-After. |
| 500 | server_error, wallet_missing | Our 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)#
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)#
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.