This guide walks you through adding a photo editor to your website with CreativeEditor SDK (CE.SDK). You’ll start from the Photo Editor starter kit, load an image into it, run it locally, and wire its export button to your own backend.
The starter kit is a small Vite project. It ships the editor’s entire configuration as source files inside your project, so once it’s running you change the editor by editing those files. Before you build it, you can try the live demo to see what you’re aiming at.
What you’ll need
- Node.js v22 or newer. Get it from nodejs.org if you don’t have it.
- A supported browser. Chrome 114+, Edge 114+, Firefox 115+, or Safari 15.6+. The full matrix is in Browser Support.
- A CE.SDK license key, optional while you’re building and needed in production. Start a free trial to get one.
Leave the key unset for now. Without one the editor runs in evaluation mode. An IMG.LY watermark sits in the bottom right of the editor viewport from the moment it loads, and a second one runs across every image you export. The browser console prints an evaluation banner at error level on every run, whether you serve the assets yourself or not, so that red line is evaluation mode rather than a fault.
Step 1: Get the starter kit
Clone the Photo Editor starter kit and install its dependencies. The release-1.82.1 tag pins the kit’s own source. It does not pin the SDK: the kit declares ^1.82.1 for @cesdk/cesdk-js, @cesdk/engine, and @imgly/plugin-background-removal-web, and ships no lockfile, so npm install resolves the newest 1.x available on the day you run it. Set those three to exact versions in package.json if you want them to keep matching the asset archive you download in the next step. The kit’s configuration plugin also calls cesdk.setEditorCompatibilityVersion('1.82.1'), which keeps the editor behaving as it did when the kit was cut.
git clone --branch release-1.82.1 https://github.com/imgly/starterkit-photo-editor-ts-web.git photo-editor
cd photo-editor
npm install
That pulls in @cesdk/cesdk-js, the engine, the background removal plugin, and the ONNX runtime it uses. None of it needs a server. The whole editor runs in the browser.
Step 2: Download the editor assets
CE.SDK keeps its fonts, icons, filters, stickers, and shapes out of the JavaScript bundle and loads them at runtime. Download the archive for your SDK version and unzip it into public/, which Vite serves at the site root.
curl -O https://cdn.img.ly/packages/imgly/cesdk-js/1.82.1/imgly-assets.zip
unzip imgly-assets.zip -d public/
rm imgly-assets.zip
It’s a large download, so give it a minute. You end up with a public/assets/ folder holding the engine’s WebAssembly core alongside the asset libraries.
Self-hosting the assets is required for production use, and doing it now saves you from changing the setup later. If you want the editor running before you download anything, you can leave the baseURL line out of the config in Step 3 and let the engine fall back to the IMG.LY CDN, which warns in the console that the CDN is meant for development. Leaving that line in before you have unzipped the archive is the one combination that does not work: the engine looks under /assets regardless, does not find the WebAssembly core there, and you get a blank page and a WasmLoadError.
Step 3: Point the editor at an image
Drop the photo you want to open into public/. This guide calls it example.jpg.
Now open src/index.ts and replace it with this:
import CreativeEditorSDK from '@cesdk/cesdk-js';
import { initPhotoEditor } from './imgly';
import { resolveAssetPath } from './imgly/resolveAssetPath';
const config = {
userId: 'photo-editor-demo',
baseURL: '/assets',
license: import.meta.env.VITE_CESDK_LICENSE,
};
CreativeEditorSDK.create('#cesdk_container', config)
.then(async (cesdk) => {
await initPhotoEditor(cesdk);
await cesdk.createFromImage(resolveAssetPath('/example.jpg'));
})
.catch((error) => {
console.error('Failed to initialize CE.SDK:', error);
});
baseURL tells the engine where the assets from Step 2 live, so leave that line out if you skipped the download. license reads a Vite environment variable, which is unset until you supply a key, so the editor runs in evaluation mode for now and your key never sits in a source file. userId is how CE.SDK deduplicates monthly active users when the same person opens your app from two devices. The constant above is fine for a local run, but it must not ship as a single shared value for every end user: pass the stable id your own system already holds for the signed-in person, or leave the field out for anonymous sessions. initPhotoEditor applies the starter kit’s configuration, which is what turns a blank canvas into a photo editor. createFromImage builds a scene sized to your image rather than to a fixed page.
Note the resolveAssetPath call around the image path. The engine resolves a relative URL against baseURL, not against the page, so a bare /example.jpg is looked up at /assets/example.jpg instead. Under npm run dev that lookup does not produce a 404 you can spot in the network tab: Vite answers an unknown path with its HTML fallback, so the request returns 200 and the console reports that the image could not be decoded. The helper ships with the kit and turns a root-relative path into an absolute URL. A full https:// URL works too, though then you’ll need CORS on the host serving it.
The editor mounts into #cesdk_container, which the kit’s index.html already provides.
Step 4: Run it
npm run dev
Open http://localhost:5173. Your image sits on the canvas with a dock down the left side: Crop, Adjust, Filter, Effects, BG Removal, Text, Shapes, and Stickers. Export Image sits top right, undo and redo top left.
Background removal is worth a click. It runs the model in the browser through the ONNX runtime, so the photo never leaves the page. The model weights are a separate download, fetched from staticimgly.com the first time you use the tool rather than from the archive in Step 2, so the first click takes a moment and needs network access.
Where the editor’s tools come from
Everything you see is configuration, and all of it is in your project under src/imgly/:
src/imgly/
├── index.ts # Wires up the config, the plugins, and the asset sources
├── resolveAssetPath.ts # The path helper from Step 3
└── config/
├── plugin.ts # The photo editor configuration plugin
├── actions.ts # Export, save, and import handlers
├── features.ts # Which tools are switched on
├── i18n.ts # Labels and translations
├── settings.ts # Engine settings
├── keyboard/ # Keyboard shortcuts and their key catalogs
└── ui/ # Dock, inspector bar, navigation bar, canvas, panels
Seven of the eight dock tools are configured in config/ui/dock.ts, so that is the file to edit when you want to drop one. Renaming depends on which tool you mean. Crop, Adjust, Filter and Effects carry their labels as literal strings inside dock.ts itself, while Text, Shapes and Stickers carry translation keys, and the three values those keys resolve to sit in config/i18n.ts. The eighth is BG Removal, and it appears in neither file. src/imgly/index.ts adds it to the dock from the background removal plugin, and its label arrives with the plugin. Export Image is a case of its own: src/imgly/index.ts sets that label after the configuration plugin has run, so an actions.export.image override placed in config/i18n.ts is discarded and you change the literal in index.ts instead. To change what Export Image does, edit config/actions.ts, which is the next step.
Step 5: Send the export to your backend
By default Export Image downloads a PNG to the user’s machine. In a real product you usually want it on your server instead.
This step assumes you already have somewhere to post to. The kit’s dev server has no /api/upload route and will not grow one, so before you wire the button up, either point the fetch below at a route your own backend serves or stand one up that accepts a multipart POST. Then open src/imgly/config/actions.ts and replace the exportDesign registration with this:
cesdk.actions.register('exportDesign', async (exportOptions) => {
const { blobs } = await cesdk.utils.export(exportOptions);
const formData = new FormData();
formData.append('image', blobs[0], 'edited-photo.png');
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed with status ${response.status}`);
}
const { url } = await response.json();
console.log('Uploaded to:', url);
});
cesdk.utils.export renders the current design and hands you the blobs, which are ordinary Blob objects. Post them to an endpoint or write them straight to object storage, whichever fits your product. With an endpoint in place, clicking Export Image sends a PNG to /api/upload as multipart form data under the field name image.
Check response.ok before reading the body, the way the snippet does. Without it, a 500 from your upload service or an auth redirect answered as JSON reads as a successful upload and logs whatever url came back. Watch the network tab as well as the editor. CE.SDK puts up a success dialog as soon as the render finishes, titled “Export complete” with “You can close this dialog.” beneath it, and it does not wait to see how your handler settled. So a failing POST leaves the editor looking like everything worked.
The same file has commented-out handlers for saving a scene as JSON, exporting an archive with the assets embedded, and sharing through the Web Share API. Uncomment the ones you need.
Before you ship
Three things still need changing before this goes live.
Add your license key. Create a .env file beside package.json holding VITE_CESDK_LICENSE= followed by your key, and the editor leaves evaluation mode, so both watermarks go away. The kit ships a .env.example carrying that variable name, but copy the name rather than the file, because the engine rejects the YOUR_LICENSE_KEY_HERE placeholder and refuses to load. The kit’s .gitignore already lists .env, so the key stays out of your commits.
Give every user their own userId. The constant from Step 3 is a stand-in. Feed the field the stable identifier your own system holds for each signed-in person, or omit it for anonymous sessions, so the monthly active user count reflects people rather than one shared string.
Serve the assets from your own infrastructure. Self-hosting is required for production use. Copy public/assets/ to a versioned path on your CDN and point baseURL at it, for example https://cdn.yourdomain.com/cesdk/1.82.1/. Versioned paths let you run two SDK versions side by side and roll back cleanly. Background removal needs its own decision, since its weights come from staticimgly.com rather than that archive: allow the host in your content security policy, or host the files yourself and pass their location as publicPath in the plugin’s provider.configuration. Serve Assets covers the rest, including how to leave out the asset libraries you don’t use so the editor loads faster.
From there the Photo Editor starter kit docs cover theming, localization, forced crop ratios, and custom UI components, and the configuration reference lists every option you can pass to create. If you’d rather read about the product first, the Photo Editor SDK page is the place to start.
Shipping the same feature on mobile? We have a photo editor guide for iOS and a photo editor guide for Android.

