Generate and Edit a Variant (OAuth Web App)

This guide walks through a complete end-to-end workflow using OAuth Web App authentication, covering template selection, variation generation, and final editing in Adobe Express using the Embed SDK.

Overview

In this workflow, we present a Web App-based scenario where a user:

  1. Authenticates (OAuth) with their Adobe ID
  2. Chooses one of their previously tagged templates in Adobe Express
  3. Generates a variation by replacing tagged elements (text, images, videos)
  4. Refines the result by editing the generated document in Adobe Express

Prerequisites

The cURL and Python tabs in the steps below assume you already have an ACCESS_TOKEN from the OAuth flow in the first step. During development, log it from your /callback handler the first time it succeeds and reuse it for the day. Tokens are valid roughly 24 hours; once a token expires the user must sign in again. If your credential has the offline_access scope available (check the Available Scopes section of your OAuth Web App credential in the Developer Console), you can also receive a refresh token.

1. Authenticate the user

This is the only step that isn't a single API call. The user's browser bounces to Adobe's identity service, comes back to your callback with a short-lived code, and your backend exchanges that code for an access token.

Sign in with Adobe

1.1 Send the user to Adobe's authorize endpoint

When the user signs in, redirect them to:

https://ims-na1.adobelogin.com/ims/authorize/v2
  ?client_id=<CLIENT_ID>
  &redirect_uri=<REDIRECT_URI>
  &response_type=code
  &scope=openid%20AdobeID%20ee.express_api
  &state=<RANDOM_STATE>

state is an opaque random string you generate and store in the user's session; verify it matches when Adobe calls you back to prevent Cross-Site Request Forgery (CSRF). For a working /login + /callback pair (state generation, verification, and token exchange), see the companion sample app.

When the user signs in, they will see the following consent screen:

OAuth consent screen

Adobe then redirects the browser to your redirect_uri with ?code=...&state=....

1.2 Exchange the code for tokens

Your backend POSTs the code to Adobe's token endpoint. This call must be server-side because it includes your client_secret.

data-slots=heading, code
data-repeat=3
data-languages=bash, javascript, python

cURL

curl -s -X POST 'https://ims-na1.adobelogin.com/ims/token/v3' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "code=$CODE" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "client_secret=$CLIENT_SECRET"

JavaScript (Node 18+)

const body = new URLSearchParams({
  grant_type: 'authorization_code',
  code,
  client_id: process.env.CLIENT_ID,
  client_secret: process.env.CLIENT_SECRET,
});

const resp = await fetch('https://ims-na1.adobelogin.com/ims/token/v3', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body,
});
const tokens = await resp.json();
// tokens: { access_token, refresh_token, expires_in, token_type, ... }

Python

import os, requests

resp = requests.post(
    "https://ims-na1.adobelogin.com/ims/token/v3",
    data={
        "grant_type": "authorization_code",
        "code": code,
        "client_id": os.environ["CLIENT_ID"],
        "client_secret": os.environ["CLIENT_SECRET"],
    },
)
tokens = resp.json()

Store access_token, refresh_token, and the absolute expiry time (Date.now() + (expires_in - 60) * 1000) in the user's session.

2. List the user's tagged templates

Once you have an access token, list the user's tagged templates so they can pick one. Use the Authorization: Bearer <ACCESS_TOKEN> header from the first step and your X-API-KEY (the same client_id from your credential).

data-variant=warning
data-slots=text
The JavaScript snippets in steps 2–5 show the raw API call for clarity. In a real web app you should never call https://express-api.adobe.io directly from the browser; keep the access token and client_secret on the server and expose your own thin proxy routes (e.g. /api/templates, /api/generate) that forward to Adobe. The companion sample app shows this pattern end to end.
data-slots=heading, code
data-repeat=3
data-languages=bash, javascript, python

cURL

curl -s 'https://express-api.adobe.io/beta/tagged-documents?start=0&limit=25&sortBy=-modifiedDate' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-API-KEY: $CLIENT_ID"

JavaScript (fetch)

const resp = await fetch(
  'https://express-api.adobe.io/beta/tagged-documents?start=0&limit=25&sortBy=-modifiedDate',
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'X-API-KEY': clientId,
    },
  }
);
const { documents, paging } = await resp.json();

Python

resp = requests.get(
    "https://express-api.adobe.io/beta/tagged-documents",
    params={"start": 0, "limit": 25, "sortBy": "-modifiedDate"},
    headers={
        "Authorization": f"Bearer {access_token}",
        "X-API-KEY": client_id,
    },
)
data = resp.json()

Response shape:

{
  "documents": [
    {
      "id": "urn:aaid:sc:EU:e6723...",
      "name": "Express API Sample Template.express",
      "thumbnailUrl": "https://aep-cs-blobstore-prod-irl1-data..."
    }
  ],
  "paging": {
		"totalRecords": 1,
		"nextUrl": ""
	}
}

Render each document's thumbnailUrl and name as a card in your UI and let the user click one to select it. Keep the id — you need it for steps 3 and 4.

List the user's tagged templates

3. Inspect the template's tagged elements

Before the user can fill in tag values, your UI needs to know what tags the template actually has. Call GET /beta/tagged-documents/{id} for the selected document.

data-slots=heading, code
data-repeat=3
data-languages=bash, javascript, python

cURL

curl -s "https://express-api.adobe.io/beta/tagged-documents/$DOCUMENT_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-API-KEY: $CLIENT_ID"

JavaScript (fetch)

const resp = await fetch(
  `https://express-api.adobe.io/beta/tagged-documents/${encodeURIComponent(documentId)}`,
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'X-API-KEY': clientId,
    },
  }
);
const detail = await resp.json();

Python

import urllib.parse
url = f"https://express-api.adobe.io/beta/tagged-documents/{urllib.parse.quote(document_id, safe='')}"
detail = requests.get(url, headers={
    "Authorization": f"Bearer {access_token}",
    "X-API-KEY": client_id,
}).json()

The response lists each page and its taggedElements. The type of each tag tells you what input to render: text accepts a plain string; image and video accept a pre-signed URL on an allowed domain (AWS, Dropbox, Azure).

This endpoint is paginated by page: append ?start=<n> to fetch tagged elements starting at page n if the template has more pages than the default page size returns.

{
  "id": "urn:aaid:sc:EU:e6723...",
  "name": "Express API Sample Template.express",
  "documentPages": [
    {
      "pageNumber": 1,
      "pageTitle": "",
      "size": { "width": 662, "height": 289 },
      "taggedElements": [
        {
          "name": "title", "type": "text",
          "position": { "x": 209, "y": 185 },
          "size": { "width": 662, "height": 289 }
        },
        {
          "name": "product-image", "type": "image",
          "position": { "x":  346, "y":  -131 },
          "size": { "width": 493, "height": 963 }
        }
      ],
      "thumbnailUrl": "https://aep-cs-blobstore-prod-irl1-data..."
    }
  ]
}

Inspect the template's tagged elements

4. Generate the variation

Submit the user's values as tagMappings. The keys are the tag names from step 3; the values are strings (text) or pre-signed URLs (images, videos).

data-slots=heading, code
data-repeat=3
data-languages=bash, javascript, python

cURL

curl -s -X POST 'https://express-api.adobe.io/beta/generate-variation' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-API-KEY: $CLIENT_ID" \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "'"$DOCUMENT_ID"'",
    "variationDetails": {
      "preferredDocumentName": "Apple green",
      "tagMappings": {
        "title": "APPLE GREEN",
        "product-image": "https://uc4fcd1dc97e1127dcb839ff192c.dl..."
      }
    }
  }'

JavaScript (fetch)

const resp = await fetch('https://express-api.adobe.io/beta/generate-variation', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'X-API-KEY': clientId,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    id: documentId,
    variationDetails: {
      preferredDocumentName: 'Apple green',
      tagMappings: {
        title: 'APPLE GREEN',
        'product-image': 'https://uc4fcd1dc97e1127dcb839ff192c.dl...',
      },
    },
  }),
});
const { jobId, statusUrl } = await resp.json();

Python

resp = requests.post(
    "https://express-api.adobe.io/beta/generate-variation",
    headers={
        "Authorization": f"Bearer {access_token}",
        "X-API-KEY": client_id,
        "Content-Type": "application/json",
    },
    json={
        "id": document_id,
        "variationDetails": {
            "preferredDocumentName": "Apple green",
            "tagMappings": {
                "title": "APPLE GREEN",
                "product-image": "https://uc4fcd1dc97e1127dcb839ff192c.dl...",
            },
        },
    },
)
job = resp.json()

Response (HTTP 202):

{
  "jobId": "af121560-218e-4dd9-918d-add12b3b6d98",
  "statusUrl": "https://express-api.adobe.io/status/af121560-218e-4dd9-918d-add12b3b6d98"
}

Hold on to jobId — that's the handle you'll poll in the next step.

Filled out tag mappings

5. Poll the job

GET /status/{jobId} returns running until the variation is ready, then succeeded (or failed/partially_succeeded). Poll every few seconds.

data-slots=heading, code
data-repeat=3
data-languages=bash, javascript, python

cURL

curl -s "https://express-api.adobe.io/status/$JOB_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-API-KEY: $CLIENT_ID"

JavaScript (fetch)

async function pollJob(jobId, accessToken, clientId) {
  while (true) {
    const r = await fetch(`https://express-api.adobe.io/status/${encodeURIComponent(jobId)}`, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'X-API-KEY': clientId,
      },
    }).then((r) => r.json());
    if (['succeeded', 'failed', 'partially_succeeded'].includes(r.status)) return r;
    await new Promise((res) => setTimeout(res, 3000));
  }
}

Python

import time

def poll_job(job_id, access_token, client_id):
    while True:
        r = requests.get(
            f"https://express-api.adobe.io/status/{job_id}",
            headers={
                "Authorization": f"Bearer {access_token}",
                "X-API-KEY": client_id,
            },
        ).json()
        if r["status"] in ("succeeded", "failed", "partially_succeeded"):
            return r
        time.sleep(3)

When the job succeeds, you get the new document back:

{
  "jobId": "af121560-218e-4dd9-918d-add12b3b6d98",
  "status": "succeeded",
  "document": {
    "id": "urn:aaid:sc:EU:3da...",
    "name": "Apple green",
    "thumbnailUrl": "https://...signed-url..."
  }
}

Variation created

The variation is now stored in the user's account, inside an Express API Documents folder in My Stuff > Files.

Variation in My Stuff

6. Open the variation with the Adobe Express Embed SDK

After the backend generates the variation, users can immediately customize or export the final result using the Adobe Express editor without leaving your experience thanks to the Embed SDK. This is especially useful in self-service scenarios where business users start from company-approved templates but still need lightweight creative control over the generated variation.

The Embed SDK exposes an Editor Workflow with an edit() method that takes the same documentId (the variation URN returned in step 5) and launches the Full Editor experience in your page.

6.1 Load and initialize the SDK

Load the SDK script, then call CCEverywhere.initialize once with the Embed SDK's Client ID and an appName that matches the Public App Name you set in the Developer Console.

data-variant=info
data-slots=heading, text

Embed SDK and Express API Client IDs

The Embed SDK and Express API rely on different Client IDs. Both are generated when you create credentials in the Developer Console, but the Projects that contain them are independent.
  await import('https://cc-embed.adobe.com/sdk/v4/CCEverywhere.js');

  const hostInfo = {
    clientId: '<CLIENT_ID>',
    appName: 'Embed SDK & Express API integration',
  };

  // lets the user start interacting with the embedded editor; sign-in is only prompted when they save or export.
  const configParams = { loginMode: 'delayed' };

  const { editor } = await window.CCEverywhere.initialize(hostInfo, configParams);

6.2 Open the variation for editing

Pass the variation URN from step 5 as documentId and call editor.edit():

const docConfig = { documentId: '<DOCUMENT_URN>' };
const appConfig = {};       // optional editor configuration
const exportConfig = [];    // optional export targets
const containerConfig = {}; // optional container configuration
editor.edit(docConfig, appConfig, exportConfig);

Variation in Embed SDK

For the full surface (appConfig, exportConfig, containerConfig), and other entry points available, please refer to the Adobe Express Embed SDK documentation.

data-variant=info
data-slots=heading, text

Open in Adobe Express

Alternatively, you can open the variation in Adobe Express (in a new Browser tab) by deep-linking to it via its URN: https://express.adobe.com/id/<DOCUMENT_URN>.

Next steps