Working with AEM's HTTP API to create and update content

For developers building headless experiences on top of Adobe Experience Manager, the HTTP API is often the cleanest path between an external system and the JCR tree. Instead of wiring up bespoke servlets or running everything through a custom OSGi bundle, you can reach a content node directly, shape it with JSON, and let the repository handle persistence. Teams across Sydney and Melbourne have leaned on this approach for years, particularly for marketing landing pages, product catalogues, and integrations between Adobe tooling and mobile apps.

What makes the HTTP surface worth understanding is the consistency it offers. The same verbs used against a REST endpoint in any other stack — POST, PUT, GET, DELETE — map cleanly onto Create, Read, Update and Delete against /content, /content/dam and other paths. Once the node types and payloads click, automating bulk updates becomes a matter of crafting the right request rather than building a custom Java service.

This walkthrough focuses on the practical mechanics: how to authenticate, what shape JSON payloads take, how the repository merges updates, and where things tend to go wrong. It draws on session material recorded at the CIRCUIT conference in Chicago and pairs the theory with examples tuned for teams running AEM 6.x in production.

Understanding the AEM HTTP API surface

Which endpoint family to call is the first decision. AEM exposes its Sling-based API under paths like /api/assets, /content/<site> and /etc. There is a specialised Sites API for managing pages and a dedicated Assets HTTP API for the digital asset manager. Most developer traffic lands on the Sites API first, because page nodes are the everyday canvas for marketers and editors who also work in the Touch UI.

Each endpoint answers to a slightly different payload. When creating a new page, the body usually carries a cq:template, cq:allowedTemplates, jcr:primaryType, jcr:title and sling:resourceType. The repository interprets these and creates the node tree beneath the parent. For assets, the workflow is closer to a multipart upload than a plain JSON POST.

One subtlety worth flagging: the dispatcher layer in front of author instances often blocks POST and PUT by default. Engineers in Australian financial services, where corporate load balancers are the norm, frequently discover that a request works perfectly on a local author instance but bounces off the dispatcher because the filter rules reject anything that is not a GET. Verify the dispatcher farm configuration before spending an afternoon debugging an empty response.

Authentication and authorisation basics

AEM does not treat its HTTP API the same way as a public REST service. Out of the box, calls typically go through cookie-based authentication on the author instance, using the login-token cookie issued by a POST against /libs/granite/core/content/login. That mechanism works well for trusted server-to-server flows, where the calling application logs in once, caches the cookie, and reuses it until expiry.

For unattended integrations, many teams prefer technical user accounts with a service token, or they generate a long-lived authentication token via the Granite Token Authentication handler. Adobe encourages this pattern because it keeps audit logs meaningful and avoids sharing personal credentials between machines.

Authorisation matters as much as authentication. A user with permission to read /content/geometrixx cannot necessarily write to /content/dam, and a technical user with content-editor rights still cannot create templates unless the underlying ACLs grant jcr:privilege=jcr:write on the relevant path. Before sending traffic, dump the effective permissions for the user and confirm both path and privilege. That small investment pays for itself the first time a request fails with a cryptic 403.

Creating content through POST requests

To create a brand new node, POST a JSON body to the target path. Call /content/<parent-path> with the new node name embedded as a suffix, supply jcr:primaryType in the body, and any additional properties the template requires. AEM responds with a 201 status and a Location header pointing to the freshly created resource.

A minimal payload for a page under /content/mysite/en looks like this:

{
  "jcr:primaryType": "cq:Page",
  "jcr:content": {
    "jcr:primaryType": "cq:PageContent",
    "jcr:title": "Summer campaign landing page",
    "sling:resourceType": "mysite/components/page"
  }
}

The structure mirrors the JCR view you would see in CRXDE Lite. The server does not require a separate "create" verb; POSTing to a parent path with the right node type is enough. Teams wrapping this in a Python or Node script skip ceremony and stream JSON straight over HTTPS.

A Queensland-based tourism portal automated roughly 4,000 page creates this way during a migration off a legacy CMS. The trick that kept the run reliable was pagination on the source side and a simple circuit-breaker on the destination, since the author instance occasionally needed to flush bundles before accepting new writes. Tools like the circuit-app companion were handy for sanity-checking requests during the rehearsal window.

Updating existing nodes with PUT

Updating follows a similar pattern but speaks to the resource directly. PUT to /content/mysite/en/about — without a suffix — replaces properties, and the body must include every required field, otherwise the missing ones fall back to defaults or vanish entirely. The safer alternative for most cases is PATCH or the proprietary Sling POST servlet, which merges properties rather than blowing them away.

The merge approach is friendlier. POST a partial JSON document to the same path and only the supplied properties change. Other values stay intact, including jcr:created and jcr:createdBy, which the repository guards against overwrite. Property types — single-value versus multi-value, date formats, locale variants — must match the existing definition, or AEM responds with a 400.

Bulk updates across many nodes turn into a loop with a polite delay between calls. Adobe recommends a few seconds of cushion to give the observation queue time to drain, especially on an author instance also serving editors in Sydney and Brisbane office hours. Running it overnight on Australian Eastern time, when traffic dips after 19:00 AEST, is often the difference between a clean run and a pile of partially-applied edits.

Containerising the local development environment

Running a full AEM author instance on a laptop is heavier than most other stacks. Quickstart jars, a JRE, an instance folder, a content package, and a dispatcher web server all need to coexist. Wrapping the lot in Docker reduces the setup to a single docker compose up, and that simplification pays off the moment a new developer joins the team.

The official Adobe-published Docker images cover author, publish and dispatcher tiers. Most Australian agencies start from those, then layer their project-specific content packages and a few cron-style hooks to seed sample content. The pattern works as well on an Apple Silicon Mac in Melbourne as it does on an Intel workstation in a Parramatta office.

For a concrete walkthrough of wiring Quickstart into containers — volume mounts, licensing gotchas and all — the containerising your development environment post goes step by step. It suits anyone who has wrestled with crx-quickstart ownership quirks and would rather not solve them again from scratch.

Practical patterns and common pitfalls

Three patterns show up repeatedly in teams that run this API well. Treat every endpoint call as a small contract: document the path, the verb, the expected status, and the JSON shape. Once the contract exists, it can be wrapped in a client library and re-used everywhere. Always run against an author instance with a non-production technical user, even for "just a quick test", because production authors carry audit and replication responsibilities you do not want to disturb. Cache aggressively on the read side, since the HTTP API is generous with caching headers once content is published, and respecting them removes a surprising amount of load from the publish tier.

Common mistakes cluster around property types and dispatcher rules. A date stored as a Calendar becomes a string the moment you round-trip it, so convert before comparison. A multi-value tag array silently drops empty entries, surprising people who rely on array length in the consumer. The dispatcher filter is the most frequent cause of "it works locally, fails in staging" tickets — worth eyeballing every time you change anything under /content.

Lean on the recorded CIRCUIT sessions for a deeper dive. The Chicago recordings cover Sightly, microservices, and the broader Adobe ecosystem with examples drawn from real delivery teams.

Time to put this into practice. Pull the session recordings, spin up a Docker-based author instance, and try posting a single page with curl. The fastest way to internalise the API is the way Australian teams have always picked it up: ship something small, point it at a local author, and iterate until the routine feels familiar.