The Firefly Custom Models API is two calls, not a platform. List your models with GET /v3/custom-models to read an assetId, then send it as customModelId on /v3/images/generate-async with a custom x-model-version header.
Two prerequisites sink more integrations than the calls do: a header the tutorials omit, and a model never shared with your technical account.
What the Custom Models API exposes
Adobe’s index gives the capability four bullets: train on a subject or a style, generate with it, combine it with style presets, share it. The one that matters for code is the last: you can grant applications access so they can list and use your models via the API.
The concepts page adds the intent: With custom models, you can capture and replicate distinctive brand aesthetics, characters, objects, or compositional arrangements.
One boundary: the reference documents a single custom-models operation, get /v3/custom-models, and Adobe’s route to a trained model runs through the Firefly app and your account team. Credentials live in our integration guide.
Subject models and style models
Adobe splits training into two modes. Subject models focus on representing specific characters, products, or objects. Style models emphasize aesthetic qualities like color palettes, patterns, brush stroke techniques, or illustrative cues.
Pick by what stays constant: a recurring character or product silhouette points at a subject model; a palette or line treatment across unrelated subjects at a style model. The modes do not stack, so “both” means two IDs.
What an asset ID is for
Custom models are assets hosted securely by Adobe, offering easy organization, versioning, and reuse, and the list response carries a version, a base model and a publication state.
Share the model before you query it
Adobe states the dependency directly: Once you've trained a custom model, you need to share it with your technical account so that the model is accessible to the List Custom Models API and the Text to Image API. Sharing is per model, by email, not per scope.
The console steps
- Open your project in the Adobe Developer Console and click the API credential link.
- Copy the Technical Account Email.
- In Firefly, open the model, click
"..."and choose Share. - Paste the address into Add people or groups and click Invite to edit.
An empty list is rarely a token problem: access can work while the invite still reads as pending, and The account may appear in the Pending state for sharing, but access to the custom model is granted.
List your models and read the asset ID
The endpoint is GET https://firefly-api.adobe.io/v3/custom-models, secured by X-Api-Key and AccessToken, plus two headers that appear only in the API reference. x-user-token carries A user token referencing the user's individual account and must be preceded by Bearer. x-request-id is an optional tracing header the server generates.
# Requires curl and jq. Exchanges credentials, then lists your custom models.
export CUSTOM_MODELS_CLIENT_ID=<your-client-id>
export CUSTOM_MODELS_CLIENT_SECRET=<your-client-secret>
export CUSTOM_MODEL_ID=<your-asset-id>
TOKEN=$(curl -s -X POST 'https://ims-na1.adobelogin.com/ims/token/v3' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials' \
-d "client_id=${CUSTOM_MODELS_CLIENT_ID}" \
-d "client_secret=${CUSTOM_MODELS_CLIENT_SECRET}" \
-d 'scope=openid,AdobeID,firefly_api,ff_apis' | jq -r .access_token)
curl -s -X GET 'https://firefly-api.adobe.io/v3/custom-models' \
-H 'Accept: application/json' \
-H "x-api-key: ${CUSTOM_MODELS_CLIENT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-user-token: Bearer ${TOKEN}" \
-H 'x-request-id: list-models-1' | jq '.custom_models[] | {assetId, displayName, trainingMode}'
The documented response is a wrapper, not a bare array.
{
"custom_models": [
{
"version": "1",
"assetName": "Warm Custom Model.ffcustommodel",
"trainingMode": "style",
"assetId": "urn:aaid:sc:VA6C2:bc1f46cd-be98-4a7b-9ffe-1111111111",
"mediaType": "application/vnd.adobe.ffmodel+dcx",
"publishedState": "published",
"baseModel": { "name": "clio_v2", "version": "2.0.0" },
"displayName": "Warm Custom Model"
}
],
"total_count": 1
}
One official inconsistency: the tutorial’s prose names the ID assetId, while its own sample reads repo:assetId.
Paging, sorting and publishedState
sortBy defaults to "modifiedDate" and reverses with a leading -; limit is capped at 1 to 50; start is "0" and is Required if a limit is specified. publishedState defaults to "published" and is where to look when a list stays empty — its enum includes all, training and failed. Documented status codes run from 200 to 503, and a 406 points at your Accept header, not the token.
Generate with the custom model
Generation reuses the ordinary async image endpoint, and the difference is one body field plus one header. The reference states it as a rule: When a custom model is used, a customModelId must also be passed in the request body.
Which x-model-version to send
x-model-version defaults to image3 and enumerates "image3" "image3_custom" "image4_standard" "image4_ultra" "image4_custom", so a custom model means image3_custom or image4_custom. Pairing the plain default with a customModelId is a contradictory request, not a silent fallback.
# Send the assetId as customModelId, and select the custom model in the header.
curl -s -X POST 'https://firefly-api.adobe.io/v3/images/generate-async' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'x-accept-mimetype: image/jpeg' \
-H 'x-model-version: image3_custom' \
-H "x-api-key: ${CUSTOM_MODELS_CLIENT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-d "{
\"numVariations\": 2,
\"size\": { \"width\": 2048, \"height\": 2048 },
\"prompt\": \"An almond seed in a warm setting\",
\"contentClass\": \"photo\",
\"style\": { \"presets\": [\"painting\"], \"strength\": 50 },
\"promptBiasingLocaleCode\": \"en-US\",
\"customModelId\": \"${CUSTOM_MODEL_ID}\"
}" | jq .

Supporting fields are numVariations, size, contentClass and style. Negative prompting is unavailable on the older custom models: Negative prompting is not supported for Firefly Custom Models on Image Model 3 or Firefly Custom Models on Image Model 4.
Polling, and the Image5 migration trap
Submission returns the standard async envelope: jobId, statusUrl, cancelUrl. Polling does not change, and the async job walkthrough covers the state machine and retry-after loop.
import os
import time
import requests
CLIENT_ID = os.environ['CUSTOM_MODELS_CLIENT_ID']
TOKEN = os.environ['CUSTOM_MODELS_ACCESS_TOKEN']
HEADERS = {'Accept': 'application/json', 'x-api-key': CLIENT_ID,
'Authorization': f'Bearer {TOKEN}'}
def pick_model_id():
"""List models and return the first usable asset ID."""
response = requests.get('https://firefly-api.adobe.io/v3/custom-models',
headers={**HEADERS, 'x-user-token': f'Bearer {TOKEN}'},
params={'limit': '10'}, timeout=30)
response.raise_for_status()
models = response.json().get('custom_models', [])
if not models:
raise RuntimeError('no models returned; check that the model is shared')
first = models[0]
return first.get('assetId') or first['repo:assetId']
def generate_and_wait(model_id, prompt):
body = {'prompt': prompt, 'customModelId': model_id, 'numVariations': 1}
submit = requests.post(
'https://firefly-api.adobe.io/v3/images/generate-async',
headers={**HEADERS, 'Content-Type': 'application/json',
'x-model-version': 'image3_custom'},
json=body, timeout=60)
submit.raise_for_status()
status_url = submit.json()['statusUrl']
status = 'running'
payload = {}
while status not in ('succeeded', 'failed'):
time.sleep(5)
polled = requests.get(status_url, headers=HEADERS, timeout=30)
polled.raise_for_status()
payload = polled.json()
status = payload.get('status', 'unknown')
if status == 'failed':
raise RuntimeError(payload)
return [item['image']['url'] for item in payload['result']['outputs']]
print(generate_and_wait(pick_model_id(), 'An almond seed in a warm setting'))
When the list is empty, widen the filter before debugging the token.
# publishedState defaults to published. Ask for everything to find training or failed models.
curl -s -X GET 'https://firefly-api.adobe.io/v3/custom-models?publishedState=all&limit=50&sortBy=-modifiedDate' \
-H 'Accept: application/json' \
-H "x-api-key: ${CUSTOM_MODELS_CLIENT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "x-user-token: Bearer ${TOKEN}" | jq '.total_count'
Image5 renames this field with consequences. Adobe pairs modelId with customModelId and annotates it New semantics; not a direct mapping. The same table retires aspectRatio, modelVersion, modelSpecificPayload and output.cai, and adds negativePrompt, promptBiasingLocaleCode and visualIntensity. Payloads that do not conform to the new schema will be rejected by the API. The Image 5 walkthrough covers the model.
Generation sits in the same rate-card row as everything else, and our API pricing breakdown covers the conversion. The training side is not published by the vendor: reference-image counts, training duration and metering are absent.
Frequently asked questions
What endpoint lists custom models? GET https://firefly-api.adobe.io/v3/custom-models, with x-api-key, Authorization and the easily missed x-user-token.
Why is my list empty? The model was never shared with your Technical Account Email, or publishedState is filtering it out at its published default.
Do I send assetId or repo:assetId? Adobe’s tutorial names the first in prose and reads the second in its sample code.
Which x-model-version do I need? image3_custom or image4_custom. No training endpoint is documented, and negative prompting is unsupported on Image Model 3 and 4 custom models.