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.
The integration surface
Section titled “The integration surface”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.
The pluggable integration framework
Section titled “The pluggable integration framework”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/PUTcalls 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.
Worked example: the Eloqua integration
Section titled “Worked example: the Eloqua integration”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.
What it does
Section titled “What it does”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.
What to mirror
Section titled “What to mirror”For your own integration, copy the Eloqua layout:
- Keep credentials and endpoint configuration in repository settings, keyed by
the
:config_idroute parameter, so one integration serves many repositories with different accounts. - Expose a
:config_id/testroute 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.
Practical guidance
Section titled “Practical guidance”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.
Consuming webhooks reliably
Section titled “Consuming webhooks reliably”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 itemidfrom 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.
Related
Section titled “Related”- Architecture overview
- Authentication
- Webhooks and workflow
- REST API reference — see the REST API Reference section in the sidebar