100% Offline POSCompare vs Toast
Resources · API Docs

Headless CMS integration.

The API a bespoke-frontend tenant (e.g. a custom-coded restaurant site) calls to pull menu, footer, and SEO content, and to capture bulk-order/booking inquiries — instead of hardcoding any of it.

Every request is scoped to a tenant by a publishable key — no login, no cookies, no custom headers. All endpoints are CORS-open, so you can call them directly from client-side JS on your own domain.

Authentication

Live key: pk_cms_live_…. Test key (same tenant, same content — use for staging/local dev): pk_cms_test_….

Pass it as a ?key= query param on GET requests, or a key field in the JSON body on POST. Your platform admin gives you this key when your tenant is provisioned.

Note

An invalid or deactivated key returns 404 { "error": "Not found" } on every endpoint — this is deliberate, to avoid leaking which keys exist.

Categories, items, and portion variants.

Cache: public, s-maxage=60, stale-while-revalidate=300 · Rate limit: 120 requests/minute per key.

Response — 200
{
  "categories": [
    {
      "id": "uuid",
      "name": "Paneer Specials",
      "nameLocal": "पनीर स्पेशल",
      "imageUrl": null,
      "items": [
        {
          "id": "uuid",
          "name": "Kaju Curry",
          "nameLocal": "काजू करी",
          "description": null,
          "descriptionLocal": null,
          "price": 240,
          "imageUrl": null,
          "foodType": "VEG",
          "isAvailable": true,
          "isPopular": false,
          "variants": [
            { "id": "uuid", "label": "Full", "price": 240, "isDefault": true },
            { "id": "uuid", "label": "Half", "price": 130, "isDefault": false }
          ]
        }
      ]
    }
  ]
}

Tip

If variants is non-empty, treat it as authoritative (e.g. show “Full ₹240 / Half ₹130”). The item's own price only matters when variants is empty — it's the uniform, single price (e.g. a ₹7 tandoori roti).

Important

isAvailable: false means show it, don't hide it. The point of this flag is that customers see “currently unavailable,” not that the item silently disappears — render it visibly de-emphasized instead of filtering it out.

GET/api/cms/v1/public/site

Footer nav, contact info, and SEO/JSON-LD — everything for page chrome and <head> tags, in one request.

Cache: public, s-maxage=60, stale-while-revalidate=300 · Rate limit: 120 requests/minute per key.

Response — 200
{
  "tenant": {
    "businessName": "Restzo Cafe",
    "tagline": "Authentic. Hygienic. Welcoming.",
    "logoUrl": null,
    "coverImageUrl": null,
    "primaryPhone": "08349215106",
    "secondaryPhone": null,
    "addressText": "NH 30, Piprod, Paduwa, MP",
    "googleMapsUrl": null,
    "openingHoursText": "12 PM – 11 PM"
  },
  "footer": {
    "columns": [
      { "label": "Company", "labelLocal": null, "links": [{ "label": "About", "labelLocal": null, "url": "/about" }] }
    ]
  },
  "seo": {
    "metaTitle": "Restzo Cafe | Best Cafe in Town",
    "metaDescription": null,
    "ogImageUrl": null,
    "jsonLd": {
      "@context": "https://schema.org",
      "@type": "Restaurant",
      "name": "Restzo Cafe",
      "telephone": "08349215106",
      "address": "NH 30, Piprod, Paduwa, MP",
      "url": "https://restzocafe.com",
      "openingHours": "12 PM – 11 PM",
      "priceRange": "₹₹",
      "servesCuisine": ["North Indian"]
    }
  }
}

Embed seo.jsonLd verbatim in an inline script tag on every page:

JSON-LD embed (Next.js example)
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(data.seo.jsonLd) }}
/>

Note

Fields are only present when configured — sameAs/servesCuisine/priceRange may be absent. Don't assume every Schema.org/Restaurant key will be there. footer.columns is [] until the tenant admin adds columns — render nothing rather than erroring on an empty array.

GET/api/cms/v1/public/sitemap-urls

Cache: public, s-maxage=300, stale-while-revalidate=1800 · Rate limit: 60 requests/minute per key.

Response — 200
{
  "urls": [
    { "path": "/", "lastModified": "2026-07-01T17:16:11.880Z", "changeFrequency": "daily", "priority": 1.0 }
  ]
}

Important

This only ever returns "/" — we don't know your site's route structure. Build your own /sitemap.xml on your own domain (sitemaps must be same-origin with the site being indexed), merging this entry with your frontend's own static routes (/menu, /about, etc.).

POST/api/cms/v1/public/inquiries

Bulk-order / group-booking / general-contact lead capture. Replaces a client-side wa.me deep link with a real backend record.

Rate limit: 10 requests per 10 minutes per (key, IP) — tuned for a rare, high-intent form submission, not a high-frequency action.

Request body
{
  "key": "pk_cms_live_…",
  "type": "BULK_ORDER",
  "customerName": "Test Traveler",
  "customerPhone": "+917508839991",
  "guestCount": 25,
  "eventDate": "2026-07-10",
  "notes": "Bus tour group",
  "items": []
}
FieldRequiredNotes
keyYesNon-empty string
typeYes"BULK_ORDER" | "TABLE_ENQUIRY" | "GENERAL_CONTACT"
customerNameYes2–80 chars
customerPhoneYesDigits, optional leading +, 8–15 digits total
guestCountNoPositive integer, max 500
eventDateNoString, max 20 chars — send an ISO date like "2026-07-10"
notesNoMax 500 chars
itemsNoArray of { name, price, quantity }, max 50 — omit or send [] for a plain lead-capture form
Response — 201 (success)
{ "id": "uuid", "status": "NEW" }
Response — 400 (validation error)
{ "error": "Invalid input", "details": ["Too small: expected string to have >=2 characters"] }

Note

A successful submission triggers a notification to the tenant's phone once their webhook is configured — this happens asynchronously and never blocks or delays your 201 response.

Error shape summary

Every non-2xx response is { "error": string }, sometimes with an extra field (details on 400). There's no separate error-code enum — match on HTTP status.

StatusMeaning
200Success (GET)
201Inquiry created (POST only)
400Missing/invalid input — fix the request
404Key doesn't exist, or tenant is deactivated
429Rate limited — respect Retry-After

Questions or something not covered here? Get in touch.