Back
Client API
OpenAPI

Integrate your website

Ember CMS is an API-only backend. Your site is programmed and hosted separately. Call these public endpoints for published content, booking slots, and shop checkout. No auth token — CORS must allow your frontend origin (set by the platform). Connected sites also get https://api.yourdomain.lt as a branded API host (same paths).

Base: /api/v1/public
Or: https://api.<your-domain>
JSON only
Active websites · published content
1. Resolve the website
Look up the CMS website by domain (production) or public slug (preview). Only active sites resolve.
GET
/api/v1/public/websites/by-domain/{domain}

e.g. by-domain/acmebakery.com

GET
/api/v1/public/websites/by-slug/{slug}

e.g. by-slug/ember-studio

GET
/api/v1/public/websites/{website_id}

After you have the id

const res = await fetch("/api/v1/public/websites/by-slug/ember-studio");
const site = await res.json();
// site.id → use for pages, blog, booking, shop
// site.default_locale, site.timezone, …
2. Pages & metadata (optional)
Only if the site enables SEO pages. Published pages only. Each section is a meta field:{ key, type, value }. Types: string, text, number, boolean, url, json. Disabled fields are omitted on the detail endpoint.
GET
/api/v1/public/websites/{website_id}/pages
GET
/api/v1/public/websites/{website_id}/pages/{slug}
const pages = await fetch(`/api/v1/public/websites/${site.id}/pages`).then(r => r.json());
const home = await fetch(`/api/v1/public/websites/${site.id}/pages/home`).then(r => r.json());
// home.sections: [{ section_type: "meta", content: { key, type, value } }, …]
3. Blog
Published posts only (drafts/scheduled are hidden). body is sanitized HTML — render with your site's styles (or a sanitizer if you transform it further).
GET
/api/v1/public/websites/{website_id}/blog/posts
GET
/api/v1/public/websites/{website_id}/blog/posts/{slug}
4. Booking
List active services/staff, compute slots, then create a booking. Requires the tenant booking plan.
GET
/api/v1/public/websites/{website_id}/booking/services
GET
/api/v1/public/websites/{website_id}/booking/staff
GET
/api/v1/public/websites/{website_id}/booking/slots?service_id=&date_from=&date_to=&staff_member_id=

date_from / date_to are ISO dates (YYYY-MM-DD). staff_member_id optional.

POST
/api/v1/public/websites/{website_id}/booking/bookings
await fetch(`/api/v1/public/websites/${site.id}/booking/bookings`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    service_id: "…",
    starts_at: "2026-08-01T10:00:00Z",
    customer_name: "Casey Morgan",
    customer_email: "casey@example.com",
    customer_phone: "+15550100",
    notes: "First visit",
    // optional free-form fields (also accepted as "extra")
    metadata: { phone: "+3706…", party_size: 2, source: "web" },
  }),
});
5. Shop
Active products and Stripe Checkout. Response includes payment_url — redirect the customer there. Tenant must have shop entitlements and a ready Stripe Connect account when Connect is required.
GET
/api/v1/public/websites/{website_id}/shop/products
GET
/api/v1/public/websites/{website_id}/shop/products/{product_id}

Your Product ID (slug), e.g. hoodie-black

POST
/api/v1/public/websites/{website_id}/shop/checkout
const order = await fetch(`/api/v1/public/websites/${site.id}/shop/checkout`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    customer_name: "Sam Patel",
    customer_email: "sam@example.com",
    items: [{ product_id: "…", quantity: 2 }],
  }),
}).then(r => r.json());

window.location.href = order.payment_url;
6. Customers & newsletter
Website-scoped end-customer accounts (separate from CMS staff login). Use for newsletter signup and “my orders”. Pass the customer access token on checkout to link the order to the account.
POST
/api/v1/public/websites/{website_id}/customers/register
POST
/api/v1/public/websites/{website_id}/customers/login
POST
/api/v1/public/websites/{website_id}/customers/refresh
GET
/api/v1/public/websites/{website_id}/customers/me

Bearer customer token

GET
/api/v1/public/websites/{website_id}/customers/me/orders

Bearer customer token

POST
/api/v1/public/websites/{website_id}/newsletter/subscribe
POST
/api/v1/public/websites/{website_id}/newsletter/unsubscribe
// Register
const auth = await fetch(`/api/v1/public/websites/${site.id}/customers/register`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "sam@example.com",
    password: "securepass",
    full_name: "Sam Patel",
    newsletter_opt_in: true,
  }),
}).then(r => r.json());

// Newsletter only (no password)
await fetch(`/api/v1/public/websites/${site.id}/newsletter/subscribe`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "news@example.com" }),
});

// My orders
const orders = await fetch(`/api/v1/public/websites/${site.id}/customers/me/orders`, {
  headers: { Authorization: `Bearer ${auth.access_token}` },
}).then(r => r.json());
Errors & CORS

Errors return JSON like { "code": "not_found", "message": "…", "details": null, "request_id": "…" }.

Browser calls only work from origins on the website’s allowlist (plus platform admin origins). Ask the platform team to add your production and preview origins when the site is connected.

Domains are connected by pointing nameservers to Cloudflare so the platform can manage DNS. Content is never rendered by this API — your frontend owns HTML/UI.

// Minimal bootstrap
async function loadSite(host = window.location.hostname) {
  const site = await fetch(`/api/v1/public/websites/by-domain/${host}`).then(async (r) => {
    if (!r.ok) throw new Error(await r.text());
    return r.json();
  });
  const page = await fetch(`/api/v1/public/websites/${site.id}/pages/home`).then((r) => r.json());
  return { site, page };
}