Part 2 of 8: Three things that break Firefly calls before a pixel renders

By Pushparajan Ramar · September 23, 2026

Part 2 of 8: Three things that break Firefly calls before a pixel renders

Series: Firefly Services for developers. Checked against Adobe's documentation on September 23, 2026.

TL;DR

  • Access tokens last 24 hours. Cache and refresh them; don't mint one per call.
  • Files travel as signed URLs or upload IDs, never inside the generation request.
  • Different failures need different responses. Don't blanket-retry.

1. Tokens

Firefly Services uses OAuth Server-to-Server credentials created in the Adobe Developer Console. Your backend exchanges a Client ID and Client Secret with Adobe's Identity Management System (IMS) for an access token.

curl -X POST 'https://ims-na1.adobelogin.com/ims/token/v3' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&scope=openid,AdobeID,session,additional_info,read_organizations,firefly_api,ff_apis'

The response looks like this:

{"access_token":"...","token_type":"bearer","expires_in":86399}

Adobe's docs say each token is valid for 24 hours and recommend storing it and refreshing before it expires. Scopes differ between APIs, so copy the scope string from the getting-started page of the API you're calling.

Every call then carries two headers:

Authorization: Bearer <access_token>
x-api-key: <client_id>
sequenceDiagram
    participant App as Your app
    participant Cache as Token cache
    participant IMS as Adobe IMS
    participant FF as Firefly API
    App->>Cache: get token
    Cache-->>App: missing or expired
    App->>IMS: client_id + client_secret
    IMS-->>App: access_token, expires_in 86399
    App->>FF: Bearer token + x-api-key

A minimal token manager (illustrative code, not from Adobe's docs):

import time, requests

class TokenManager:
    def __init__(self, client_id, client_secret, scope, early=3600):
        self.cid, self.secret, self.scope, self.early = client_id, client_secret, scope, early
        self.token, self.expires_at = None, 0

    def get(self):
        if not self.token or time.time() >= self.expires_at - self.early:
            r = requests.post(
                "https://ims-na1.adobelogin.com/ims/token/v3",
                data={"grant_type": "client_credentials", "client_id": self.cid,
                      "client_secret": self.secret, "scope": self.scope},
                timeout=30)
            r.raise_for_status()
            j = r.json()
            self.token = j["access_token"]
            self.expires_at = time.time() + j["expires_in"]
        return self.token

Renewing an hour early is my habit. It is not an Adobe requirement. If you run several workers, share the token through a cache or secrets store rather than fetching one per worker.

2. Files travel by URL

Firefly reads inputs from pre-signed URLs, or from an ID returned by its upload endpoint.

Pre-signed URL domains. For Firefly image workflows, Adobe's docs list three:

Provider Domain
Amazon S3 *.amazonaws.com
Microsoft Azure *.windows.net
Dropbox *.dropboxusercontent.com

Other Firefly Services APIs list different domains, so check each API's storage page:

  • The Audio/Video API also lists frame.io, cloudfront.net, drive.google.com and adobe.io.
  • The Photoshop and Lightroom docs describe Google Drive signed URLs as well.
  • For the InDesign API, an Adobe Community reply says only AWS S3, Dropbox and Azure are supported and that domain allow-listing is enforced. That is a community answer, not official documentation.
flowchart LR
    S[Your storage] -- signed URL --> F[Firefly]
    F -. result URLs .-> S

The upload endpoint. If you'd rather not host the file, upload it once:

curl --location 'https://firefly-api.adobe.io/v2/storage/image' \
  --header 'x-api-key: <CLIENT_ID>' \
  --header 'Authorization: Bearer <TOKEN>' \
  --header 'Content-Type: image/png' \
  --data-binary '@/path/to/source.png'
# -> {"images":[{"id":"e42cf05b-..."}]}
  • The returned ID is valid for 7 days.
  • The maximum upload size is 15 MB.
  • Send raw binary, not base64, not multipart form data.
  • Image inputs are JPEG, PNG and WebP. The upload API's reference also mentions TIFF and JXL.

Results come back as URLs. In one older Adobe sample the result URLs were S3 links with a one-hour expiry. Don't depend on that number; copy finished assets into your own storage promptly.

3. Handle failures differently

Situation What I'd do Basis
Token expired Refresh once, retry once Design guidance
Rate limited (429) Back off with jitter 429 is listed in Adobe's API reference
Not entitled (403) Stop and fix product profile or project setup 403 is listed in Adobe's API reference

The API reference also lists 408, 415, 422 and 500 responses. Log each class separately.

Checklist

  • Client secret lives in a secrets manager
  • One shared token cache, refresh before expiry
  • Inputs use approved domains or upload IDs
  • Finished assets copied into your storage
  • Retry logic distinguishes auth, rate limit and entitlement errors

Sources