Skip to content

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/config lists 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 ​

MethodPathChargedDescription
GET/v1/characters/confignoViews, 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/quotenoPrice, availability and ETA of a new character, plus limit_reached
POST/v1/charactersyesCreate a character and start its identity sheet
GET/v1/charactersnoYour characters, limits and usage
GET/v1/characters/{id}noOne character: views with identity scores, sheet job state
PATCH/v1/characters/{id}noRename, describe, change the strength, exclude or re-include views
DELETE/v1/characters/{id}noDelete (active jobs are cancelled and refunded)
POST/v1/characters/{id}/sheetyesRegenerate the identity sheet
POST/v1/characters/{id}/resolvenoReferences, prompt prefix and strength for one model
POST/v1/characters/{id}/generations/quotenoThe model's price plus the lock fee, availability, ETA
POST/v1/characters/{id}/generationsyesA character-locked image (returns a job)
GET/v1/characters/{id}/jobsnoThe character's recent jobs
GET/v1/characters/jobs/{job_id}noJob status, result or error, refund state
POST/v1/characters/jobs/{job_id}/cancelnoCancel 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 quote answers the exact price for your account in credits and usd, before anything is charged.
  • The USD wallet rates are listed by GET /v1/pricing: the price keys are aiw-character-create (a new sheet) and aiw-character-lock (the fee on top of a generation). Ask for one with GET /v1/pricing?model=aiw-character-create. While the feature is disabled, GET /v1/pricing?model=aiw-* answers 404 for 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_generation returns both and their total.

Create a character ​

1. Read the config ​

bash
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 ​

bash
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 ​

python
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"])
typescript
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);
bash
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"
  }'
FieldTypeDescription
namestringRequired, 1-60 characters
source"photos" | "prompt"Required
kind"face" | "non_face"Default face; use non_face for mascots and product characters
photosstring[]1-5 paths in your own storage, photos/<your user id>/.... A path that is not yours is a 422
promptstringThe character in a few words (source: "prompt"; up to 1000 characters). Not allowed together with photos
descriptionstringUp to 500 characters; used in later generations
strengthnumber 0-1How strictly later generations keep the identity
consentobjectRequired for photos: subject, version, locale (en, pl, de), sha256
seedintegerReproducible sheet
request_idUUIDIdempotency 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 ​

python
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"])
FieldTypeDescription
modelstringAn image model id that accepts reference images (config().models). Example: seedream-5-0-260128
promptstring3-1500 characters
aspect_ratiostring1:1 (default), 3:4, 4:3, 9:16, 16:9, 2:3, 3:2
strengthnumber 0-1Overrides the character's strength for this image
seedinteger
request_idstring8-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 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 422 with "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.c2pa is false).

  • Where each step runs. The authoritative list is config().consent.residency. At the time of writing:

    StepWhere
    Face analysisEU (FOTOhub CPU servers, Poland)
    Image review (identity check)United States (AI vision model hosted in the United States)
    Identity-sheet generationUnited States (FOTOhub GPU servers) or a partner image provider
    Storage of photos, sheet and resultsEU (FOTOhub storage, Poland)

    A generation with character_id is 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.

StatuserrorMeaning
401authentication_requiredNo or invalid key
402insufficient_fundsWallet too low; the body has required_usd, balance_usd, shortfall_usd, topup_url
403feature_disabledNot enabled for your account yet (reason, min_tier)
404character_not_foundNot yours, or deleted
409sheet_in_progressThe identity sheet is still being created
422invalid_photo_path, invalid_photos, duplicate_photos, consent_required, prompt_required, photos_not_allowed, invalid_aspect_ratio, moderation codesFixable input; nothing charged
429ceiling_exceededkind is character_limit, daily_sheets or active_jobs (with limit, used, retry_after_s); Retry-After header; nothing charged
503backend_unavailable, store_unavailableTemporarily not offered; Retry-After; not charged