Skip to content

Building integrations

CM Box is designed to sit inside a larger ecosystem — marketing platforms, delivery front ends, sync jobs, and automation tools. This page explains the surfaces available to you as an integrator, how the built-in integration framework routes requests to named integrations, and what the Eloqua integration that ships with CM Box looks like as a reference implementation.

You can connect an external system to CM Box through four surfaces:

Surface Direction Use it for
REST API Inbound Content CRUD, publishing, administration — see the REST API Reference section in the sidebar
GraphQL Inbound Reading content per repository at /api/graphql/:repo
Webhooks Outbound Reacting to content events (created, published, deleted, and so on) in your own systems
Integration endpoints Inbound Calling a named integration handler at /api/integration/[name]/...

Programmatic callers authenticate as a service account using the OAuth 2.0 client credentials grant: POST your client_id and client_secret to /oauth2/token and receive a Bearer token valid for one hour. See Authentication for the full flow and Webhooks and workflow for the outbound event surface.

CM Box exposes a generic passthrough route:

GET | POST | PUT /api/integration/[name]/[...path]

The route (src/pages/api/integration/[name]/[...path].js in the platform source) holds a map of integration names to handler modules. When a request arrives, it looks up the handler for [name] and delegates the whole request context to it; if no handler is registered under that name, the route returns 404 Not Found.

Each handler declares a route table per HTTP method — a list of sub-path patterns paired with client functions:

this.routes = {
GET: [
{ path: ":config_id/test", handlerFunc: MyClient.testConnection },
{ path: ":config_id/things", handlerFunc: MyClient.listThings }
],
POST: [
{ path: ":config_id/thing/create", handlerFunc: MyClient.createThing }
]
}

A shared utility (integration_handler_util.js) matches the request’s remaining path against those patterns, extracts the named parameters (such as :config_id), invokes the matching client function with (routeParams, context), and serializes its return value as a JSON response. Unmatched sub-paths also return 404.

The pieces you would mirror to add an integration are therefore:

  • A client — a class whose methods talk to the external system. Each method receives the matched route parameters and the request context.
  • A handler — a thin class that declares the route tables and forwards GET/POST/PUT calls to the shared route matcher.
  • Registration — an entry in the route’s handler map keyed by the integration name, which becomes the [name] URL segment.

Handler registration currently requires a change to the platform source — there is no runtime plugin loader — so treat this framework as the pattern CM Box itself uses for built-in integrations, and as the shape a custom integration takes when delivered as part of your CM Box build.

CM Box ships with an example integration for Oracle Eloqua, a marketing automation platform. It lets the site builder export a rendered page to Eloqua as an email asset. It is enabled per repository with the Oracle Eloqua Integration (BETA) feature toggle, and configured on the same repository settings page with three values: Company Name, User Name, and Password.

The Eloqua client stores no state of its own. Every route takes a :config_id — the repository whose settings hold the Eloqua credentials — loads that repository’s eloqua_settings, and calls the Eloqua REST API (version 2.0) with HTTP basic authentication as companyName\userName. Before its first asset call, it resolves the tenant’s base URL from Eloqua’s login.eloqua.com/id endpoint and caches it for one hour.

The registered routes:

Method Path What it does
GET /api/integration/eloqua/:config_id/test Verifies credentials by calling Eloqua’s ID endpoint; also resolves and caches the tenant base URL
GET /api/integration/eloqua/:config_id/emails Lists email assets from Eloqua
GET /api/integration/eloqua/:config_id/landingPages Lists landing page assets from Eloqua
POST /api/integration/eloqua/:config_id/email/create Creates an Eloqua email asset from name, subject, and html in the request body
POST /api/integration/eloqua/:config_id/email/export Fetches the HTML from a CM Box preview url in the request body, creates an Eloqua email from it, and returns externalId, previewLink, and directLink

The export route is the interesting one: rather than requiring the caller to supply HTML, it takes the URL of a rendered CM Box page, fetches that page’s HTML itself, and pushes the result into Eloqua. The site builder’s export drawer uses exactly this route, and the returned directLink deep-links to the new asset inside Eloqua. A few routes (updating emails and creating or updating landing pages) are declared but not yet implemented.

For your own integration, copy the Eloqua layout:

  • Keep credentials and endpoint configuration in repository settings, keyed by the :config_id route parameter, so one integration serves many repositories with different accounts.
  • Expose a :config_id/test route so administrators can verify a connection before relying on it.
  • Put all remote-system logic in the client; keep the handler to a declarative route table.
  • Return plain objects from client methods — the framework handles JSON serialization.

Authenticate with a minimal service account

Section titled “Authenticate with a minimal service account”

Create a dedicated service account per integration rather than reusing a person’s credentials. Effective permissions come from the roles assigned to the account, and roles compose permissions from bundles — named permission sets. CM Box ships defaults that map well to integration jobs: give a read-only sync the Viewer bundle, a content importer Content Contributor (create and edit, no publish or delete), and reserve Content Editor (adds delete and publish) for integrations that must go live unattended. Grant the smallest bundle that covers the calls you make; you can always widen it later. See Authentication.

Webhook deliveries are dispatched through a background queue: each matching event produces a single HTTP POST to your endpoint, and the delivery worker does not retry failed deliveries. Plan for that on your side:

  • Treat each delivery as at-most-once. If your system must not miss changes, pair webhooks with periodic reconciliation through the REST or GraphQL API.
  • The same logical change can legitimately fire more than once (for example, repeated saves each emit ITEM_UPDATED), so make your consumer idempotent — key processing on the event name plus the item id from the payload.
  • Respond quickly and do heavy work asynchronously; the payload carries the event name and, at the default payload size, the full item.

See Webhooks and workflow for events, payload sizes, and custom payload templates.