Member API
How to build your own member portal (a single-page app or a headless site's account area) on the member API at /api/member/v1: what it requires, signing members in through the engine's sign-in screens, the same-origin session cookie and CSRF token, a worked request sequence, the form description, errors, rate limits and the OpenAPI contract.
engine-dmo
The member API is the JSON surface behind the member portal: everything a business’s people do in the reference portal (edit their organisation and listings, upload photos, invite colleagues, read their member report) is available to a portal you build with another toolchain. It serves the same data under the same rules as the reference portal: the tier’s editable fields, the approval workflow and the partner role permissions are the engine’s, not the client’s.
The full list of operations, with the permission each needs, is in Portal screens and actions. This page is about calling it.
Requirements
| Setting | Value |
|---|---|
server.api.member.enabled in engine.yaml | true (the default) |
portal.mode | api or both (the default). api serves only the member API and the sign-in screens; both serves the reference portal too. |
| Origin | Your portal must be served from the same origin as the API, so the session cookie reaches it. On a headless site, proxy /api/member/v1/ and the member sign-in paths under the frontend’s domain. |
Signing in
The member API does not sign anyone in. Members sign in through the engine’s own sign-in screens under the portal path (server.portal_path, /members by default): <portal_path>/login, with the methods auth.member enables (magic link, passkeys, password). These screens, and the pages members reach from links in mail, are always rendered by the engine and restyled in templates/portal/.
Sign-in sets the session cookie __Host-mrd_member: secure, host-only and sent only to the same origin. Every API call carries it automatically when your portal runs on that origin and sends credentials.
A request sequence
// 1. Who is signed in, which organisations they act for, and the CSRF token.
const me = await fetch("/api/member/v1/me", { credentials: "same-origin" }).then((r) => r.json());
// me.person, me.organisations[] ({id, name, role, partner_role, permissions, sites, is_primary}),
// me.capabilities, me.csrf
// 2. The organisation's listings.
const org = me.organisations[0];
const listings = await fetch(`/api/member/v1/organisations/${org.id}/listings`, { credentials: "same-origin" }).then((r) => r.json());
// 3. The form this person may fill for one listing at their tier.
const form = await fetch(`/api/member/v1/listings/${listingId}/form`, { credentials: "same-origin" }).then((r) => r.json());
// 4. Save field values. Unsafe methods carry the CSRF token.
const res = await fetch(`/api/member/v1/listings/${listingId}`, {
method: "PATCH",
credentials: "same-origin",
headers: { "Content-Type": "application/json", "X-CSRF-Token": me.csrf },
body: JSON.stringify({ summary: "Riverside pub with a garden", website: "https://redlion.example" }),
});
// 200: {saved, discarded, status, approval}; 422: a problem with field_errors | Rule | |
|---|---|
| Base path | /api/member/v1 on the site’s host. |
| Authentication | The __Host-mrd_member session cookie; a request without a valid session is refused. |
| Unsafe requests | POST, PATCH and DELETE carry the CSRF token from GET /me in X-CSRF-Token. |
| Bodies | JSON, except POST /assets, which is multipart/form-data (file, organisation, licence_accepted required; alt unless decorative; optional caption, listing, field). |
| Language | Listing values are read and written in the site’s source locale. |
The form description
GET /listings/{id}/form is the contract a portal draws its listing editor from: the schema’s fields this person may edit at their organisation’s tier, in the locale.
| Key | Meaning |
|---|---|
locale | The locale the form edits. |
groups | {key, label, fields}: the fields in the editor’s groups. |
fields[] | Each {name, type, label, help, required, required_when, visible_when, value, options, max, multiple, constraints, params, fields, blocks, asset, link}: type is the field type, value the working value, options for a select, fields and blocks for containers, asset and link the rules of those field types. visible_when and required_when are JSON predicate trees, never strings. |
validation | Cross-field rules: {rule, fields, message, severity}. |
approval | {mode, expected_turnaround_hours, review_window_hours, always_review}: whether a save goes to staff review or is published at once with a review window. |
engine-dmo api member form --listing <id> --person <id> --project . prints the description a given member would receive.
A PATCH /listings/{id} body is field values by field name. Fields the tier does not permit are discarded and named in discarded, never an error. The answer is {saved, discarded, status, approval}; status is {live, last_live_at, pending, returned, approval}, also served by GET /listings/{id}/status.
Errors
Every failure is an RFC 9457 problem detail (type, title, status, detail). A 422 carries field_errors, each {path, code, message}, with structured paths such as stages[2].title.
Rate limits
Every write is limited per session by endpoints.defaults.rate_limit.member in engine.yaml (60/hour/session by default), and four times that per address.
# config/engine.yaml
endpoints:
defaults:
rate_limit:
member: 120/hour/session The contract
engine-dmo api member routes --project . # every operation and whether this project serves it
engine-dmo api member openapi --project . # the OpenAPI 3.1 document The same document is served at GET /api/member/v1/openapi.json. The path carries the major version; changes within v1 are additive, so ignore keys you do not know.
What it does not cover
Declared portal screens and actions are portal features and are not exposed through the API, and event submission has no API operation yet (GET /events lists the organisations’ event documents). Forms on the public site are posted through the content API, not this one.
Content MCP
How to connect an AI agent (Claude or any Model Context Protocol client) to a Meridian site: turning the endpoint on, OAuth sign-in and the consent choices, the mcp configuration block, registering clients, personal access tokens and engine-dmo mcp call for scripts, engine-dmo mcp over stdio for local development, the tools, resources and prompts, limits, and what to tell a data protection officer.
Migration
How to move an existing site into Meridian without losing content, media or search rankings: the engine's three imports (media manifests, content files with the HTML path, redirect CSVs) and their formats, the meridian-migrate toolkit for WordPress and Sanity exports, crawling the old site and matching its addresses, and a rehearsed cutover runbook.