Build a Sitecore Connector for Any DAM

The architecture behind a custom Marketplace app for SitecoreAI — the field design, the six platform limits, and how to rebuild it for your own DAM in a week.

· 25 min read · AI , Sitecore AI

1Why we did not replace anything

We run a large martech stack. Sitecore for the web, a DAM for the assets, and the usual crowd of analytics, CRM and campaign tools around them. Once a year someone senior asks whether we should replace one of the pieces.

The answer is almost always no. Replacing a platform is never just a licence swap. You pay for migration, retraining, integration rework, a slower delivery quarter, and a long tail of small things nobody put in the business case. Extending a platform you already own costs a fraction of that.

So the real question is not "is there something better out there". It is "is this vendor's roadmap clear enough that I am happy building on top of it". If yes, extend. If no, start the replacement conversation properly.

For Sitecore the answer was yes. The Marketplace extension model is clearly where they are investing, which means anything I build on it improves over time instead of becoming a customisation I have to defend at every upgrade.

2What we needed to build

This summer the question landed on my desk in a smaller shape. Our authors were downloading images from Brandworkz, our DAM, and uploading them into Sitecore by hand. Every upload made a second copy of an asset the brand team thought they controlled. Nobody could say where a given image was used. Expired licences stayed live on the site because nothing in Sitecore knew a licence existed.

I looked at three routes.

Buy the vendor's connector

Some DAM vendors publish one. Bynder does. Ours does not. That closed the option in ten minutes, though I still spent a day studying Bynder's connector, because it is the closest thing to a reference implementation the market has. That day paid for itself later.

Sync the DAM into the Media Library

Copy assets across on a schedule and let authors pick them normally. It demos well. Then you own a sync engine, a storage bill, two systems that both think they own the asset, and a stale-copy problem that grows every month. Cheap in a slide, expensive in year two.

Build our own Marketplace app

Write the app, host it, register it in our Cloud Portal, and let Sitecore load it where authors need it. More work up front, but we own the behaviour and the data shape, and the DAM stays the single source of truth.

I picked the third one. A useful detail if you go the same way: a Marketplace app does not have to be public. You can register it as a custom, organisation-scoped app inside your own Cloud Portal. No review, no listing, no approval cycle. Publishing it later is a new App ID over the same code.

3The architecture came first

Everything else in this article depends on this section. Before a single screen was drawn I wrote down who the parties are, which one is allowed to talk to which, what gets stored where, and which parts I expect to change later. An hour of that saved weeks, and it is the part an AI agent cannot do for you, because it is all judgement and no typing.

Here is the whole integration on one page.

Author’s browser SitecoreAI / XM Cloud Pages page builder + plugin field Connector iframe custom field · full screen · standalone Connector app Authoring UI (React) picker · card · rich text · admin Server-side API layer /api/bw/*  ·  /api/sitecore/* all secrets live here, never in the browser The DAM REST API search · folders · login (session) Delivery CDN public URLs and thumbnails XM Cloud field stores the JSON reference only no binaries, no proxy items Authoring GraphQL API Experience Edge (publish) Head application the renderer package parse the field value Image / Video / File / RichText components a bad value renders nothing, never an error Site visitor HTML from the head application the image tag points at the DAM CDN and loads from there HTTPS clean data only server-side auth Marketplace SDK read and write the field value Authoring API (admin key required) field JSON HTML assets load from the CDN The browser never calls the DAM. The server-side API layer is the only caller of the DAM and of the Sitecore Authoring API, and the only holder of credentials.
The integration architecture. Six parties, and every channel between them. Two properties define the whole design: the browser never calls the DAM, and the live site renders from stored URLs with no round trip to the DAM at all.

The four runtime parties

Party Role
XM Cloud (Pages)Loads the connector in iframes at registered extension points. Stores the field value. Never stores a binary.
The connector appThe authoring UI plus a server-side API layer. Owns all secrets. The only party that talks to the DAM or to the Sitecore Authoring API.
The DAMSystem of record for assets and metadata. Provides search, the folder taxonomy, asset detail and delivery URLs.
The head applicationThe public site. Reads the field value, emits image, video or link tags pointing at DAM delivery URLs. No DAM calls at render time.

Two parts I expected to change, so I isolated them

This is the highest-value paragraph in the architecture. Two surfaces were outside my control, so each one lives behind exactly one module. When either changes, one file changes and nothing else notices.

The Marketplace SDK

Pre-1.0 and still moving. All of it sits behind one folder: one hook that starts it up with a timeout, and one module that is the only caller of read, write and close. Nothing else in the app imports the SDK.

Public URL resolution

How a DAM produces a permanent public link differs by vendor, by licence and sometimes by tenant. All of that logic lives in one file, and it is built to fail loudly rather than store a link it cannot stand behind.

The third isolation is the picker itself, which never imports the SDK at all. It talks to a small interface instead, and that decision is worth its own section later, because it ended up serving five different surfaces without forking.

4What Sitecore actually stores

There are two ways to answer this, and they lead to two completely different products.

Copy the asset. For every selection, create a media item in Sitecore that mirrors the DAM record. You get "where used" for free, plus Content Editor support and search indexing. You also get a sync engine, two owners for one asset, and a lifecycle mess.

Reference the asset. Store a small JSON object in the field. No file ever enters Sitecore.

I checked what the mature commercial connector does before deciding, and it settled the argument. Bynder does not create per-asset media items and does not sync the DAM. It stores JSON in the field and ships two supporting pieces around it: base templates and a rendering package. If the vendor with the most experience of this problem chose the reference model, the burden of proof is on the other option.

So this is the value that lands in the field:

{
  "schemaVersion": 1,
  "provider": "brandworkz",
  "assetGuid": "d76e2bc0-7933-4cc2-b4b0-3b8183279758",
  "type": "Image",
  "title": "spring-hero-mountains",
  "publicUrl": "https://cdn.example-dam.net/.../spring-hero_1920.jpg",
  "thumbnailUrl": "https://cdn.example-dam.net/thumbs/.../320x320.jpg",
  "rendition": "web-large-1920",
  "width": 7016,
  "height": 4961,
  "alt": "Hiker looking over a spring mountain valley",
  "folderPath": "Brand/Campaign 2026",
  "expiryDate": "2027-01-31",
  "keywords": ["spring", "outdoor"],
  "selectedAtUtc": "2026-07-07T14:05:00Z",
  "selectedBy": "author@example.com"
}

Multiple assets wrap in a versioned envelope. My first draft used a plain { assets: [...] } wrapper and I caught it in review. A wrapper with no version number defeats the point of having a version number.

Rich text is the one exception. Those fields store plain sanitised HTML, exactly like Sitecore's own rich text field, not this JSON. More on why in section 15.

A component in Pages with one field holding stored JSON and another holding its settings string
The same thing on a real component. The left field holds a stored value: schema version, provider, asset ID, title, the public URL, the rendition, alt text and the timestamp. The right field is still empty, so it shows its standard value instead — the settings string covered in the next section. One field type, two completely different shapes, which is exactly why the app has to decide what to show from the shape of what it reads. Client name, URLs and marketing imagery are blurred in this and the following screenshots.

Four rules that keep the format honest

  • One writer. Only one function ever produces a value, and it validates before it writes. A half-written value is not possible because no other code path can create one.
  • Read defensively. Every read goes through a schema check that never throws. It returns one of five states: empty, one asset, several assets, unreadable, or a version this build does not know. The last two send the author to a recovery screen with the raw value visible and untouched.
  • Adding optional fields is free. When we added a keywords snapshot, nothing broke and no version bump was needed. Removing or changing a field means a version bump and a conversation with the migration team.
  • The build checks the paperwork. If the format code changes without the format document changing in the same commit, the build fails.

That last rule matters more with an AI in the loop than without one. An agent will happily change a schema and move on. A build rule will not let it.

A real bug from this exact schema. We validated the asset ID as a standard UUID. Our DAM's real IDs are valid hex in the right shape but not standards-conformant, so every single insert was rejected. Validate the shape you were actually given, not the standard you assumed. That cost an afternoon.

5How the fields get built in Sitecore

This is the part most write-ups skip, and it is the part a content architect actually has to live with. A connector is not finished when the picker works. It is finished when someone can add a DAM field to a template in two minutes without typing a GUID.

The field type, and the one rule about Source

A DAM field in Sitecore is an ordinary template field with two settings:

Type: Marketplace Types → Plugin

Source: the connector's App ID, and nothing else

That second line is not a style preference. The Source must be the bare App ID. My design had per-field settings appended as query parameters, which seemed obvious. Do that and Pages silently fails to resolve the app: the field renders as a plain text area, the "Open app" button disappears, and no error appears anywhere. I spent an hour convinced I had broken the deployment.

Where per-field settings actually go

Settings live in the field's standard values, which is also what Bynder does. It works because of a small piece of platform behaviour you can lean on: an empty field returns its standard value when the app reads it. So the standard value holds a short settings string, and the app treats it as "empty field, configured like this" rather than as content.

Setting What it does
types=ImageRestrict which asset types the author can pick. Values are case-sensitive.
mode=single|multiOne asset or several.
max=NCap the number of assets in multi-select.
folder=<id>Lock browsing to one part of the DAM tree, so a campaign field only ever shows campaign assets.
rendition=<name>Force a default image size, so authors cannot accidentally place an 8000px original.
search=<term>Pre-fill a search so the field opens on something useful.
ui=richtextRender the rich text editor instead of the picker.

Because settings and content share one string, the app decides what to show from the shape of what it reads, and the order of those checks matters.

read the field one raw string what shape is it? settings check first never crashes types=Image&mode=single empty but configured, open the picker <p>...</p> rich text, open the editor {"schemaVersion":1,...} one asset, show the card {...,"assets":[...]} several assets, show the list anything else recovery screen, never a crash
Check for settings before trying to read JSON. The other way round and a perfectly normal empty field looks broken to the author.

Base templates, so nobody types a GUID

Telling an architect "create a plugin field, set the Source to this ID, and put a settings string in standard values" is a support ticket waiting to happen. So the connector ships ready-made base templates under its own folder in the template tree. Each one carries a single, correctly wired field. A site template inherits the matching base and the field arrives working.

Base template Field Behaviour
BaseImageBrandworkzImageOne image, images only
BaseVideoBrandworkzVideoOne video
BaseAudioBrandworkzAudioOne audio asset
BaseDocumentBrandworkzDocumentOne document
BaseSingleSelectBrandworkzAssetOne asset of any type
BaseMultiSelectBrandworkzAssetsSeveral assets, stored in the envelope
BaseRichTextContentThe rich text editor with the DAM image button
SettingsPortalUrl, DefaultsNon-secret configuration. Never credentials.

These are delivered two ways: as serialised items you can commit and deploy with the rest of your content, or pushed straight into an environment by a script that can be run repeatedly without making a mess. Both come from the same generator, so there is one definition of the truth and no wiki page to go stale.

How an architect actually uses them

  • One DAM field on a template: inherit the matching base. Done in a minute, nothing to configure.
  • Two or more DAM fields on one template: inheritance cannot give you the same field name twice, so add the plugin fields by hand and copy the Source and standard-value pattern from a base.
  • Name fields for humans. "Hero Image (Brandworkz)" tells an author what will open. "Asset1" does not.
Secrets never live in Sitecore items. The settings item holds URLs and toggles only. Credentials stay in server environment variables. If an item can hold a secret, one day it will.

Shipping a component has two halves

If you also ship droppable components that use these fields, plan for two deployments, not one. The Sitecore half is the datasource template, the rendering item and its place in the palette, all of which a script can push. The website half is registering the component name in the head application and deploying it.

Pages disables a palette tile until the editing host knows the component name, and there is no placeholder to fall back on. I found this out by scripting a perfect item deployment into a site where nothing could actually be added. Land both halves together.

6Six months we did not have

So the architecture was settled. Now price it the traditional way and you get two quarters. Wireframes and sign-off. A design system. A front end. A server layer. Templates. Renderers. An admin screen. Tests. Documentation. Nobody was going to fund six months of that to solve an image-copying problem.

So I did not run it as a traditional project. I ran it as an AI-led build, where I stayed on the decisions and an AI coding agent did the typing. It went from an empty repository to a working end-to-end connector in a handful of days of build time, and the git history still shows it: first commit to a verified insert inside Pages, inside one working week.

I want to be precise about what that does and does not mean, because there is a lot of noise about this right now.

What it changed

The cost of producing code, tests, scripts and documentation dropped to almost nothing. I could try an approach, see it working, and throw it away the same afternoon.

What it did not change

Everything in sections 3, 4 and 5 was still mine to decide. And every vendor limitation still had to be discovered by hand. No agent will tell you that a query string silently breaks your field. Only a live test does that.

That is the honest split. The architecture is the thinking. The build is the typing. Only one of those got faster.

7How I set up the AI workflow

Half a day, no product code. An AI agent with no rules produces a large pile of plausible code that nobody can maintain. Four things did most of the work.

A rules file the agent reads every session

One document at the root of the repo, mirrored under the filenames different AI tools look for, so the same rules load whichever tool I use that day. It holds the project context, the architecture decisions from sections 3 to 5, the things we will not build, the security rules and the quality bar. It reads like instructions to a new mid-level engineer, because that is roughly the right level. When something in it turns out to be wrong, I update it and every future session inherits the correction.

Folders that keep four kinds of work apart

This sounds like housekeeping and turned out to be the biggest single quality lever:

Folder What lives there
inputsThings I provide: designs, vendor emails, exports. The agent reads them and never edits them.
directivesProcedures in plain English. Registering the app, credentials, tunnelling for local development, release.
executionSmall, commented scripts that do repeatable things. Smoke tests, format validation, link checks, template deployment.
knowledgeThe project's memory. Decision log, the data format, verified API behaviour, and a file of things that cost more than fifteen minutes.
app / srcThe product. Reviewed code only. No experiments, no dead code, no half-finished stubs.
.tmpEvery scratch file: API dumps, throwaway scripts, generated samples. Never committed, always safe to delete.

The rule is that shipped code never imports from scratch folders, and scratch work never lands in product folders. A clean repository at the end of every session means nothing is in the wrong place. It sounds fussy. It is the difference between a codebase you can hand over and a demo you have to rewrite.

Push repetition into scripts, keep judgement with me

Anything I would otherwise check by hand became a script. Do not eyeball a CDN URL, run the URL checker. Do not read JSON to see if it is valid, run the validator. Do not click through the DAM to see if credentials still work, run the smoke test. Language models are probabilistic and errors compound across steps. Scripts are not. Every check I moved into code made the next twenty sessions more reliable.

Every failure gets written down before it is closed

This is the part I would keep even if I went back to writing everything by hand. When something broke, the fix was not finished until five things had happened:

  1. Reproduce it with the smallest repeatable thing. A failing test or a script, never a manual click-through.
  2. Fix it.
  3. Confirm the failing case passes and the suite is still green.
  4. Write down what was learned, the same day, in the knowledge folder.
  5. Add a test or extend a script so it cannot come back quietly.

The effect compounds. By week two the agent was starting each session by reading a file of verified API quirks it had discovered itself, so it stopped repeating the same mistakes. Most of this article came out of those notes.

Me architecture, decisions credentials, sign-off Rules how this repo works step-by-step procedures AI agent plans, writes, fixes and documents Scripts and tests prove it really works against the real systems The connector reviewed code only nothing half-finished Knowledge base decision log · verified API behaviour · gotchas · the data format every failure written down read before every session
The loop. I stay on the left, on decisions. The agent works in the middle. Scripts and tests decide whether something is actually done. Anything learned goes into the knowledge base, which is the first thing read at the start of the next session.
If you take one thing from this section: the value is not in the code generation. It is in a loop where the agent's mistakes get caught by scripts, written down once, and never repeated. Skip the loop and you get fast code and slow progress.

8Step 1: Draw the screens

With the architecture written down, the build starts with pictures. Not because design comes first as a principle, but because a wireframe is the cheapest place to have an argument. Changing a picture takes a minute. Changing a built screen takes an afternoon and an opinion about whether it was worth it.

I described the journeys in plain English: an author opens an empty field, searches, opens an asset, adds alt text, inserts it, and sees a card. Then the failure paths, which are the ones people forget: no results, no connection, an expired asset, a value nobody can read.

That produced these eight wireframes. Between them they cover every surface the connector ended up shipping, so they are also the quickest way to see what the product actually does.

WF-01 - An empty field in Page builder
WF-01 — An empty field in Page builder
What the author sees before anything is picked: the component's content panel, the Brandworkz field with an Open app button, the alt text box, and a note showing the field type, its Source and what gets stored.
WF-02 - Browse and search
WF-02 — Browse and search
The picker itself: folder tree on the left, search with type filters and sort along the top, a results grid with selection and hover preview, paging, and Insert asset.
WF-03 - Asset detail
WF-03 — Asset detail
Open a card and you get the large preview, the DAM metadata, the size to insert, and the alt text field that has to be filled before the insert button will do anything.
WF-04 - The field once an asset is in it
WF-04 — The field once an asset is in it
The stored JSON rendered back as a card: thumbnail, the technical line, a link health check, and Replace, Preview and Remove.
WF-05 - Connect and sign in
WF-05 — Connect and sign in
Signing in to the DAM per author, so search results respect that person's own permissions. Supports SSO, with a shared service account as an option the administrator can switch on.
WF-06 - Admin settings
WF-06 — Admin settings
The administrator's screen: connection test, sign-in mode, default sizes, and whether DAM metadata gets stored in the field.
WF-07 - Loading, empty and error
WF-07 — Loading, empty and error
The states people forget to design. Skeletons while loading, an empty result that suggests what to try next, and a DAM unreachable message that tells the author their work is safe and the field is unchanged.
WF-08 - The context panel
WF-08 — The context panel
Parked for phase two, but designed anyway: quick search, recently used assets on this site, and a per-page audit showing how many DAM assets are on the page, how many links are healthy and how many expire soon.

Reading them back months later, the useful thing is how little changed. The picker shipped almost exactly as drawn. The one screen that moved was the rich text editor, which did not exist yet at this point and arrives in section 14 after a fight with the platform.

My job

Describe the journeys and the failure paths. Decide what an author must not be allowed to do, like inserting an image with no alt text.

The AI's job

Turn those journeys into labelled wireframes and a numbered requirement for every behaviour, so nothing gets built that nobody asked for.

One rule made everything downstream easier: every screen got an ID and every requirement got a number. From then on, work was either traceable to one of those or it did not get built. That is how you stop an eager agent adding features nobody wants, and it is how you keep a small project honest.

9Step 2: Build the design system

The app runs inside Sitecore, so it has to look like it belongs there. Sitecore publishes its own design system, Blok, and using it is the difference between an app that feels native and one that feels bolted on. That decision took ten minutes and saved weeks.

Later we wanted the connector to carry our own brand too, which is where most teams either fork the design system or start overriding styles screen by screen. Instead the brand became a thin layer on top: swap one colour scale, keep everything else. Because every button and tab already referenced the primary colour by name rather than by hex code, changing one file rebranded the whole product.

Two details worth copying. Status colours were left alone, because success and error must never look like brand colours. And the brand values live in a plain constants file separate from the theme, because theme code only runs in the browser and server-rendered pages needed the same values. That second one cost me half an hour before I understood it, which is exactly the kind of thing that ends up in the gotchas file.

My job

Use the host's design system, and treat branding as a layer rather than a replacement.

The AI's job

Wire the theme, build the shared header and logo as self-contained components, and apply them consistently to every screen.

10Step 3: Build the picker on its own

Our XM Cloud instance was empty when I started. Waiting for it would have wasted a week, so I built the picker as a normal web app on its own URL, with no Sitecore in the picture at all.

That turned out to be one of the better calls. I could develop with hot reload instead of inside an iframe in someone else's app. I could demo on day two. And it became a real feature, because people who never open Sitecore can use it to browse the DAM and copy a public link.

What went into it, in the order authors care about:

Search that keeps up

Typing is debounced, there is a two-character minimum, and paging is real.

The real folder tree

Loaded a level at a time, with expand arrows only where there are children.

Type filters

Image, video, document, audio, and a field can lock them down.

Asset detail

Large preview, metadata, keywords, size choice, and a link back into the DAM.

The alt text gate

You cannot insert an image without alt text. The only moment anyone will type it.

Expiry and link health

If a licence is running out or a link has gone bad, the author sees it. Never silently.

Keyboard navigation and a focus trap went in with the layout, not afterwards. Retrofitting accessibility into a grid of cards is miserable, and the accessibility checks run in the tests so it stays fixed.

This is also where the third architectural isolation from section 3 gets built. The picker never talks to Sitecore directly. It talks to a small interface, and I wrote two implementations of it: one that writes to a Sitecore field, one that copies to the clipboard.

export interface SelectorHost {
  readonly kind: "sitecore" | "standalone" | "rte";
  readonly capabilities: { canWriteField: boolean; canClose: boolean };

  readValue(): Promise<string>;

  // sitecore   -> write to the field, wait, close
  // standalone -> copy the JSON, stay open
  // rte        -> hand back an <img> tag
  // Must throw and change nothing if it fails.
  commitSelection(json: string): Promise<void>;

  writeValue?(json: string): Promise<void>;  // save without closing
  cancel(): Promise<void>;
}

Three to five days to put that in before the UI work, against about two weeks to retrofit it. It has paid back repeatedly: the same picker now serves the field inside Pages, the standalone browser, the rich text editor, the clipboard helper and the standalone field editor. Five surfaces, one picker, no forks.

The picker search · folders · grid · detail · alt gate · builds the JSON only ever talks to one small interface read the value · commit a selection · save · cancel Inside Sitecore the field in Pages write, wait, close built first On its own URL no Sitecore involved copy the JSON or the link needs a personal DAM login Inside rich text images in body copy returns tagged image markup added later, no changes needed
One picker, three homes. The third arrived long after the interface was frozen and needed no changes to the picker itself. That is the only real proof an abstraction earned its keep.
The asset picker open on a field inside XM Cloud Pages
The picker, open on a field called LeftImage. The DAM folder tree on the left, search across 4,717 assets, grid or list view, and results as cards. Behind the modal you can see the Pages content panel with both DAM fields on the component and their Open app buttons.
The asset detail view with metadata and keywords
Open a card and you get the detail view. The asset ID and GUID, format, dimensions, version, status, created and modified dates, and the DAM’s own keywords. Insert asset is the only route by which a value ever reaches the field.

11Step 4: Connect the real DAM API

The server side is small and boring on purpose. Every route wraps its work in one shared handler that assigns a correlation ID, applies rate limiting, and maps errors onto a short list of categories, so no raw exception or upstream payload ever reaches the browser. The Sitecore routes additionally require an admin key.

Route What it does
/searchSearch proxy: query, type filter, folder scope, paging. Returns clean data, never the raw response.
/asset/[id]One asset's detail.
/asset/[id]/resolve-urlThe permanent public URL. The isolated seam from section 3.
/foldersThe folder tree, one level at a time.
/bootstrapNon-secret facts the UI needs, like the portal URL for deep links.
/healthReachability plus a categorised diagnosis. A deep mode also runs a real search.
/link-checkChecks a stored URL still works. Restricted to known DAM hosts so it cannot be used to probe your network.
/sitecore/*Item read and write, tree browse, and the usage report, through the Authoring API. Admin key required.

Now the part that matters if you are working with an AI agent. The documentation for an API and the actual behaviour of your tenant are two different things, and a language model has only ever read the documentation. So the first thing built was not a feature. It was a script that hits the live API with real credentials and prints exactly what comes back. In one afternoon it found four problems that would each have cost days later.

My job

Supply the credentials, run the script against the live tenant, and read the output properly instead of assuming.

The AI's job

Write the probe script, then turn each finding into working client code, a note in the knowledge file, and a test.

Trap 1: HTTP 200 does not mean it worked

Every response is wrapped: { result: { code, message }, ... }. Code 0 means success. Code 1 means "you are not logged in", and it arrives with a cheerful HTTP 200. The fetch wrapper checks the envelope, not the status line.

Trap 2: the token that authenticates nothing

I got a valid bearer token from the token endpoint. The REST endpoints rejected it. They want a session cookie from a separate integration login, plus load balancer cookies that rotate on every response and have to be carried forward or the session dies mid-browse. I found this by reading the JavaScript on the vendor's own Swagger login page.

Trap 3: case-sensitive IDs

One endpoint hands you a folder ID in uppercase. The search index stores IDs in lowercase and matches case-sensitively. Filter with the ID you were just given and you get zero results, no error, forever. Every identifier is now lowercased at the boundary, with a comment saying why, because it looks redundant until you know.

Trap 4: the endpoint that looks right and is not

My first folder tree showed one empty folder while the DAM portal showed hundreds. I was listing folder-shaped assets from the search index instead of the actual folder structure, which lives behind a completely different endpoint. Two more surprises on the same trail: a third endpoint looks perfect for browsing but returns no asset IDs, so it cannot build my JSON at all, and a folder-scoped search returns an empty list with a correct total unless you also send a search term. Browsing now sends a wildcard.

Two habits from this step. Never ask an API for all fields. Name the ones you want, and treat every one as optional, because unknown field names are silently dropped rather than rejected. And write the live probe script on day one. With an AI agent this matters more than usual: it is the fastest way to replace what the model assumes with what your tenant actually does.

12Step 5: Put it inside Sitecore

With the picker working, the API mapped and the field architecture already decided, embedding was mostly configuration. Register the app, point the extension points at the deployed URL, create the plugin field, set the standard value, open a page. My app appeared inside the field. That was a good afternoon.

Then I hit the thing I would warn every reader about.

To save a value you call setValue() and then closeApp(). The catch is that setValue resolves when the host acknowledges the message, which can happen before the value is stored. Close on that acknowledgement and you have built a connector that occasionally loses an author's work: rarely enough that nobody can reproduce it, often enough that people stop trusting it.

The fix is boring. Acknowledge, wait, then close. It lives in exactly one file, with a comment explaining why the number is there so nobody tidies it away later.

// The only place in the app that reads or writes a field value.
// setValue resolves on acknowledgement, which can arrive before the
// value is actually stored. Do not remove the guard delay.
export const SET_VALUE_GUARD_MS = 500;

export async function writeFieldValue(client: ClientSDK, json: string) {
  await client.setValue(json, true);
  await new Promise((r) => setTimeout(r, SET_VALUE_GUARD_MS));
}

// Save and close. Throws WITHOUT closing if the save fails, so the field
// is never left half-written.
export async function writeFieldValueAndClose(client: ClientSDK, json: string) {
  await writeFieldValue(client, json);
  await client.closeApp();
}

Notice what happens on failure. It throws and does not close. The modal stays open, the author sees an error they can act on, and whatever was in the field before is untouched. That behaviour is the difference between a connector people trust and one they work around.

Author in Pages My app in the field My API routes DAM API opens the field read what is stored, decide what to show search for "hero" session cookie, named fields only wrapped response, check the result code clean data for the UI, no secrets picks an asset, types alt text build the JSON, validate it, check the alt text save the value acknowledged wait 500 ms. Acknowledged is not the same as saved. close the app If anything above fails stop, keep the modal open, leave the old value alone, show the author a real error
One insert, end to end. The two purple arrows are the only moments the app writes to Sitecore. The red step is what makes them safe.
A field that already holds an asset, shown as a card with alt text and stored JSON
Reopening a field that already holds an asset. The stored JSON has become a card: thumbnail, the technical line, the asset ID, a verified pill from the live link check, an alt text box that saves without closing the modal, Replace, Preview and Remove, and the raw JSON one click away.

13Step 6: Test as you go

Tests are where AI-led development either works or falls apart. Generated code looks convincing, so you need something other than your own reading to tell you whether it is right. Tests were written with each feature, never afterwards, and the rule was simple: a feature without tests was not finished.

Layer What it protects
UnitThe pure logic. Building and reading the JSON, the API mapping, the sanitiser, the image markup, error handling.
ComponentThe picker and the renderers, with accessibility checks on every screen.
JourneyBrowser tests of the real app: search to insert with the alt text gate, the admin screen, and what happens when the app is opened outside Sitecore.
SmokeScripts that hit the live DAM and my own API. These are what catch the vendor changing something.

That is currently 87 unit and component tests across 14 files, three browser journeys, and a handful of scripts. The whole unit suite runs in about two seconds, which is the only reason anyone actually runs it. Every gate runs in CI in the same order every time: lint, type check, test, build.

14Adding images inside rich text

Then came the requirement I should have seen coming. Authors do not only put images in image fields. They put them inside body copy. This part taught me the most, so here are all four attempts.

Attempt 1: add a button to the editor toolbar. The obvious answer. I spent two days on it and the answer was no. The Marketplace SDK has no rich text extension point at all. The classic editor profiles can be edited through the API, but a custom button needs server-side JavaScript in the web root, and XM Cloud does not allow custom server code. You can create the button. It will do nothing when clicked.

Attempt 2: use the clipboard. Author picks in my full screen app, I build the markup and copy it, author pastes into the editor. This worked, and it proved something I needed to know: the editor kept all my custom data attributes through a paste, so the governance survives. But two clicks and a paste is a workaround, not a feature.

Attempt 3: reuse Sitecore's own DAM hook. There is a separate integration that loads an external search page in an iframe and inserts whatever it sends back. I chased it for a while before establishing that the Pages editor's image button goes to the Media Library instead, and that reusing the mechanism had licensing implications I did not want.

Attempt 4: stop asking. If I cannot put my button in their editor, I ship my own editor. The custom field is already an extension point, so I added a rich text mode to it, switched on by one setting from section 5. It has an editing surface, a toolbar matching Sitecore's own structural set, a source view, and one extra button that opens the same picker through the same interface from step 3.

Two storage decisions made attempt 4 survivable. First, the field stores plain sanitised HTML, exactly like Sitecore's own rich text field, not a JSON wrapper. My first version used a wrapper and I threw it away, because it blocked source-level editing and would have made migrating old HTML painful. Now legacy HTML pastes straight in and the source view edits it raw.

Second, inserted images carry their own identity:

<img src="https://cdn.example-dam.net/.../hero_1920.jpg"
     alt="Hiker looking over a spring mountain valley"
     width="1920" height="1080" loading="lazy"
     data-bw-provider="brandworkz"
     data-bw-asset-guid="d76e2bc0-7933-4cc2-b4b0-3b8183279758"
     data-bw-rendition="web-large-1920"
     data-bw-schema="rte-1" />

Those attributes are the whole governance story for inline images. Browsers ignore them, and the usage report in section 17 finds every DAM image in the content tree by scanning for them. They also version separately from the field JSON, because markup and data change for different reasons.

On sanitising. I loosened the allowed tag list to let through headings, tables, spans, divs, classes, inline styles, figures and code blocks, because migrated content has all of that and mangling it helps nobody. Scripts, iframes, event handlers and javascript links are still stripped. I also left out the font family, size and colour pickers on purpose. Inline font styling defeats the design system, and once it is in the toolbar you never get it back out.
The connector rich text editor with an Insert Brandworkz image button
My own editor, inside their field. The structural toolbar, a Source view, and the one button that exists nowhere else in Sitecore: Insert Brandworkz image, which opens the same picker as every other surface. The image in the body is a DAM asset carrying the data attributes above.

15Showing assets on the live website

A connector that only writes JSON into a field is half a product. Somebody still has to turn that JSON into an image on the page, and if you leave that to each front-end team you have shipped a bug generator. So the connector also ships a small package for the website: a reader and a set of React components.

import { parseBrandworkzField, BrandworkzImage } from "@brandworkz/sitecore-renderers";

export function Hero({ fields }) {
  const parsed = parseBrandworkzField(fields.heroAsset?.value);

  // One function, every stored shape, never throws:
  //   { kind: "asset",    assets: [...] }
  //   { kind: "richtext", html: "..."   }
  //   { kind: "empty" }
  if (parsed.kind !== "asset") return null;

  return <BrandworkzImage asset={parsed.assets[0]} sizes="100vw" />;
}

Three things make it worth packaging instead of copy-pasting. It never throws, so a DAM problem cannot take down a page. It has no dependencies beyond React, so it runs anywhere. And it stays tolerant on purpose: it reads the current format, the multiple-asset envelope, plain rich text HTML, and an older wrapper format I stopped writing months ago. Backward compatibility is the package's job, not the caller's.

Notice what is not happening here. The page renders from the URL stored in the field. There is no call to the DAM at render time, which means the website keeps working normally even if the DAM API is having a bad afternoon.

16Finding where each asset is used

This is the one honest weakness of storing references instead of copies. Without media items, Sitecore's built-in "where used" knows nothing about DAM assets. The brand and legal teams care far more about that than about how nice the picker looks.

So I built the answer directly. A report that walks the content tree through the Authoring API, finds every field holding one of my values and every inline image carrying my data attributes, then groups it by asset:

  • thumbnail and title, so it reads as pictures rather than a list of IDs;
  • every item using it, with path, item ID and a direct edit link;
  • link health, from a server-side check of the stored URL;
  • expiry status from the stored metadata, as a warning rather than a block.

Three implementation notes. The walk is cached for a minute and returns a "truncated" flag rather than falling over on a big tree. Writes through the same client are limited to paths under the content root and to fields that are genuinely mine, so even if that endpoint were abused it could only touch my own fields. And link checks run in the background, because a slow CDN should make the report slower, not broken.

The usage and governance report listing each asset and the items that use it
The report that answers the brand team’s question, without a single media item in Sitecore. Three assets found across 534 items scanned. Each row gives the asset, whether its link still resolves, how many places use it, and every item and field that references it, with a CSV export at the bottom. The connection test and the rest of the administrator’s settings sit on the other tab: portal URL, sign-in mode, default image sizes, and whether DAM metadata gets stored in the field. Credentials are masked and write-only there, and never leave the server.

17Six limits you should know about

Each of these cost me at least a day to establish. None is a bug. They are consequences of running on a SaaS platform, which on the whole is a trade I am happy with. An AI agent will not warn you about any of them, because they are not in the documentation. Design around them and you will move faster than I did.

The limit What to do instead
No way to add a rich text toolbar button
no extension point, and no custom server code allowed to register one
Ship your own rich text field as a custom field.
Nothing renders in the classic Content Editor
these fields show raw JSON there; Bynder's connector has exactly the same limitation
Point authors at the Pages content tab. I also built a standalone field editor over the Authoring API for people who work that way.
The field Source cannot carry settings Put them in standard values and decide what to render from the shape of the value (section 5).
The SDK never settles outside a Sitecore host
open the app directly and it neither succeeds nor fails, it just waits forever
Race it against your own timeout, and treat the timeout as a switch into standalone mode rather than an error screen.
Components are not addable until the website knows them Treat every connector component as two deployments that have to land together (section 5).
Your app's sign-in client may not do server-to-server
the Marketplace app client is interactive only
Get a separate automation client provisioned early, before the sprint that needs it.
And one on the DAM side. Not every DAM licence gives you permanent, public, per-size URLs, and the mechanism differs per tenant. That is exactly why it is one of the two isolated seams in section 3. Mine fails loudly rather than storing a link it cannot guarantee, because a broken URL in ten thousand items is far more expensive than a blocked insert.

18Build this for your own DAM in a week

Here is the part I did not expect when I started. Almost none of this is specific to our DAM.

Look at what the connector is made of. A picker built from wireframes. A data format. A server layer that proxies an API and holds the secrets. An adapter that writes to a Sitecore field. Base templates. A package that renders on the website. A usage report. Tests around all of it. Exactly one piece knows anything about Brandworkz: the module that talks to the DAM.

First, check your DAM can support it

Before you plan anything, confirm the API gives you these five things. If it does, everything in this article applies. If the last one is missing, stop and talk to your vendor before writing code, because it is the one thing you cannot work around.

You need Why
Server-side authenticationA machine credential your backend can use. If the only way in is an interactive browser login, the whole proxy model gets much harder.
Search with pagingThe picker is mostly search. You also want a type filter, or you will have to filter client-side and the counts will lie.
A folder or category listingAuthors browse as well as search, and locking a field to a folder is one of the most requested settings.
Asset detail by IDFor the preview, the metadata, and refreshing a stored reference later. Check the ID you get from search is the same one detail accepts.
A permanent public URLThe whole model rests on this. It must be stable, public, and ideally available per size. Confirm it is covered by your licence, not just technically possible.

What you reuse, and what you rewrite

Stays the same

The architecture. The wireframes. The design system choice. The field format. The picker. The host interface. The save-and-wait discipline. The Sitecore field type, the Source rule, the settings-in-standard-values transport, the base templates. The renderers. The usage report. The tests. The whole AI workflow.

Changes per DAM

One module: how you authenticate, search, list folders, fetch asset detail, and get a public URL. Plus the mapper that turns their response into your data format, and the provider value in the JSON.

The week

This is the plan I would follow, knowing what I know now. It assumes one engineer with an AI agent, credentials in hand, and Cloud Portal access already sorted.

Day 0, half a day — set the rules

Create the repo with the folder discipline from section 7. Write the rules file. Start the decision log. Register the Marketplace app and note the App ID. Get the DAM credentials into environment variables. No product code today.

Day 1 — probe the API, then write the format

Write the probe script and run it against the live tenant. Record the auth mechanism, the response shape, paging, ID casing, and which fields actually come back. Then write your field format and get it reviewed by whoever owns content migration. Both artefacts land in the knowledge folder.

Day 2 — the server layer

The DAM client, the shared route wrapper, and the search, folders, detail and health routes. Mappers that turn their responses into your DTOs, with everything optional. Unit tests for the format and the mapping. By the end of the day the smoke script should return real assets through your own API.

Day 3 — the picker, standalone

Search, folder tree, results grid, detail view, alt text gate, on top of the host interface with the clipboard implementation. Keyboard support and accessibility checks as you go. Demo it to someone who will use it, on its own URL, with no Sitecore involved.

Day 4 — inside Sitecore

Point the extension points at your deployment. Add the Sitecore host implementation with the save, wait, close discipline. Create one plugin field by hand and prove the round trip: open, save, close, reopen, read it back. Then script the base templates so nobody has to repeat that by hand. This is the highest-risk day, which is why it comes before the polish.

Day 5 — the website half and the handover

The renderer package and its parser. One browser test for the main journey. The admin screen with a connection test. A short deploy runbook. Whatever you learned this week, written down properly, because that file is what makes week two fast.

What will actually slow you down

Not the code. In my experience it is these four, so start them in parallel on day zero:

  • Access. Cloud Portal permissions, an App ID, a DAM service account, and an automation client for server-to-server calls. Every one of these needs somebody else to press a button.
  • Public URL licensing. Confirm in writing that your DAM licence covers permanent public delivery links. This is the single question most likely to stop the project.
  • Vendor auth surprises. Budget a day for whatever your DAM does that the documentation does not mention. There will be something.
  • The website deployment. If you ship components, you depend on the site team's release cycle. Ask early.

The AI workflow is what makes a week realistic rather than optimistic. The expensive part of a connector was never the idea, it was the volume of careful, boring work: screens, states, mappers, validation, tests, templates, documentation. That is exactly the work an agent is good at when you give it a clear architecture, deterministic checks, and a memory of what it has already learned. The part that stays human is the judgement in sections 3 to 5. That did not get automated, and I do not think it is about to be.

If you remember six things

  1. Decide the architecture before you write code. Who talks to whom, what gets stored, and which parts you expect to change.
  2. Write the field format first, and get it signed off by whoever will migrate content into it.
  3. Design how the fields get created in Sitecore, not just how the picker looks. Base templates are what make it usable.
  4. Write a live probe script on day one. Replace what the model assumes with what your tenant does.
  5. Put an interface in front of anything you do not control, and build the standalone version. Both are nearly free early and expensive later.
  6. Write down every failure the day it happens. That file is what makes the next session faster than the last one.

There is nothing clever in this connector. It is an app, a proxy, a schema, an interface with three implementations, and some React components. The hard part was never the technology. It was deciding the architecture properly, refusing the shortcuts, and keeping a process tight enough that moving fast did not mean moving carelessly.

About this write-up

Everything here comes from a working connector: a custom, organisation-scoped Marketplace app that puts a commercial DAM inside XM Cloud Pages. Picker, rich text mode, standalone browsing, base templates, a renderer package for the website, an admin screen and a usage report. Tenant names, IDs and internal identifiers have been changed. The architecture, the traps and the decisions are exactly as they happened.

If you are building something similar, or you have hit a limit I have not listed, I would like to hear about it. The Marketplace extension model is new enough that our shared notes are still worth more than the documentation.

Photograph of Ashish Kapoor

About the author

Ashish Kapoor

Global Director of Marketing Technology | Chief Technology Advisor | Architecting the Future with SaaS MACH & Agentic AI | 2x Sitecore Ambassador MVP

  • 21+ years in enterprise product architecture
  • Sitecore MVP Ambassador (2023, 2024)
  • Global digital delivery across 40+ countries
  • 100+ AI agents shipped in production
  • $2M+ MarTech rationalisation savings
Read the full bio