Firefly Generate Image — V3 Async
The production pattern for text-to-image generation with Adobe Firefly's V3 asynchronous API (/v3/images/generate-async). V3 async is the right shape for every workload above one-off interactive use. The synchronous V3 endpoint (/v3/images/generate) is still available but its per-request blocking makes it unsuitable for production volume.
When to Use This Skill
Use this skill when:
- Generating images from text prompts at any production volume
- Migrating from the synchronous endpoint (
/v3/images/generate) to V3 async (/v3/images/generate-async) - Building a campaign pipeline, banner-at-scale system, or batch generator
- Adding style or structure references to a generate call
- Designing a webhook-based generation pipeline
Do NOT use this skill when:
- The user wants a variation of an existing image — use
firefly-generate-similar - The user wants to extend an image canvas — use
firefly-expand-fill - The user is generating video — use
firefly-video-model
Sync vs Async — When to Use Which
| Property | V3 sync (/v3/images/generate) |
V3 async (/v3/images/generate-async) |
|---|---|---|
| Latency to first byte | 10-30s (blocking) | ~200ms (returns jobId) |
| Time to result | Same | Same |
| Connection lifetime | Whole job | Just submission |
| Resilient to caller restarts | No — results lost on disconnect | Yes — pick up by jobId |
| Webhook callbacks | No | Yes (preferred) |
| Recommended for production | No | Yes |
| Recommended for one-shot CLI | Acceptable | Acceptable |
Default to V3 async for everything. The only acceptable reason to use sync is a one-shot script where the user is watching the terminal. The sync endpoint returns 200 with the finished {size, outputs[]} body directly — no jobId. The async variant (/v3/images/generate-async) is documented in Adobe's async API guide; as of this writing the bundled @adobe/firefly-apis SDK spec covers only the sync path, so use raw HTTP for async submission.
The Async Workflow
1. POST /v3/images/generate-async → { jobId, statusUrl, cancelUrl }
2. Either:
a. Poll statusUrl every 1-2s until status === "succeeded" | "failed"
b. OR provide a webhook callback URL — Firefly calls it on completion
3. On success: response includes outputs[].image.url (pre-signed)
4. Download from URL within ~1 hour or it expires
Step 1 — Submit the Generation Job
Minimum required request:
curl --silent -X POST 'https://firefly-api.adobe.io/v3/images/generate-async' \
-H "Authorization: Bearer $FIREFLY_SERVICES_ACCESS_TOKEN" \
-H "X-Api-Key: $FIREFLY_SERVICES_CLIENT_ID" \
-H 'Content-Type: application/json' \
-d '{
"prompt": "a single red apple on a white background",
"contentClass": "photo",
"numVariations": 1,
"size": {"width": 1024, "height": 1024}
}'
Response:
{
"jobId": "urn:ff:jobs:example:00000000-0000-4000-8000-000000000000",
"statusUrl": "https://firefly-api.adobe.io/v3/status/urn:ff:jobs:...",
"cancelUrl": "https://firefly-api.adobe.io/v3/cancel/urn:ff:jobs:..."
}
The submission returns in ~200ms regardless of job complexity. Persist the jobId immediately — that is the only thing that lets you recover the result if the worker crashes mid-poll.
Step 2 — Request Shape
Full request shape with all common fields:
{
"prompt": "string (required, 1-1024 chars)",
"negativePrompt": "string (optional, things to avoid)",
"contentClass": "photo | art",
"numVariations": 1,
"size": {"width": 1024, "height": 1024},
"seeds": [12345],
"visualIntensity": 6,
"style": {
"presets": ["bold_colors"],
"imageReference": {"source": {"uploadId": "abc-123"}},
"strength": 75
},
"structure": {
"imageReference": {"source": {"url": "https://..."}},
"strength": 50
},
"customModelId": "optional-uuid-for-custom-model"
}
Supported sizes (V3)
| Dimensions | Aspect |
|---|---|
| 1024×1024 | Square (1:1) |
| 2048×2048 | Square (1:1) |
| 2304×1792 | Landscape (4:3) |
| 1344×768 | Landscape (7:4) |
| 1152×896 | Landscape (9:7) |
| 2688×1536 | Widescreen (16:9) |
| 1792×2304 | Portrait (3:4) |
| 896×1152 | Portrait (7:9) |
Other dimensions are rejected with a 400 bad_request whose message enumerates the currently valid sizes — live-verified 2026-08-10: Size must be one of {(2688, 1536), (1344, 756), (896, 1152), (1344, 768), (2688, 1512), (2304, 1792), (1152, 896), (2048, 2048), (1792, 2304), (1024, 1024)}. Note the live error enumerates 10 tuples — a superset of the documented sizes in the table above (it additionally accepts 1344×756 and 2688×1512); the table shows Adobe's documented set. Prefer the documented sizes, or generate at the nearest match and crop in post.
Content class
| Value | Use for |
|---|---|
photo |
Photorealistic output — products, scenes, people |
art |
Stylized output — illustrations, paintings, designs |
If omitted, Firefly auto-detects the content class from the prompt; setting it explicitly produces sharper results in the chosen direction.
Variations and seeds
numVariations: 1-4. Production typically uses 2-4 to give downstream selection logic options.seeds: array of integers. The same seed with the same prompt and model biases generation toward a consistent composition, but does not guarantee byte-identical output (live-verified: identical seeded requests produced different bytes). Use seeds for near-reproducibility and A/B comparisons, and archive the actual output artifact when you need an audit trail. If provided alongsidenumVariations, the seed count must equalnumVariations.
Step 3 — Poll for Completion
JOB_ID=$(echo "$SUBMIT_RESPONSE" | jq -r .jobId)
STATUS_URL=$(echo "$SUBMIT_RESPONSE" | jq -r .statusUrl)
while true; do
RESPONSE=$(curl --silent "$STATUS_URL" \
-H "Authorization: Bearer $FIREFLY_SERVICES_ACCESS_TOKEN" \
-H "X-Api-Key: $FIREFLY_SERVICES_CLIENT_ID")
STATUS=$(echo "$RESPONSE" | jq -r .status)
case "$STATUS" in
succeeded|failed) echo "$RESPONSE"; break ;;
*) sleep 1 ;;
esac
done
Node implementation:
async function pollJob(statusUrl, accessToken, clientId, { intervalMs = 1000, maxMs = 300_000 } = {}) {
const start = Date.now();
while (Date.now() - start < maxMs) {
const res = await fetch(statusUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
'X-Api-Key': clientId,
},
});
if (!res.ok) throw new Error(`Status check failed: ${res.status}`);
const data = await res.json();
if (data.status === 'succeeded' || data.status === 'failed') return data;
await new Promise(r => setTimeout(r, intervalMs));
}
throw new Error('Job polling timed out');
}
Polling cadence
| Cadence | When to use |
|---|---|
| 1s | Interactive workloads, user is waiting |
| 2s | Background batch jobs, no user attention |
| 5s | Very large batches where polling rate matters more than latency |
Polling every 250ms or faster is wasteful — typical Firefly V3 jobs complete in 3-10 seconds. Sub-second polling will not make them complete faster.
Polling and generation limits
In live testing, status polls did not trigger generate-endpoint 429s. Adobe does not publicly document how status calls are billed or rate-limited relative to generation (treatment may be org/contract-dependent — verify for your org), so keep polling to the 1-2s cadence above rather than assuming polls are free. The production concern with fast polling is wasted compute either way.
Step 4 — Webhook Callbacks (Preferred at Scale)
Illustrative — verify against current Adobe docs. The
notify/webhookUrl/X-Adobe-SignatureHMAC fields below are not part of the published Firefly generate-image request schema or the official SDK at time of writing. Treat this section as a design pattern, not a documented contract: confirm field names, headers, and signature scheme against the current Adobe Firefly Services documentation before relying on it. If your account does not expose webhook callbacks, use the polling pattern in Step 3.
For production batch workloads, webhooks beat polling. The pattern is: pass a callback URL on submission and Firefly calls it when the job completes.
{
"prompt": "...",
"notify": {
"webhookUrl": "https://api.example.com/firefly/callback",
"secretKey": "shared-secret-for-hmac-validation"
}
}
In this design shape, the service POSTs the job result body to the callback URL. Validate an HMAC signature over the body before trusting the payload — the header name and signature scheme must be confirmed against current Adobe documentation.
A webhook receiver in this pattern should provide:
| Component | Detail |
|---|---|
| Public URL | Reachable from the calling service |
| HMAC validation | e.g. SHA-256 over the body with the shared secret (confirm scheme with Adobe docs) |
| Idempotency | Callbacks may be delivered more than once; key jobs by jobId |
| Prompt 2xx acknowledgment | Respond quickly; treat delivery timing and retry semantics as unspecified until confirmed |
Regardless of webhook behavior, the result remains retrievable via the original statusUrl — always implement polling fallback for robustness.
Step 5 — Read the Result
A succeeded job's response:
{
"status": "succeeded",
"jobId": "urn:ff:jobs:...",
"result": {
"size": {"width": 1024, "height": 1024},
"outputs": [
{
"seed": 12345,
"image": {
"url": "https://pre-signed-cdn-url..."
}
}
]
}
}
Download immediately. The image.url is a pre-signed CDN URL that typically expires within 1 hour. For production:
const result = await pollJob(statusUrl, token, clientId);
for (const output of result.result.outputs) {
const imgRes = await fetch(output.image.url);
const buffer = await imgRes.arrayBuffer();
// Persist to your own bucket
await s3.putObject({
Bucket: 'my-outputs',
Key: `${jobId}/${output.seed}.png`,
Body: Buffer.from(buffer),
ContentType: 'image/png',
});
}
Never store the raw Firefly URL long-term. Always re-host in your own storage.
Style and Structure References
V3 image generation supports two reference mechanisms — style (via presets and/or an image reference) and structure (via an image reference):
| Reference | Effect |
|---|---|
style.imageReference |
Output matches the visual style of the reference |
style.presets |
Output matches a named style preset |
structure.imageReference |
Output matches the composition of the reference |
Combine for fine control:
{
"prompt": "a futuristic city at sunset",
"contentClass": "art",
"style": {
"presets": ["bold_colors"],
"imageReference": {"source": {"uploadId": "style-ref-id"}},
"strength": 75
},
"structure": {
"imageReference": {"source": {"uploadId": "structure-ref-id"}},
"strength": 50
}
}
strength: structure accepts 0-100; style accepts 1-100 (0 is excluded). Higher = stronger influence. Start at 50 and tune.
The reference image must be a valid storage reference — see firefly-services-storage-refs.
Custom Models
To generate with a custom-trained model, pass customModelId in the body and send the x-model-version: image3_custom header:
curl --silent -X POST 'https://firefly-api.adobe.io/v3/images/generate-async' \
-H "Authorization: Bearer $FIREFLY_SERVICES_ACCESS_TOKEN" \
-H "X-Api-Key: $FIREFLY_SERVICES_CLIENT_ID" \
-H 'x-model-version: image3_custom' \
-H 'Content-Type: application/json' \
-d '{
"prompt": "an icon of a key in our brand style",
"customModelId": "00000000-0000-0000-0000-000000000000",
"contentClass": "art",
"size": {"width": 1024, "height": 1024}
}'
The x-model-version: image3_custom header is required — without it, the request runs against the base Firefly model and customModelId is ignored, so you get base-model output with no error. Custom model IDs come from the custom-model training workflow — see firefly-custom-models.
Production Patterns
Pattern: Single-job CLI
For interactive one-shot use, submit + poll in a single function. Acceptable for <50 calls.
Pattern: Queue-fronted batch
For >50 calls, use the SQS / Lambda / Token-Bucket pattern from firefly-services-rate-limits. Each queue message is one generate call. Worker submits, polls (or relies on webhook), persists result.
Pattern: Multi-variation A/B funnel
For campaigns where you want choice:
- Submit with
numVariations: 4and exactly 4 seeds (seed count must equalnumVariations) - Persist all 4 outputs to your bucket
- Downstream selection logic (human or automated) picks 1
- Audit which combinations win for future prompt tuning
This is the standard pattern for high-volume template-driven campaign asset production — variations give downstream creative teams options without re-running the pipeline.
Validate
A correctly wired V3 async pipeline:
- Submits jobs and persists
jobIdbefore any subsequent work - Polls with 1-2s cadence (webhook callbacks, where available and confirmed against current docs, are an alternative — see Step 4)
- Honors
statusUrlfrom the submission response — does not hardcode URLs - Downloads result URLs within 1 hour and re-hosts in your own bucket
- Has retry-with-backoff on submission (covered by
firefly-services-rate-limits) - Logs
jobIdfor every submission for downstream audit
Troubleshooting & Edge Cases
numVariations> 4 rejected: Max is 4. Submit multiple jobs if you need more.- Size rejected as invalid: Use only the published sizes (see Supported sizes table above).
promptrejected as too long: Max 1024 chars. Strip or rephrase.- Webhook never fires: Adobe was unable to reach the URL. Test with a
curl -X POSTfrom outside your VPC. Fall back to polling. - Job stuck in
runningfor >5 minutes: Cancel viacancelUrland resubmit. Adobe-side jobs almost always complete in under 30s; 5+ minutes is a sign something is wrong. outputsarray is empty on success: Content safety filtered all variations. Rephrase the prompt — seefirefly-services-troubleshoot§6.- Different output between identical requests: Expected — generation is not byte-deterministic even with a fixed seed. Set
seeds: [<int>]to bias toward a consistent composition, and archive outputs you need to reproduce exactly.
Chaining with Other Skills
firefly-services-auth— Token freshness before submissionfirefly-services-storage-refs— Required for any reference-image-based generationfirefly-services-rate-limits— Production batch pipelinefirefly-services-troubleshoot— When generation fails