---
title: "How to Keep AI-Generated Marketing Assets On-Brand"
description: "A template your sales team can't break, and a validator that refuses to export anything off-brand."
url: "https://img.ly/blog/keep-ai-generated-marketing-assets-on-brand/"
type: "blog"
date: "2026-11-03"
author: "Graeme"
tags: ["AI","CE.SDK","How-To","Creative Workflows"]
---

> This is the markdown version of [How to Keep AI-Generated Marketing Assets On-Brand](https://img.ly/blog/keep-ai-generated-marketing-assets-on-brand/). For all pages in one file, see [llms-full.txt](https://img.ly/llms-full.txt). For an index of all available pages, see [llms.txt](https://img.ly/llms.txt).

---

## What You Will Build: Brand Guardrails for AI-Generated Assets

By the end of this article you'll have a web app where a sales rep, franchisee, or customer can create a marketing asset from a locked template. AI generates the image, the user writes the headline, and nothing they do is allowed to deviate from the brand's style. The logo and legal line can't be touched, colors and fonts come from a fixed list, and the AI output has exactly one place to land. After that, a validator checks the design against the brand rules before any file is actually generated.

The complete code is in the [brand guardrails example repository on GitHub](https://github.com/imgly/blog-brand-guardrails-ai).

This guide will be handy for anyone that has sales teams, franchisees, dealers or customers that make their own marketing assets. Afterwards, you can say goodbye to off-brand output from AI tools that miss the mark and create substandard assets that can't be used. Brand managers and marketing operations teams will love it.

<div class="image-column">

![The validator refusing an off-brand design: a failing report, a locked export button, and the editor's own export refused](https://img.ly/_astro/validator-refusing-off-brand-design.7guX6xOw.webp)

</div>

## Why Brand Guidelines Do Not Stop Off-Brand AI Assets

People are bound to try and make marketing assets with AI at some point, whether their company gives them a tool or not. We're all familiar with scenarios where a franchisee needs a flyer by Friday, types a prompt, and gets something that looks great, so they go ahead and print it. The problem is that generative AI hardly ever matches the design constraints of a brand.

Think about nuances when you critique a branding attempt. The blue is close, but it's not the brand's blue, the font is whatever the tool picked, and the logo (if not deformed or just plain wrong) is squeezed into a corner. Headlines bring their own oddities like running to the edge of the page or spawning in random areas of your image.

The usual answer is a brand guidelines PDF and some training. Guidelines can describe the rules, but someone has to read them (the horror), remember them, and check every asset manually. Asking an AI system to follow the rules helps, but AI has a tendency to fill in the gaps if your instructions are not explicit.

A better approach is to use something that acts as a mechanistic system. The rules live in deterministic code and the editor only does what that code allows. AI output only has one area that it is allowed to land, and a design that still breaks a rule can't be exported. The ideal scenario is allowing a non-designer to make an asset, and only allowing on-brand files to leave the app.

## Brand Control Approaches Compared: Guidelines, Shared Libraries, Locked Templates With a Validator

| Property                           | Brand guidelines document                                    | Shared design library with approval step                                                | Locked template plus validator                                                        |
| ---------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Stops off-palette colors           | No. It lists the palette, and a reviewer spots misses by eye | Partly. Library assets use the palette, but new elements can use any color until review | Yes. The editor offers only palette colors, and the validator refuses any other color |
| Stops wrong fonts                  | No. It relies on the person reading it                       | Partly. Shared styles help, but any font can still be picked                            | Yes. The font list shows only approved faces, and the validator refuses others        |
| Enforces logo size and clear space | No. A reviewer measures by eye                               | Partly. The logo file is approved; how it's placed is checked at review                 | Yes. The logo is locked, and the validator checks its width and clear space           |
| Keeps text in the safe area        | No                                                           | At review, by eye                                                                       | Yes. Measured on every export, to the pixel                                           |
| Limits where AI output can land    | No                                                           | No. A generated image can go anywhere in the design                                     | Yes. Generation writes only into the image placeholder                                |
| Blocks prohibited content          | It lists what isn't allowed                                  | At review, by a person                                                                  | Checked at export through a moderation service (banned words tested in the demo)      |
| Works for non-designers            | Poorly. Applying the rules takes design knowledge            | Partly. The library helps, and the approver catches the rest                            | Yes. The rules are applied for them, and each failure says what to fix                |
| Needs a human approver             | Yes, for every asset                                         | Yes, the approval step                                                                  | No, for the rules the validator checks. Optional for judgment calls (step 6)          |
| Scales to hundreds of users        | No. Review time grows with every user                        | Only as far as the approvers' time goes                                                 | Yes. Checks run in code, in about a second per design in the test run                 |
| What you have to build             | A document                                                   | A shared library and a review workflow                                                  | A template, a brand kit config, a validator and an export gate                        |

The first two approaches need someone to check everything by eye, which means that output only scales with the number of reviewers you have, not the number of users generating assets. Using the locked template moves the rules into the editor and the validator so that human reviewers have less policing to do.

## Step 1: Define the Brand Kit (Palette, Fonts, Logo Rules)

"Brand kit" in this article just means your own config and files, not a CE.SDK feature name. In the example app, it's a TypeScript module with the palette, the approved typefaces, the logo rules, and the safe area for a brand (Northwind, in our example). The template gets built from it, and the image generation step takes its prompt prefix from it, leaving the validator to check against it. Each color has a token name, which the editor shows as a convenient swatch label.

```typescript
// src/brand-kit.ts
/** Colors the user may pick from, as hex. Nothing else is allowed. */
export const PALETTE = {
  blue: '#1E5ADC',
  navy: '#102050',
  mist: '#EEF2FA',
  white: '#FFFFFF',
} as const;

/**
 * The two approved typefaces. Both ship in CE.SDK's default typeface
 * library, so there are no font files to host.
 */
export const FONTS = {
  headline: 'Montserrat',
  body: 'Open Sans',
} as const;

export const ALLOWED_FONTS: readonly string[] = Object.values(FONTS);

export const LOGO_RULES = {
  /** Width to height of the logo files, 600 by 200. */
  aspectRatio: 3,
  /** Smallest width the logo may be exported at, in pixels. */
  minWidth: 180,
  /** Empty space around the logo, as a fraction of its height. */
  clearSpace: 0.5,
};

/** Every block must sit this many pixels inside the page edges. */
export const SAFE_AREA_INSET = 64;
```

It's important to realize that the config only describes the rules. `installBrandKit` is what makes the editor follow them. We're using the pattern from the IMG.LY [Enforce brand guidelines](https://img.ly/docs/cesdk/js/rules/enforce-brand-guidelines-23a1e3.md) guide. In our example, the editor's color and typeface libraries are replaced with the brand's.

```typescript
// src/brand-kit.ts
engine.asset.addLocalSource('brand.colors');
for (const [token, hex] of Object.entries(PALETTE)) {
  const { r, g, b } = toRGBA(hex);
  engine.asset.addAssetToSource('brand.colors', {
    id: `brand.color.${token}`,
    label: { en: token },
    payload: { color: { colorSpace: 'sRGB', r, g, b } },
  });
}
cesdk.ui.updateAssetLibraryEntry('ly.img.colors', {
  sourceIds: ['brand.colors'],
});

// The two approved faces, copied from CE.SDK's own typeface library so
// the font files match the SDK version.
const { assets } = await engine.asset.findAssets('ly.img.typeface', {
  page: 0,
  perPage: 100,
});
engine.asset.addLocalSource('brand.fonts');
for (const asset of assets) {
  if (asset.label != null && ALLOWED_FONTS.includes(asset.label)) {
    engine.asset.addAssetToSource('brand.fonts', {
      id: asset.id,
      label: { en: asset.label },
      payload: asset.payload,
      meta: asset.meta,
    });
  }
}
cesdk.ui.updateAssetLibraryEntry('ly.img.typefaces', {
  sourceIds: ['brand.fonts'],
});

cesdk.feature.disable([
  'ly.img.fill.color.picker',
  'ly.img.text.background',
  'ly.img.text.styles',
]);
```

To use your own brand, all you need to do is change the values in `brand-kit.ts`. If your typefaces aren't in CE.SDK's default library, simply register them from your own font files, just like the [CE.SDK custom fonts guide](https://img.ly/docs/cesdk/js/text/custom-fonts-9565b3.md) explains.

<div class="image-narrow">

![The font list and the color panel offer only the brand kit](https://img.ly/_astro/brand-kit-font-and-color-panels.BvfzUHdr.webp)

</div>

## Step 2: Lock the Template and Scope What Users Can Change

CE.SDK has two roles that matter here. The [Creator role](https://img.ly/glossary/creator-role.md) ignores every lock and is for whoever designs the template. The [Adopter role](https://img.ly/glossary/adopter-role.md) enforces the locks and is for the people filling it in. `buildTemplate` builds a 1080 by 1350 page in the Creator role and hands it over in the Adopter role (see [Lock the template](https://img.ly/docs/cesdk/js/create-templates/lock-131489.md)). The page has four blocks, and what each one allows is a single table:

```typescript
// src/template.ts
const ALLOWED: Record<string, Scope[]> = {
  [BLOCK.logo]: [],
  [BLOCK.image]: ['editor/select', 'fill/change', 'layer/crop'],
  [BLOCK.headline]: [
    'editor/select',
    'text/edit',
    'text/character',
    'fill/change',
    'layer/move',
    'layer/resize',
  ],
  [BLOCK.legal]: [],
};
```

The logo and the legal line don't allow any changes or even selection. The image slot allows us to replace and crop its picture if we need to, and the headline can be retyped, moved and resized. Its font and color can be changed, but only from the lists in step 1. On a text block, `fill/change` is the permission used for its color.

```typescript
// src/template.ts
// Locks: global scopes defer to the blocks, every block starts with
// nothing allowed, then gets back only what ALLOWED lists.
for (const scope of engine.editor.findAllScopes()) {
  engine.editor.setGlobalScope(scope, 'Defer');
}
for (const block of [page, logo, image, headline, legal]) {
  for (const scope of engine.editor.findAllScopes()) {
    engine.block.setScopeEnabled(block, scope, false);
  }
  for (const scope of ALLOWED[engine.block.getName(block)] ?? []) {
    engine.block.setScopeEnabled(block, scope, true);
  }
}

// The image slot is a placeholder: behavior on the fill, the overlay
// and Replace button on the block.
engine.block.setPlaceholderEnabled(image, true);
engine.block.setPlaceholderBehaviorEnabled(engine.block.getFill(image), true);
engine.block.setPlaceholderControlsOverlayEnabled(image, true);
engine.block.setPlaceholderControlsButtonEnabled(image, true);

engine.editor.setRole('Adopter');
```

There are two things that are important to understand about how this setup works. Every global scope is set to Defer because a global Allow overrides a block's own lock. Every scope is switched off on every block before the allowed ones go back on, so a block's permissions are exactly the same as its list. The page doesn't allow anything either, so picking a library image with the slot selected will replace the picture instead of adding a new block (see the [CE.SDK placeholders guide](https://img.ly/docs/cesdk/js/create-templates/add-dynamic-content/placeholders-d9ba8a.md)), which is what we're after when we want to lock a brand's design in.

In the test run you can see that clicking the logo didn't select anything, and pressing Delete didn't remove the headline. To change what users are allowed to edit, all you need to do is change `ALLOWED`, and `engine.editor.findAllScopes()` will list every scope there is.

<div class="image-narrow">

![The locked template as a user first sees it, with the image slot waiting for a picture](https://img.ly/_astro/locked-template-first-view.BbJCipIi.webp)

</div>

One thing that the locks don't do is bind code. In the test run, moving the locked logo through the engine API still worked in the Adopter role because the locks cover people using the editor, and step 4 handles everything else.

## Step 3: Scope AI Generation to the Image Placeholder

The only code that generates an image here is `generateIntoPlaceholder`, and it has no target parameter so that it always writes into the image slot.

```typescript
// src/generate.ts
export async function generateIntoPlaceholder(
  cesdk: CreativeEditorSDK,
  client: GatewayClient,
  prompt: string
): Promise<void> {
  // The placeholder is 4:3, one of the formats Ideogram V3 accepts, so the
  // image fills it without cropping.
  const input = await buildInput(client, MODEL, {
    prompt: PROMPT_PREFIX + prompt,
    format: '4:3',
    style: 'REALISTIC',
  });
  const uri = await persistImage(await client.generate(MODEL, input, {}));

  const engine = cesdk.engine;
  const image = findBlock(engine, BLOCK.image);
  const fill = engine.block.getFill(image);
  // A library pick leaves a source set on the fill, which would win over
  // the new URI, so clear it.
  engine.block.setSourceSet(fill, 'fill/image/sourceSet', []);
  engine.block.setString(fill, 'fill/image/imageFileURI', uri);
  engine.block.resetCrop(image);
  // The slot is filled now. The editor does the same when a user picks
  // an image, which hides the placeholder overlay; the user can still
  // replace the image, because the block's scopes are unchanged.
  engine.block.setPlaceholderEnabled(image, false);
  engine.editor.addUndoStep();
}
```

The user's prompt goes in after the brand kit's prefix, and the call goes through IMG.LY's AI gateway to Ideogram V3. The slot is 952 by 714 pixels, so a 4:3 image fills it exactly. `persistImage` downloads the result before it goes into the block, because the gateway's image links are short-lived.

In the test run, three generations took between 20 and 26 seconds with one model call each, and every image landed in the slot without needing to move blocks or anything else. If you would like to use another model, change `MODEL` to any text-to-image model that the gateway lists, and check which formats it accepts.

<div class="image-narrow">

![One generation, written into the image slot](https://img.ly/_astro/generation-in-image-slot.D8ZvK2PW.webp)

</div>

## Step 4: Build the Export Validator

The locks and the libraries stop a user from breaking the rules in the editor, and the validator catches everything else, like a design file from somewhere else, an integration that writes to the design, or code using the API. It runs six checks on the design as it is, and each check returns the rule, whether it passes or fails, and a message that anyone who isn't a designer can follow if they get stuck.

| Check            | Rule, from the brand kit                                    | Message when it fails, from the test run                                                                                 |
| ---------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Palette          | Every fill and text color is one of the four palette colors | The page background is #E4002B. Use a palette color: #1E5ADC, #102050, #EEF2FA, #FFFFFF.                                 |
| Typefaces        | All text uses Montserrat or Open Sans                       | The headline uses Lobster Two. Pick one of the brand typefaces.                                                          |
| Logo size        | The logo is at least 180 px wide                            | The logo is 120 px wide. Set it back to 180 px or wider.                                                                 |
| Logo clear space | Nothing sits within half the logo's height of it            | The headline is inside it. Keep 40 px clear on every side of the logo.                                                   |
| Safe area        | Every block sits 64 px inside the page edges                | The headline is 48 px outside it. Move it back inside the safe area.                                                     |
| Content          | No banned word in any text, and no refused image category   | The headline uses a word the brand doesn't allow (guaranteed, cheapest, miracle, cure). Change the image or the wording. |

```typescript
// src/validator.ts
export async function validate(engine: CreativeEngine): Promise<CheckResult[]> {
  return [
    checkPalette(engine),
    checkFonts(engine),
    checkLogoSize(engine),
    checkLogoClearSpace(engine),
    checkSafeArea(engine),
    await checkContent(engine),
  ];
}
```

The first five checks read the design through the engine API. The safe area check measures each block against the page and reports how far outside it is.

```typescript
// src/validator.ts
function checkSafeArea(engine: CreativeEngine): CheckResult {
  const rule = `Everything sits ${SAFE_AREA_INSET} px inside the page edges`;
  const pageBox = bounds(engine, page(engine));
  const problems: string[] = [];
  for (const block of designBlocks(engine)) {
    const box = bounds(engine, block);
    const outside = Math.max(
      SAFE_AREA_INSET - box.x,
      SAFE_AREA_INSET - box.y,
      box.x + box.width - (pageBox.width - SAFE_AREA_INSET),
      box.y + box.height - (pageBox.height - SAFE_AREA_INSET)
    );
    if (outside > 0.5) {
      problems.push(
        `${label(engine, block)} is ${Math.round(outside)} px outside it`
      );
    }
  }
  return result(rule, problems, 'Move it back inside the safe area.');
}
```

The content check is the only one that leaves the browser and works behind the scenes. It uses [Stream's moderation API](https://getstream.io/moderation/docs/node/content-moderation/check/), which only runs server-side, so our demo has a small server function that holds the Stream secret. The browser renders the page as a JPEG under Stream's default 250 KB image limit and sends it with each text block in the design. The server forwards both of them to Stream's analyze endpoint, which is what provides answers about whether to keep, flag or remove items from each field:

```typescript
// server/moderate.ts
// A stateless call: verdicts only, nothing goes to a review queue.
const body = new FormData();
body.append(
  'payload',
  new Blob([JSON.stringify({ config_key: MODERATION.policy, texts })], {
    type: 'application/json',
  })
);
body.append('image:design', image, 'design.jpg');

const response = await fetch(`${ANALYZE_URL}?api_key=${apiKey}`, {
  method: 'POST',
  headers: {
    Authorization: serverToken(apiSecret),
    'stream-auth-type': 'jwt',
  },
  body,
});
return json(await response.json(), response.status);
```

The Stream policy comes from the brand kit too: `npm run setup:stream` creates a blocklist of words that the brand never uses, and a list of image categories it refuses, like alcohol and gambling. If the check can't reach Stream, then it fails, because an unchecked design isn't a passing one.

To add a check, all you need to do is write a function that takes the engine and then returns a rule along with a result and a message. Then add it to `validate`.

## Step 5: Gate the Export on a Passing Validation

`exportChecked` runs the validator during the export and refuses if any check fails. It doesn't trust an earlier result either, because code can change a design without the editor picking it up. `gateEditorExports` uses the same check when the editor's Export Images and PDF buttons are pressed, so there's no sneaking around it.

```typescript
// src/export.ts
export async function exportChecked(
  cesdk: CreativeEditorSDK,
  mimeType: 'image/png' | 'application/pdf' = 'image/png'
): Promise<ExportResult> {
  const engine = cesdk.engine;
  const report = await validate(engine);
  if (report.some((check) => !check.passed)) return { report, file: null };

  const page = engine.scene.getPages()[0];
  return { report, file: await engine.block.export(page, { mimeType }) };
}

/**
 * Wraps every export action the editor registered (its Export Images
 * and PDF buttons run these) so the validator runs first. A failing
 * design gets a notification listing what to fix instead of a file.
 */
export function gateEditorExports(cesdk: CreativeEditorSDK): void {
  for (const id of cesdk.actions.list({ matcher: 'export*' })) {
    if (id === 'exportScene') continue; // saving a draft is not publishing
    const original = cesdk.actions.get(id);
    if (original == null) continue;
    cesdk.actions.register(id, async (...args: any[]) => {
      const report = await validate(cesdk.engine);
      const failed = report.filter((check) => !check.passed);
      if (failed.length > 0) {
        cesdk.ui.showNotification({
          type: 'error',
          message: `Export blocked: ${failed.map((check) => check.message).join(' ')}`,
          duration: 'long',
        });
        return;
      }
      return (original as (...a: any[]) => unknown)(...args);
    });
  }
}
```

In the demo's panel, the export button stays locked until a check passes, and any edit locks it again. The lock is for the user and the check inside `exportChecked` is what acts as our gate.

<div class="image-column">

![A failing report keeps the export button locked, and a passing report enables it](https://img.ly/_astro/failing-and-passing-report.eiHsls-T.webp)

</div>

## Step 6 (Optional): Add a Human Approval Step

Some teams have specific checks and validations that require a person to sign off. This could be for situations that involve new campaigns, regulated product claims, or co-branded assets that need both parties to accept.

The validator makes that review faster to complete, because the reviewer doesn't have to check colors, fonts and margins when the validator has already handled them. The approval step belongs at the export gate, after the checks pass.

## How to Verify: Four Refusals and One Clean Export

The demo's panel has test buttons that you can try out for yourself. Each breaks a rule through the engine API, much the same way a file from somewhere else could if you were trying to use it. Break a rule, click Check the design, and read the report. In the test run, each button failed the check that it targets.

<div class="image-column">

![The palette check refusing an off-palette background](https://img.ly/_astro/refused-off-palette-background.C373ntLp.webp)

![The typeface check refusing an unapproved font](https://img.ly/_astro/refused-unapproved-font.BLVNZu0h.webp)

![The logo size check refusing a logo below the minimum width](https://img.ly/_astro/refused-logo-below-minimum-width.D1PqMMxH.webp)

![The safe area check refusing a headline outside the safe area](https://img.ly/_astro/refused-headline-outside-safe-area.BeZTLNg5.webp)

</div>

The content check refused a headline with a banned word, "Guaranteed quiet, wherever you go", through Stream's blocklist.

<div class="image-column">

![The content check refusing a banned word in the headline](https://img.ly/_astro/refused-banned-word.DgvFY9os.webp)

</div>

To validate how it works for yourself, you can undo the change and check again. When all six checks pass, the export button is enabled and you can download the PNG. In the test run, a full check, including the call to Stream, took between half a second and a second, which is not too shabby.

<div class="image-column">

![All six checks passing on the finished ad](https://img.ly/_astro/all-checks-pass.BdJFnWml.webp)

</div>

## When a Checklist and a Shared Library Are Enough

**One designer makes everything.** If a small in-house team produces everything themselves, the people making the assets are the people who know the rules. A guidelines document and a shared library should be all that is needed to produce compliant content.

**Low volume, with a review already in place.** If a handful of assets go out every month and they all go past a brand manager on their way out, then a validator will save a little bit of time, but it might not make enough of a difference. Look at building it when the number of people making assets grows to the point where the reviewer can't cope.

**Rules that code can't check.** Issues of style and taste like art direction, tone of voice, or whether a photo "feels right" for the brand are not measurements that a machine can decide for you. The validator handles the rules that you can measure, and a checklist plus a person can do the rest.

## FAQ: On-Brand AI Content and Export Validation

### Can the AI still be creative inside a locked template?

Yes it can, but only inside the image slot. The model gets the user's prompt after the brand prefix and it can generate any picture that passes the content check. The template only decides where the picture goes and what is around it.

### What exactly does the validator check?

It checks six things: every color comes from the palette, all text uses an approved typeface, and the logo is at least its minimum width. It also checks that nothing sits in the logo's clear space, every block is inside the safe area, and the text and image pass the content check. Each check returns the rule, whether it passes or fails, and a message that says what needs to change.

### Can I add my own checks?

Yes you can. Each check is a function that takes the engine and returns a rule, a result and a message. You can add yours to `validate` in `src/validator.ts` if there is anything specific that isn't catered for in the example app, like a minimum font size or an unchanged legal line.

### How do I handle exceptions such as co-branded assets?

Give the exception its own brand kit and template, with both logos and a palette that covers both brands. The validator reads its rules from the brand kit, so a second kit means a second set of rules without adding new checks.

### Does the moderation hook work with my own provider?

Yes. The browser side sends a JPEG and the design's text to `/api/moderate`, and only `server/moderate.ts` talks to Stream. If you want to, you can replace that call with a call to your own provider. You can also map its answer to keep, flag or remove per field in `src/moderation.ts`.

### Can I log who exported what?

The demo doesn't log anything by default, but `exportChecked` is the place to implement it if you need logging. It has the report and the file, so you can write a record with the user, the time, the template and the report before it returns. Every export path goes through it, so the log can cover the editor's own buttons too.

## Glossary: Brand Guardrail Terms

**Brand kit.** In this article, your own config file of palette, typefaces, logo rules and safe area. Not a CE.SDK feature name.

**Locked block.** A block whose [scopes](https://img.ly/glossary/scope.md) are switched off, so a user in the Adopter role can't select, move, resize, delete or change it.

**Placeholder scope.** The permissions on a [placeholder block](https://img.ly/glossary/placeholder.md) that decide what a user can do with it, such as replacing its image (`fill/change`) or cropping it (`layer/crop`).

**Safe area.** The part of the page, inside a fixed margin from every edge, where all content must sit so it isn't cut off or crowded.

**Clear space.** The empty margin a brand requires around its logo. Here it's half the logo's height on every side.

**Validator.** Code that checks a design against the brand rules and returns, for each rule, pass or fail and a message.

**Content moderation.** Checking text and images against a policy of refused words and categories, here through Stream's API.

## Next Steps

Clone the [brand guardrails repository](https://github.com/imgly/blog-brand-guardrails-ai) and run it locally with your own IMG.LY keys. The [Lock the template](https://img.ly/docs/cesdk/js/create-templates/lock-131489.md) and [Enforce brand guidelines](https://img.ly/docs/cesdk/js/rules/enforce-brand-guidelines-23a1e3.md) docs cover the roles, scopes and brand libraries that this build uses. The companion post shows how to [generate an ad as editable layers](https://img.ly/blog/edit-ai-output-without-regenerating.md) instead of one flat image. For a CE.SDK trial license, see the [pricing page](https://img.ly/pricing.md).

---

## More Resources

- **[IMG.LY Website](https://img.ly/index.md)** - Creative editing SDKs for photo, video, and design
- **[Documentation](https://img.ly/docs/cesdk/)** - CE.SDK developer documentation
- **[Build with a coding agent](https://img.ly/SKILL.md)** - A skill that adds a CE.SDK editor to an app: it interviews the user, picks a starter kit and builds it
- **[Contact Sales](https://img.ly/forms/contact-sales.md)** - Get a custom quote. A public JSON API accepts the request directly, no account or key needed. Ask your user for consent and their details first.
