Exporting Released Images
This guide shows how to pick up finished images from Brandmachine automatically, for example to move them into a DAM, a PIM or a shop. The idea is simple: your team reviews and releases images in the Brandmachine app, and your integration collects every released image on a schedule.
You need an API token (see Authentication). Everything below is read-only, so the integration never starts paid work or changes anything in the app.
How it works
- Your team releases images in the app. Releasing is the approval step: only released images are exported.
- Your integration calls the
releasedImagesquery on a schedule, for example every 15 minutes. Each call returns the images released or changed since the previous call. - For each image, your integration downloads the file from
exportImageUrland stores it under the releaseid.
The sync query
query Sync($after: String) {
releasedImages(after: $after, first: 100) {
cursor
hasMore
releases {
id
updatedAt
exportImageUrl
productStudioOutput {
id
exportFileName
productStudio { id title }
}
campaignImageResult { id }
groupShotResult { id }
}
}
}
releaseslists the released images, oldest change first. Each one belongs to exactly one ofproductStudioOutput,campaignImageResultorgroupShotResult; the other two arenull.cursormarks how far you have read. Store it, and pass it asafteron your next call.hasMoreistruewhen more releases are waiting. Keep calling with the newcursoruntil it isfalse.firstis the page size, from 1 to 500. It defaults to 100.
On the very first sync, leave out after. You then receive every image your team has released so far.
How the cursor works
The cursor is a bookmark into the list of releases, ordered by when each release last changed. Brandmachine stores nothing about your integration: the cursor itself says where you are. If you lose it, call without after and you receive everything again.
Pass the cursor back exactly as you received it. A value that was not returned by releasedImages is rejected with Invalid cursor.
Edited and re-released images
A released image can still change. If someone edits it in the photo editor, or releases a newer version of the same output, the release appears in your next sync again, with the same id and a new exportImageUrl. Use the release id as your key and overwrite the file you stored before.
Changes appear in the feed about one minute after they happen. This makes sure a release is fully saved before it is handed out, so a sync never skips one.
Deleting a product studio or campaign in the app removes its releases. The feed does not report deletions.
Downloading the file
exportImageUrl is a plain HTTPS link. Download it with a GET request, no token needed. The file is:
- a JPEG with an embedded sRGB colour profile
- labelled as AI-generated in its metadata, using the IPTC Digital Source Type
trainedAlgorithmicMedia(see AI Metadata on Downloads)
It is the same file the JPEG (sRGB) download in the app gives you. Content Credentials (C2PA) are in progress. When they are ready, they will be added to the file behind the same URL, so your integration does not need to change.
The downloaded file is named after its storage key. For product studio images, exportFileName gives the name your team's filename convention produces (without extension; add .jpg). It is null when your team has not set up a convention, or when a studio mixes several products and there is no single correct name.
Don't use downloadableImageUrl. It points to the stored original, which has no colour profile and no AI label.
Example: a complete sync
const API = 'https://production.api.brandmachine.shop/graphql';
const TOKEN = process.env.BRANDMACHINE_API_TOKEN;
const QUERY = `
query Sync($after: String) {
releasedImages(after: $after, first: 100) {
cursor
hasMore
releases {
id
exportImageUrl
productStudioOutput { exportFileName }
}
}
}
`;
// `storedCursor` is what you saved after the previous run (null on the first).
async function sync(storedCursor) {
let cursor = storedCursor;
while (true) {
const response = await fetch(API, {
method: 'POST',
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: QUERY, variables: { after: cursor } }),
});
const result = await response.json();
if (result.errors) throw new Error(result.errors[0].message);
const page = result.data.releasedImages;
for (const release of page.releases) {
const file = await fetch(release.exportImageUrl);
const name = release.productStudioOutput?.exportFileName ?? release.id;
await saveFile(release.id, `${name}.jpg`, await file.arrayBuffer()); // your storage
}
cursor = page.cursor;
if (!page.hasMore) return cursor; // save this for the next run
}
}
Save the returned cursor only after the files are stored. If a run fails half way, the next run starts again from the last saved cursor and fetches those images again, which is safe because you overwrite by release id.
What an API token cannot do
An integration only reads and picks up work. Starting generations, releasing images, deleting work and managing tokens stay in the app, and the API refuses them for API tokens. Authentication has the full list.