Character Studio
Character Studio keeps one person, mascot or product character recognisable across every image you generate. You create a character once, from one to five photos or from a short description. FOTOhub AI turns it into an identity sheet (several views of the same character: front, three-quarter, profile, expression, full body, neutral outfit), checks every view against your references and keeps the ones that pass. From then on you generate with character_id and the references travel with the request.
Capabilities
- Create from photos or from a prompt: 1-5 photos of a real person (consent required), or a description for a fictional character
- Identity check on every result: each view, and each generation, is scored against your references
- Fair billing: a sheet with too few accepted views is refunded in full; the character-lock fee is charged only when a generated image passes the identity check
- Works with the image models that accept reference images:
GET /v1/characters/configlists them with their reference caps - Free helpers: quotes,
resolve(the references and prompt prefix for a model, if you prefer to call the model yourself), rename, delete
Availability
Character Studio is rolled out in stages. Until it is enabled for your account, every route answers 403 with {"error": "feature_disabled", "feature": "character_studio"} (and "min_tier" when the reason is your plan). Nothing is charged.
Endpoints
| Method | Path | Charged | Description |
|---|---|---|---|
| GET | /v1/characters/config | no | Views, photo limits, consent declaration (all locales, with hashes), where each step runs, models with their reference caps, your limits and today's usage |
| POST | /v1/characters/quote | no | Price, availability and ETA of a new character, plus limit_reached |
| POST | /v1/characters | yes | Create a character and start its identity sheet |
| GET | /v1/characters | no | Your characters, limits and usage |
| GET | /v1/characters/{id} | no | One character: views with identity scores, sheet job state |
| PATCH | /v1/characters/{id} | no | Rename, describe, change the strength, exclude or re-include views |
| DELETE | /v1/characters/{id} | no | Delete (active jobs are cancelled and refunded) |
| POST | /v1/characters/{id}/sheet | yes | Regenerate the identity sheet |
| POST | /v1/characters/{id}/resolve | no | References, prompt prefix and strength for one model |
| POST | /v1/characters/{id}/generations/quote | no | The model's price plus the lock fee, availability, ETA |
| POST | /v1/characters/{id}/generations | yes | A character-locked image (returns a job) |
| GET | /v1/characters/{id}/jobs | no | The character's recent jobs |
| GET | /v1/characters/jobs/{job_id} | no | Job status, result or error, refund state |
| POST | /v1/characters/jobs/{job_id}/cancel | no | Cancel a queued or running job; refunded once |
Base URL: https://apis.fotohub.appAuthentication: Authorization: Bearer fh_live_your_api_key (see Authentication)
Pricing
Prices are never hard-coded in this page. They come from the API:
- Every
quoteanswers the exact price for your account increditsandusd, before anything is charged. - The USD wallet rates are listed by
GET /v1/pricing: the price keys areaiw-character-create(a new sheet) andaiw-character-lock(the fee on top of a generation). Ask for one withGET /v1/pricing?model=aiw-character-create. While the feature is disabled,GET /v1/pricing?model=aiw-*answers404for its keys. - A generation costs the chosen model's own price (charged when you submit) plus the lock fee, which is charged only when the identity check accepts the image.
quote_generationreturns both and theirtotal.
Create a character
1. Read the config
curl https://apis.fotohub.app/v1/characters/config \
-H "Authorization: Bearer $FOTOHUB_API_KEY"You need three things from it: consent.version, consent.texts[<locale>].sha256 (the declaration the person agrees to) and limits (how many characters and new sheets your plan includes).
2. Quote
curl https://apis.fotohub.app/v1/characters/quote \
-H "Authorization: Bearer $FOTOHUB_API_KEY" -H "Content-Type: application/json" \
-d '{"name": "Ada", "source": "prompt", "prompt": "a friendly red-haired pirate captain, freckles"}'available: false carries a reason; the sheet is not offered and nothing is charged. limit_reached (character_limit or daily_sheets) tells you creating would be refused with a 429.
3. Create
from fotohub import FotoHub
client = FotoHub() # reads FOTOHUB_API_KEY
cfg = client.characters.config()
consent = {
"subject": "self", # self | consented_person | fictional
"version": cfg["consent"]["version"],
"locale": "en",
"sha256": cfg["consent"]["texts"]["en"]["sha256"],
}
created = client.characters.create(
"Ada",
source="photos",
photos=["photos/<your-user-id>/ada-1.jpg", "photos/<your-user-id>/ada-2.jpg"],
consent=consent,
)
job = client.characters.wait_for_job(created["job"]["job_id"])
print(job["result"]["accepted_views"], job["result"]["identity_score"])import { FotoHub } from "fotohub";
const client = new FotoHub({ apiKey: process.env.FOTOHUB_API_KEY! });
const cfg = await client.characters.config() as any;
const created = await client.characters.create({
name: "Ada",
source: "photos",
photos: ["photos/<your-user-id>/ada-1.jpg", "photos/<your-user-id>/ada-2.jpg"],
consent: {
subject: "self", // self | consented_person | fictional
version: cfg.consent.version,
locale: "en",
sha256: cfg.consent.texts.en.sha256,
},
});
const job = await client.characters.waitForJob((created as any).job.job_id);
console.log(job.result);curl https://apis.fotohub.app/v1/characters \
-H "Authorization: Bearer $FOTOHUB_API_KEY" -H "Content-Type: application/json" \
-d '{
"name": "Ada",
"source": "photos",
"photos": ["photos/<your-user-id>/ada-1.jpg", "photos/<your-user-id>/ada-2.jpg"],
"consent": {"subject": "self", "version": "<consent.version>", "locale": "en", "sha256": "<sha256>"},
"request_id": "6f1d3c52-8a0e-4b49-9c1f-0a2b3c4d5e6f"
}'| Field | Type | Description |
|---|---|---|
name | string | Required, 1-60 characters |
source | "photos" | "prompt" | Required |
kind | "face" | "non_face" | Default face; use non_face for mascots and product characters |
photos | string[] | 1-5 paths in your own storage, photos/<your user id>/.... A path that is not yours is a 422 |
prompt | string | The character in a few words (source: "prompt"; up to 1000 characters). Not allowed together with photos |
description | string | Up to 500 characters; used in later generations |
strength | number 0-1 | How strictly later generations keep the identity |
consent | object | Required for photos: subject, version, locale (en, pl, de), sha256 |
seed | integer | Reproducible sheet |
request_id | UUID | Idempotency key. The same request_id again returns the character it already created (replayed: true) and never bills twice. The SDKs generate one for you |
The answer is {"character": {...}, "job": {"job_id": ...}, "eta": {...}, "quote": {...}, "replayed": false}. The character's status is pending or generating until the sheet job succeeds (ready) or fails (failed).
Refund rule. The sheet is refunded in full when fewer than min_accepted_views views pass the identity check (the value is in config()).
Generate with a character
quote = client.characters.quote_generation(
character_id, "Ada on a bicycle in the rain, cinematic", model="seedream-5-0-260128"
)
print(quote["total"], quote["available"])
job = client.characters.generate(
character_id, "Ada on a bicycle in the rain, cinematic",
model="seedream-5-0-260128", aspect_ratio="3:4",
)
done = client.characters.wait_for_job(job["job"]["job_id"])
print(done["result"]["image"]["url"], done["result"]["identity"]["verified"])| Field | Type | Description |
|---|---|---|
model | string | An image model id that accepts reference images (config().models). Example: seedream-5-0-260128 |
prompt | string | 3-1500 characters |
aspect_ratio | string | 1:1 (default), 3:4, 4:3, 9:16, 16:9, 2:3, 3:2 |
strength | number 0-1 | Overrides the character's strength for this image |
seed | integer | |
request_id | string | 8-64 characters, A-Za-z0-9-. Idempotency key |
The finished job's result is { "character_id", "model", "image": {"url", "path"}, "identity": {..., "verified": bool}, "references_used", "prompt", "lock_fee": {"charged": bool, "price_key"} }.
If you would rather call an image model yourself, POST /v1/characters/{id}/resolve with {"model": "...", "strength": 0.8} returns the reference URLs, the prompt prefix and the effective strength. It is free.
Job states and refunds
GET /v1/characters/jobs/{job_id} returns status (queued, running, succeeded, failed, cancelled), stage, progress, eta, the result once it succeeded and an error when it failed or was cancelled. For a failed or cancelled job refunded is true once the charge went back, with a refund_message. Refunds happen exactly once. Cancelling a queued or running job (POST .../cancel) refunds it.
Consent, labelling and where processing happens
Consent is mandatory for photos. The declaration (who the person is: yourself, a person who consented, or a fictional person) is recorded with a timestamp, the declaration version and its hash. The record contains no photos and is kept for the period stated in
config().consent.retention_years, even after you delete the character, as proof that consent was given. Photos, sheet and previews are deleted with the character.Moderation. Names, descriptions and prompts are screened before anything is charged; a refusal is a
422with"charged": false.Labelling. Everything Character Studio produces is AI generated and should be labelled as such where you publish it. FOTOhub AI does not apply C2PA content credentials to these images (
config().labelling.c2paisfalse).Where each step runs. The authoritative list is
config().consent.residency. At the time of writing:Step Where Face analysis EU (FOTOhub CPU servers, Poland) Image review (identity check) United States (AI vision model hosted in the United States) Identity-sheet generation United States (FOTOhub GPU servers) or a partner image provider Storage of photos, sheet and results EU (FOTOhub storage, Poland) A generation with
character_idis processed wherever the model you choose runs: FOTOhub-operated servers in the EU, Switzerland (Zurich) or the United States, or a partner provider. Do not describe Character Studio as "processed only in the EU" in your own product copy or privacy policy: parts of it run in the United States.
Errors
Errors carry detail = {"error": "<code>", "message": "...", ...}. Refusals that happen before any charge say "charged": false.
| Status | error | Meaning |
|---|---|---|
| 401 | authentication_required | No or invalid key |
| 402 | insufficient_funds | Wallet too low; the body has required_usd, balance_usd, shortfall_usd, topup_url |
| 403 | feature_disabled | Not enabled for your account yet (reason, min_tier) |
| 404 | character_not_found | Not yours, or deleted |
| 409 | sheet_in_progress | The identity sheet is still being created |
| 422 | invalid_photo_path, invalid_photos, duplicate_photos, consent_required, prompt_required, photos_not_allowed, invalid_aspect_ratio, moderation codes | Fixable input; nothing charged |
| 429 | ceiling_exceeded | kind is character_limit, daily_sheets or active_jobs (with limit, used, retry_after_s); Retry-After header; nothing charged |
| 503 | backend_unavailable, store_unavailable | Temporarily not offered; Retry-After; not charged |
Related
- Image Generation: the models you can lock a character into
- Product Shot: put a product, not a person, in a scene
- Billing & Pricing and
GET /v1/pricing - Python SDK and TypeScript SDK

