# Cimplify Docs — full content Concatenated markdown for every doc page. For a section index instead, fetch /llms.txt. # Agent prompts URL: /docs/agent-prompts > Copy-paste prompts for the most common Cimplify workflows. Drop any block into Claude Code, Cursor, or any MCP-aware agent — fill the angle-bracket placeholders and run. These prompts are deliberately terse. Each one names the canonical doc the agent should read first, the exact commands or [MCP tools](/docs/mcp) to call, and the exit condition. Replace `` placeholders before running. Every prompt assumes the agent has either: - the [Cimplify CLI](/docs/cli) installed and `cimplify login` complete, **or** - an MCP connection to `https://api.cimplify.io/mcp` ([setup](/docs/mcp#connect)) ## Ship a new storefront end-to-end ```text title="Scaffold → deploy → custom domain" Set up a Cimplify storefront called and deploy it to . Source of truth: https://cimplify.dev/docs/tldr - Use the MCP tools at api.cimplify.io/mcp when available; otherwise shell `cimplify --json`. - After every command, echo the JSON envelope. - Stop on the first non-zero exit code and name it from https://cimplify.dev/docs/cli#exit-codes. - Poll deployment status with cimplify_get_deployment_status (or `cimplify status --json`); never `sleep`. - Idempotent — re-running any step is safe. ``` ## Rebrand an existing storefront ```text title="Apply a new brand to a scaffolded template" Rebrand the storefront in <./my-store> to match these tokens: name: primary: <#0F172A> accent: <#F59E0B> fontDisplay: <"Playfair Display"> fontBody: <"Inter"> Rules (https://cimplify.dev/docs/templates/brand): - Single source of truth is `lib/brand.ts`. Update it first. - Mirror color tokens into `app/globals.css` `@theme` block. Do not hard-code colors elsewhere. - Do not edit ejected SDK components; they pick up tokens automatically. - Run `bun run check:brand` after edits; fix any schema errors it reports. - Verify by opening `http://localhost:3000` (start `bun dev` if not running). - Commit with message `brand: `. Do not deploy. ``` ## Add a homepage section ```text title="New Server Component section, cache-tagged" Add a `` section to the homepage of the storefront in <./my-store>. Pattern (mirrors the Server Component sections under `app/_sections/` in the scaffolded template): - Create `app/_sections/stories.tsx` as an async Server Component with `"use cache"` and `cacheTag(tags.collections())` from `@cimplify/sdk/server`. - Source data from `client.catalogue.getCollection({ slug: "stories" })`. - Render with the existing `` and `` SDK components (do not eject). - Insert into `app/page.tsx` between the hero and the featured collection. - Run `bun run check` — must pass typecheck, lint, brand, cart-flow, contract. - Do not modify `lib/brand.ts` or any file under `components/ui/`. ``` ## Migrate to a new SDK version ```text title="SDK upgrade with breaking-change scan" Upgrade the storefront in <./my-store> from `@cimplify/sdk@<0.44.x>` to `@cimplify/sdk@`. Process: - Read the release notes for the target range (`npm view @cimplify/sdk versions --json` to list, then `npm view @cimplify/sdk@` per release). - Bump the pin in `package.json`, run `bun install`. - For each breaking change called out in the changelog, grep the project for the old symbol and apply the documented replacement. - Run `bun run check`. If `check:contract` fails, the SDK ↔ mock contract drifted — stop and report. - Do not run `cimplify deploy`. End by printing a summary of every file you changed and every breaking change you applied. ``` ## Add a custom checkout payment method ```text title="Wire an additional payment option" Add as a checkout payment option in the storefront at <./my-store>. Reference: https://cimplify.dev/docs/api-reference/checkout (flat ProcessArgs body — top-level fields, not nested). - The body shape is the contract — never wrap fields. - Eject the existing `` component with `cimplify add payment-method-picker`. - Add the option in the ejected picker, wiring the provider-specific body (e.g. `mobile_money_details: { phone_number, provider }`). - Validate the body against the typed `ProcessArgs` from `@cimplify/sdk`. - Run `bun run check:cart` — the add→checkout flow must still pass. ``` ## Provision a CI deploy key ```text title="One-time setup for headless deploys" Create a CI-scoped API key for the Cimplify project linked at <./my-store>, and write the GitHub Actions snippet that uses it. Steps: - Confirm `.cimplify/project.json` exists; if not, fail with NOT_LINKED (exit 4). - Print the dashboard URL the human needs to visit to create the key (https://app.cimplify.io/settings/developer). - Once the user provides `dk_live_…`, store it as a repo secret named `CIMPLIFY_API_KEY` (instruct, do not perform). - Write `.github/workflows/deploy.yml` that runs on push to main: install CLI via the curl one-liner, `cimplify deploy --prod --api-key "$CIMPLIFY_API_KEY" --yes --json`. - Print the exit-code legend from https://cimplify.dev/docs/cli#exit-codes so the workflow can map failures. ``` ## Diagnose a deploy that didn't go live ```text title="Triage a failed or stuck deployment" The latest deploy of the project at <./my-store> isn't live. Find out why. Triage path: - `cimplify status --json` — what's the latest deployment_id and status? - If status is `failed`, `cimplify logs --deployment --json | tail -200`. Quote the first error line you see. - If status is `queued` or `building` for >5 minutes, the build is stuck — try `cimplify rollback `. - If status is `superseded` (exit 2), a newer push raced it; usually fine — confirm with `cimplify status` again. - If domains misroute, `cimplify domains ls --json` — confirm the verified+attached domain. - Report: deployment_id, status, the failing log line (if any), and the recommended next action. Do not auto-rollback without confirming. ``` ## Reset a sandbox storefront to a clean state ```text title="Wipe and re-seed for repeatable demos" Reset the local mock state for the storefront in <./my-store>. - Stop any running `bun dev` process. - `cimplify mock reset` (clears the in-process mock backing store). - `cimplify mock seed ` (re-seeds with the chosen industry fixture). - `bun dev` to restart both servers. - Verify: `curl localhost:8787/v1/catalogue/products | jq '.data | length'` returns the expected seed count (~24 for retail). - Do not touch the linked cloud project; this is local-only. ``` ## How these are built Every prompt above is structured the same way so you can adapt them: 1. **Goal** — one sentence, with `` placeholders for the moving parts. 2. **Source of truth** — the canonical doc URL the agent reads first. 3. **Rules** — operational guardrails: which tools to call, what to echo, when to stop. 4. **Exit condition** — what counts as success, what counts as a halt. If you build a prompt that should live here, open a PR against `content/docs/agent-prompts.mdx`. Keep them under ~12 lines; if a workflow needs more, it belongs in a dedicated doc page and the prompt should link to it. --- # API Reference URL: /docs/api-reference > Cimplify exposes a single REST surface at `/api/v1/*`. All requests are JSON. Most reads are public storefront calls keyed by `pk_`; writes that touch a customer require an active session cookie obtained from `/auth/verify-otp`. ## Base URL | Environment | Base URL | | --- | --- | | `Production` | `https://api.cimplify.io/api/v1` | | `Sandbox` | `https://sandbox.cimplify.io/api/v1` | ## Authentication Identify your business and authorize the request with one of the following header combinations. | Header | Value | When to use | | --- | --- | --- | | `X-Public-Key` | `pk_live_…` / `pk_test_…` | Browser, storefront, mobile. Read catalogue, manage cart, run checkout. | | `X-API-Key` | `sk_live_…` / `sk_test_…` | Server-side. Required for write operations on behalf of the business. | | `X-Business-Id` | `biz_…` | Optional override when a key serves multiple businesses. | ```bash title="cURL" curl https://api.cimplify.io/api/v1/catalogue/products \ -H "X-API-Key: sk_test_your_secret_key" \ -H "X-Business-Id: biz_abc123" ``` ## Response envelope Every response is wrapped in a discriminated envelope. Successful responses set `success: true` and put the payload on `data`. Failures set `success: false` and include an `error` object with a stable `code` for programmatic handling. ```json { "success": true, "data": { "id": "prod_abc123", "name": "Espresso" } } ```
## Error codes | HTTP | Code | Meaning | | --- | --- | --- | | `400` | `VALIDATION_ERROR` | Request body or query failed validation | | `401` | `UNAUTHORIZED` | Missing or invalid API key / session | | `403` | `FORBIDDEN` | Authenticated but not allowed | | `404` | `NOT_FOUND` | Resource missing or not visible | | `409` | `CONFLICT` | Idempotency replay or duplicate write | | `429` | `RATE_LIMITED` | Too many requests; back off and retry | | `503` | `SERVICE_UNAVAILABLE` | Upstream subsystem temporarily down | ## Idempotency Mutating endpoints (`POST /cart/items`, `POST /checkout`, `POST /orders/:id/cancel`, `POST /scheduling/bookings/*/cancel`, `POST /uploads/init`) accept an optional `Idempotency-Key` header. Replay the same key and the server returns the original response without re-executing the action. Pick a UUID per logical attempt; reuse only on retry. ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/cart/items \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Idempotency-Key: 5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" \ -H "Content-Type: application/json" \ -d '{"item_id": "prod_abc123", "quantity": 1}' ``` ## Rate limits Public-key traffic is rate-limited at 100 req/s per IP and 1,000 req/min per business. Secret-key traffic is limited at 50 req/s per key. Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. Exceeding the limit returns `429` with `code: RATE_LIMITED` and a `Retry-After` header. ## Sections - [**Catalogue**](/docs/api-reference/catalogue): Products, variants, categories, collections, bundles, composites, add-ons - [**Cart**](/docs/api-reference/cart): Server-side cart: add, update, remove items, apply coupons - [**Checkout**](/docs/api-reference/checkout): Submit a cart for payment; flat body shape - [**Orders**](/docs/api-reference/orders): List, retrieve, cancel; guest access via bill_token - [**Subscriptions**](/docs/api-reference/subscriptions): Customer subscription management - [**Auth**](/docs/api-reference/auth): OTP login, session status, profile - [**Scheduling**](/docs/api-reference/scheduling): Service booking, slot availability, variant-aware - [**Inventory**](/docs/api-reference/inventory): Stock and availability for products and variants - [**Activity**](/docs/api-reference/activity): Session events, intent, recommendations, dismissals - [**FX**](/docs/api-reference/fx): Live rates and lockable currency quotes - [**Places**](/docs/api-reference/places): Address autocomplete and place details - [**Uploads**](/docs/api-reference/uploads): Presigned PUT uploads for images and attachments - [**Support**](/docs/api-reference/support): Customer chat widget conversation and messages - [**Webhooks**](/docs/api-reference/webhooks): Server-side event subscriptions --- # Choosing a checkout integration URL: /docs/checkout > Cimplify ships five layered ways to take a payment. Pick the highest tier you can; each step down the list trades a few minutes of integration time for more control. All five eventually reach the same backend endpoint and emit the same [Element events](/docs/concepts/element-events). ## The five tiers | Tier | Hosted by | Roughly | You give up | | --- | --- | --- | --- | | [Drop-in (hosted Pay)](/docs/checkout/drop-in) | `pay.cimplify.io` | 5 LOC + a redirect | Customer leaves your site | | [Embedded iframe](/docs/checkout/embedded) | `link.cimplify.io` | ~20 LOC | You wire postMessage yourself | | [Controlled Elements (React)](/docs/checkout/elements) | Iframe inside your React tree | 1 component, ~30 LOC | You stay on the React side of the iframe | | [Vanilla Elements](/docs/checkout/vanilla) | Iframe inside your DOM | ~50 LOC | You manage mount/unmount manually | | [Headless (`CheckoutPage` or your own)](/docs/checkout/headless) | You | Open-ended | No iframe; you build every screen | ## Decision tree ```text Do you ship a frontend at all? No → Drop-in (Pay session). Done. Yes → Do you need to keep the customer on your domain? No → Drop-in. Yes → Are you on React? No → Vanilla Elements. Yes → Do you want Cimplify-rendered UI for auth/address/payment? Yes → Controlled Elements (`` or piecewise). No → Headless (`` or your own UI driving `client.checkout.process`). ``` ## What stays the same across all tiers - The [checkout lifecycle](/docs/concepts/checkout-lifecycle) (`preparing → processing → … → success | failed`) is identical. - The cart shape (built via `client.cart.addItem({ item_id, quantity, ... })`) is the same. Drop-in / Pay sessions accept a `cart_id`; Elements expect the cart to already exist on the customer's session cookie. - Server-side, every tier funnels into `POST /v1/checkout` via the same `CheckoutFormData` body. - [Appearance API](/docs/checkout/appearance) works the same way for every iframe-based tier. ## What differs | Concern | Drop-in | Embedded / Elements / Vanilla | Headless | | --- | --- | --- | --- | | Where the iframe lives | `pay.cimplify.io` | Your page | | | Cimplify-rendered fields | All of them | All of them | None | | Compliance scope (PCI, OTP) | Cimplify | Cimplify (data never leaves the iframe) | You | | Cart pre-fill UX | Server-built session | Use `set_cart` / `cartId` prop | You | | Custom layout/copy | Limited (Appearance API) | Limited (Appearance API) | Unlimited | ## Two-line drop-in example For most teams this is the right starting point. You can always graduate to embedded later. ```ts title="server" // Server: create a session with your secret key const session = await fetch("https://api.cimplify.io/v1/checkout/sessions", { method: "POST", headers: { "Authorization": `Bearer ${process.env.CIMPLIFY_SECRET_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ cart_id: cart.id, success_url, cancel_url }), }).then(r => r.json()); return Response.redirect(session.url); ``` ## Next - [**Drop-in (hosted Pay)**](/docs/checkout/drop-in): Redirect to `pay.cimplify.io` - [**Controlled Elements**](/docs/checkout/elements): React components that render the iframe --- # CLI URL: /docs/cli > Every storefront workflow — scaffold, develop, deploy — runs through the Cimplify CLI. ## Install ```bash curl -fsSL https://cimplify.io/install | sh ``` Detects your OS + arch, downloads the matching binary from the latest GitHub release, verifies its sha256, and drops it in `~/.local/bin/cimplify`. Cold start ≈ 20 ms. The binary bundles every template and the full component registry, so `init`, `add`, and `list` work the same as `deploy`, `logs`, `env`, and the rest — no separate `@cimplify/sdk` install required. Pin a version or change the install location: ```bash curl -fsSL https://cimplify.io/install | sh -s -- --version cli-v0.1.0 curl -fsSL https://cimplify.io/install | sh -s -- --prefix ~/.cimplify ``` Need to run it once without installing? `bunx @cimplify/cli ` (or `npx`, `pnpm dlx`) works too — slower cold start, same behavior. ## Zero to production For the scannable ten-command flow, see the [TL;DR](/docs/tldr). The rest of this page is the per-subcommand reference. ## Subcommand index ### Scaffold & develop | Command | What it does | | --- | --- | | [`cimplify init`](/docs/cli/init) | Scaffold a Next.js storefront from one of six templates. | | [`cimplify add`](/docs/cli/components) / `cimplify list` | Eject one of 67 registry components into your project. | | `cimplify dev [--remote]` | Run your project's dev script (optionally with prod env). | | [`cimplify-mock`](/docs/cli/mock) | Boot the local mock API on `:8787`. Ships with `@cimplify/sdk`. | ### Auth | Command | What it does | | --- | --- | | [`cimplify login`](/docs/cli/login) | Browser-first OAuth Authorization Code + PKCE on a loopback redirect. `--api-key` is the CI escape hatch. | | `cimplify logout` | Forget saved credentials. | | `cimplify whoami` | Show the current account and business. | ### Projects & deploys | Command | What it does | | --- | --- | | `cimplify projects ls` | List projects in the current business. | | `cimplify projects create ` | Create a new Next.js project. | | `cimplify link ` | Link CWD to a Cimplify project (writes `.cimplify/project.json`). | | `cimplify unlink` | Remove the local project link. | | [`cimplify deploy`](/docs/cli/deploy) | Push current SHA + trigger a build (preview by default; `--prod` for production). | | `cimplify rollback ` | Re-deploy a previous deployment's SHA. | | `cimplify status` | Show the latest deployment for the linked project. | | `cimplify logs [--follow]` | Stream build logs. | ### Env & domains | Command | What it does | | --- | --- | | [`cimplify env ls / pull / push / add / rm`](/docs/cli/env) | Manage env vars per scope: `development \| preview \| production`. | | [`cimplify domains ls / add / verify / rm / attach / detach / primary`](/docs/cli/domains) | Manage business-scoped domains; attach to projects per env. | ### Agent helpers | Command | What it does | | --- | --- | | [`cimplify introspect`](/docs/cli/introspect) | One-shot snapshot of the current storefront: auth, link, brand, mock seed, env keys, git. Pure local — no API calls. | | [`cimplify doctor [--offline]`](/docs/cli/doctor) | 14 pre-flight checks with verdicts and fix hints. Exits `1` on any `fail`. | | [`cimplify explain [topic]`](/docs/cli/explain) | Print canonical guidance offline (cart, products, bundles, composites, errors, …). 20 bundled topics. | ## Global flags for agents and CI Two global flags apply to every cloud subcommand and let scripts (or LLM agents) drive the CLI deterministically. | Flag | Equivalent env | What it does | | --- | --- | --- | | `--json` | `CIMPLIFY_JSON=1` | Emit a single JSON envelope on stdout instead of colorized progress. Success: `{ "ok": true, "data": ... }`. Failure: `{ "ok": false, "error": { "code", "message", "remediation"? } }`. All human chatter (`step`, `success`, `info`) is suppressed; the envelope is the agent's only parse target. | | `--yes`, `-y` | `CIMPLIFY_YES=1` | Auto-confirm every yes/no prompt. Without it, a non-TTY run that needs confirmation throws `INTERACTIVE_REQUIRED` rather than guessing. | ```bash title="Agent-driven flow" # Headless CI auth cimplify login --api-key dk_live_xxx --json # Capture latest deployment state DEPLOY=$(cimplify status --json) # Trigger and wait, parsing JSON outcome cimplify deploy --prod --yes --json ``` ```bash title="Sample JSON envelopes" # Success {"ok":true,"data":{"deployment":{"id":"dep_01H…","git_sha":"abc…","environment":"production","status":"active","url":"https://…"}}} # Failure {"ok":false,"error":{"code":"NOT_LINKED","message":"no linked project in this directory","remediation":"run `cimplify link `"}} ``` In `--json` mode the browser PKCE login path refuses (interactive); use `--api-key` to authenticate non-interactively. ## Exit codes Distinct per error class so callers can branch on `$?` without parsing output: | Code | Name | Meaning | | --- | --- | --- | | `0` | OK | Success. For `deploy`, deployment is `active`. | | `1` | FAILED | Generic failure (build failed, deploy errored). | | `2` | SUPERSEDED | A newer push raced this deploy; usually fine. | | `3` | ABORTED | User canceled (Ctrl-C) or `cimplify_cancel_deployment`. | | `4` | NOT_LINKED | `.cimplify/project.json` missing. Run `cimplify link `. | | `7` | INTERACTIVE_REQUIRED | A prompt was needed but `--yes` not passed and stdin is non-TTY. | | `9` | NOT_FOUND | Resource (project, domain, deploy) does not exist. | | `10` | NETWORK_ERROR | Could not reach `api.cimplify.io`. | | `12` | TIMEOUT | Long-poll exceeded its window; the underlying job may still be running. | | `20` | NOT_LOGGED_IN | No saved token. Run `cimplify login` or pass `--api-key`. | | `21` | AUTH_FAILED | Token expired or revoked. Re-login. | | `30` | INVALID_INPUT | Argument/flag validation failed. | Full list in `@cimplify/cli/errors`. ## Mock API The mock storefront API ships in `@cimplify/sdk` because it's tightly coupled to the SDK's domain types and seed data. Once the SDK is installed in your project, boot it with `bunx @cimplify/sdk mock` (or the `cimplify-mock` bin directly). Full surface in [the mock reference](/docs/cli/mock). ## Where next - [**init: scaffold a storefront**](/docs/cli/init) Six templates, working in 60 seconds. - [**login: browser-first OAuth**](/docs/cli/login) PKCE on a loopback redirect. - [**deploy: push and ship**](/docs/cli/deploy) Preview, prod, rollback, logs. - [**mock: local API**](/docs/cli/mock) Eight seeds, frozen clocks, webhooks. --- # Docs URL: /docs > Build storefronts, embed checkout, and let agents transact — on Cimplify. The SDK, hosted checkout, and UCP all share one backend and one data model. import { Cards, Card } from 'fumadocs-ui/components/card' import { Store, CreditCard, Bot, Code, Blocks, Link2, FlaskConical, BookOpen, Terminal, Server, } from 'lucide-react' # Build with Cimplify Whether you're shipping a full storefront, embedding checkout in an existing site, or building an agent that transacts on behalf of a customer — there's a path for it. Same backend, same data model, three surfaces. } title="Storefronts" description="A complete Next.js App Router storefront from six industry templates. 90+ React components and 13 typed services, so most of your code is brand." href="/docs/quickstart" /> } title="Checkout" description="A single line of code on any site, or full control with Elements. Mobile money, cards, 1-click via Cimplify Link." href="/docs/checkout" /> } title="Agents (UCP)" description="The Universal Commerce Protocol lets AI agents discover products, build carts, and check out — with signed mandates and verifiable consent." href="/docs/ucp" /> ## Accelerate with the Cimplify CLI ```bash curl -fsSL https://cimplify.io/install | sh # one-time, ~5 seconds cimplify init my-store --template retail # scaffold a Next.js storefront cd my-store && bun dev # mock API on :8787 + Next on :3000 ``` A single native binary handles scaffolding, dev, env vars, deploys, custom domains, and self-update. The [TL;DR page](/docs/tldr) walks the ten commands from empty shell to live production domain; the full [CLI reference](/docs/cli) covers every subcommand, JSON envelopes for agents and CI, and exit codes. ## Reference } title="TypeScript SDK" description="createCimplifyClient + 13 services. Every method returns Result — never throws." href="/docs/sdk" /> } title="React SDK" description="90+ components, 30+ hooks. CimplifyProvider, CartDrawer, full-page CataloguePage and CheckoutPage." href="/docs/sdk/react" /> } title="Cimplify Link" description="Cross-merchant saved cards, addresses, orders. Four embeddable iframe Elements at link.cimplify.io." href="/docs/link" /> } title="REST API" description="Every endpoint the SDK uses, documented end-to-end. Flat /checkout, variant-aware /scheduling, signed UCP." href="/docs/api-reference" /> ## Tools } title="CLI" description="cimplify init / login / deploy / env / domains / update. Every flag, every JSON envelope, every exit code." href="/docs/cli" /> } title="MCP server" description="api.cimplify.io/mcp exposes 26 typed tools so Claude / Cursor / ChatGPT can drive every workflow as tool calls." href="/docs/mcp" /> } title="Testing harness" description="In-process Hono mock + three pre-baked vitest suites. 30-second feedback loop, zero production calls." href="/docs/testing" /> ## Environments | Environment | Public key | Secret key | Description | | --- | --- | --- | --- | | Live | `pk_live_` | `sk_live_` | Real data and real payments | | Test | `pk_test_` | `sk_test_` | Isolated sandbox, no real charges | Test mode uses a completely isolated sandbox business. Create your keys at [app.cimplify.io/settings/developer](https://app.cimplify.io/settings/developer). ## For agents This site is built for both human readers and AI agents. - Every doc URL has a Markdown twin: `/llms/.mdx`, e.g. `/docs/sdk/cart` maps to `/llms/docs/sdk/cart.mdx`. - A section index lives at [`/llms.txt`](/llms.txt). Full content concatenated at [`/llms-full.txt`](/llms-full.txt). - Heading IDs are server-rendered, so anchor links work in raw HTML fetches. - `robots.txt` explicitly allows GPTBot, ClaudeBot, anthropic-ai, PerplexityBot, Google-Extended, CCBot. - Every scaffolded storefront ships `/.well-known/ucp` so UCP-aware agents can discover the business automatically. --- # Cimplify Link URL: /docs/link > Link is the shopper-facing layer of Cimplify: a saved-payment-and-address vault, an OTP identity, and a small set of embeddable iframe Elements that let merchants pull that identity into any storefront. It's hosted at `link.cimplify.io` and ships as the `@cimplify/link` package. ## Two surfaces - **Embeddable Elements**: iframe-based UI components a merchant drops into their storefront: `AuthElement`, `CheckoutElement`, `AccountElement`. These are the building blocks behind every Cimplify checkout integration. - **Dashboard**: a logged-in shopper UX at `link.cimplify.io` where customers manage their saved details across every Cimplify-powered store. ## The Element types | Element | Iframe route | Use it for | | --- | --- | --- | | [AuthElement](/docs/link/auth-element) | `/elements/auth` | OTP sign-in (email or phone). Single-line input that turns into a Code field on submit. | | [CheckoutElement](/docs/link/checkout-element) | `/elements/checkout` (alias `/elements/payment`) | Full unified checkout: auth, address, payment method, submit. The iframe behind `` and the hosted Pay page. | | [AccountElement](/docs/link/account-element) | `/elements/account/*` | Logged-in account portal (orders, addresses, payment methods, sessions). | ## The dashboard Customers can also visit `link.cimplify.io` directly to manage their saved details outside any merchant context. The dashboard surfaces the same data the Elements read from. See [dashboard pages](/docs/link/dashboard) for the per-page breakdown. ## How shoppers join Link There's no signup. Any time a customer pays through any Cimplify merchant and ticks "remember me for 1-click checkout", their email/phone, address, and payment method are saved to their Link identity. Next checkout (anywhere on the network) starts with their saved details prefilled after a single OTP. ## How merchants opt in Merchants don't configure Link explicitly. Using any of the iframe-based checkout tiers ([drop-in](/docs/checkout/drop-in), [embedded](/docs/checkout/embedded), [controlled](/docs/checkout/elements), [vanilla](/docs/checkout/vanilla) ) automatically gives shoppers Link if they want it. Pass `enroll_in_link: false` on `processCheckout` to suppress the "remember me" checkbox. ## Authentication model Link is OTP-based. The `AuthElement` sends a one-time code via email or SMS; verification mints a session token (`lk_…`) the parent controller broadcasts to every other mounted Element via `set_token`. The token is short-lived and auto-refreshes; the Link cookie on `link.cimplify.io` is the long-lived identity. ## Smallest possible embed ```tsx import { CimplifyClient } from "@cimplify/sdk"; import { CimplifyCheckout } from "@cimplify/sdk/react"; const client = new CimplifyClient({ publicKey: "pk_live_…", credentials: "include", }); // One component → AuthElement + AddressElement + PaymentElement, all wired up. ``` ## Next - [**AuthElement**](/docs/link/auth-element): OTP sign-in - [**CheckoutElement**](/docs/link/checkout-element): Unified checkout iframe --- # MCP server URL: /docs/mcp > Cimplify exposes a Model Context Protocol server at api.cimplify.io/mcp. Agents (Claude Code, Cursor, ChatGPT Connectors, Continue, Goose) speak streamable HTTP per the MCP 2025-11-25 spec and drive every CLI workflow as typed tool calls: no shell-out, no help-text parsing, no stdio bridge required. ## What's there - **Endpoint**: `https://api.cimplify.io/mcp` (single endpoint, POST/GET/DELETE per spec) - **Transport**: Streamable HTTP, MCP protocol `2025-11-25` - **Auth**: `Authorization: Bearer dk_…` (same API keys the CLI uses) - **Session**: server-assigned `MCP-Session-Id` header at `initialize`, echoed on every subsequent request The full design lives in `docs/tickets/mcp-server.md` in the platform repo. Below is the customer-facing surface. ## Connect ### Claude Desktop / Claude Code `~/.config/claude/claude_desktop_config.json` (Linux) or the equivalent on macOS/Windows: ```json { "mcpServers": { "cimplify": { "url": "https://api.cimplify.io/mcp", "headers": { "Authorization": "Bearer dk_live_xxx" } } } } ``` ### Cursor `.cursor/mcp.json` in any project root: ```json { "mcpServers": { "cimplify": { "url": "https://api.cimplify.io/mcp", "headers": { "Authorization": "Bearer dk_live_xxx" } } } } ``` ### ChatGPT Connectors / Continue / Goose Use the same `url` + `Bearer` header. Spec-compliant clients all accept this shape. ### Headless agents POST `initialize` with `Origin` set to one of the allowlisted hosts (or omitted for non-browser clients), `MCP-Protocol-Version: 2025-11-25`, and the Bearer token. The server returns `MCP-Session-Id` in the response headers; include it on every subsequent request. ## Auth scopes API keys must hold `mcp:invoke` to enter the MCP surface at all. Per-tool scopes are checked individually: | Scope | Tools | | --- | --- | | `mcp:invoke` | (meta) required for every call. `cimplify_whoami` needs only this. | | `projects:read` | `cimplify_attach_project`, `cimplify_ctx`, `cimplify_get_project`, `cimplify_list_projects` | | `projects:write` | `cimplify_create_project` | | `repos:read` | `cimplify_get_repo` | | `repos:write` | `cimplify_provision_repo`, `cimplify_connect_repo`, `cimplify_unlink_repo`, `cimplify_mint_clone_url` | | `deploys:read` | `cimplify_get_deployment_status`, `cimplify_list_deployments`, `cimplify_read_logs` | | `deploys:write` | `cimplify_deploy`, `cimplify_rollback`, `cimplify_cancel_deployment` | | `env:read` | `cimplify_list_env` | | `env:write` | `cimplify_set_env`, `cimplify_remove_env` | | `domains:read` | `cimplify_list_domains` | | `domains:write` | `cimplify_add_domain`, `cimplify_verify_domain`, `cimplify_attach_domain`, `cimplify_detach_domain` | | `docs:read` | `cimplify_search_docs` | Create keys with the scopes you need at `/v1/businesses/:bid/api-keys` (REST). The dashboard surfaces this as a checkbox set. ## Destructive ops Three tools are tagged destructive: - `cimplify_unlink_repo` (when `purge=true`, also deletes the upstream repo) - `cimplify_remove_env` (data loss) - `cimplify_detach_domain` These refuse with `DESTRUCTIVE_DENIED` unless the target project has `settings.mcp_destructive_ops_enabled = true`. Flip via: ```bash PUT /v1/businesses/:bid/projects/:pid Content-Type: application/json { "settings": { "mcp_destructive_ops_enabled": true, ...existing settings } } ``` Opting in is **never** an MCP tool; agents can't enable their own destructive surface. Bootstrap from outside. ## Idempotency Write tools accepting `idempotency_key` (string) cache the response per-session for 1 hour. Same key + same input returns the same response without re-executing: - `cimplify_create_project` - `cimplify_provision_repo` - `cimplify_connect_repo` - `cimplify_deploy` - `cimplify_rollback` - `cimplify_add_domain` Underlying services have their own dedup floor too: `cimplify_deploy` deduplicates by `(project, git_sha, env_scope)` regardless of the idempotency key. ## Tool surface 26 tools total (12 reads, 14 writes). Discover at runtime via `tools/list`. Every tool's input schema is JSON Schema, exposed on the listing. ## Resources Four read-only URIs: - `cimplify://ctx`: account + business + attached project snapshot - `cimplify://projects`: first 100 projects in the business - `cimplify://projects/{id}`: single project + repo + last deploy - `cimplify://docs`: pointer to `cimplify.dev/llms.txt`. Fetch the URL via your standard WebFetch. The docs surface is intentionally a pointer, not a proxy. The MCP server doesn't ship docs content; agents read it from `cimplify.dev` directly. The advantage: docs iterate independently of platform deploys. ## Errors JSON-RPC standard codes (`-32700` parse error, `-32600` invalid request, `-32601` method not found, etc.) plus Cimplify-specific codes in the server-defined range: | Code | Meaning | | --- | --- | | `-32001` | UNAUTHORIZED: Bearer invalid | | `-32002` | SCOPE_DENIED: key lacks a required scope | | `-32003` | DESTRUCTIVE_DENIED: project hasn't opted in | | `-32004` | SESSION_REQUIRED: `MCP-Session-Id` missing/unknown | | `-32005` | PROTOCOL_VERSION_UNSUPPORTED | | `-32006` | RESOURCE_NOT_FOUND | | `-32008` | RATE_LIMITED | | `-32009` | CROSS_BUSINESS_DENIED | ## End-to-end example A typical agent session shipping a fresh storefront: ``` 1. POST /mcp initialize → MCP-Session-Id assigned 2. POST /mcp tools/list → 26 tools 3. POST /mcp resources/read cimplify://docs → docs URLs 4. (agent WebFetches cimplify.dev/llms.txt + relevant pages) 5. POST /mcp tools/call cimplify_create_project → new project 6. POST /mcp tools/call cimplify_attach_project → bind for subsequent calls 7. POST /mcp tools/call cimplify_provision_repo → Freestyle repo URL 8. POST /mcp tools/call cimplify_set_env → env vars for preview 9. POST /mcp tools/call cimplify_deploy → deployment_id, status: queued 10. POST /mcp tools/call cimplify_get_deployment_status (poll until active) 11. POST /mcp tools/call cimplify_add_domain → DNS records (customer publishes DNS) 12. POST /mcp tools/call cimplify_verify_domain → verified 13. POST /mcp tools/call cimplify_attach_domain → live ``` About 12 tool calls, no shell-out, no help-text parsing, fully observable in the agent's UI. ## Capabilities advertised `initialize` returns: ```json { "protocolVersion": "2025-11-25", "capabilities": { "tools": { "listChanged": false }, "resources": { "subscribe": false, "listChanged": false } }, "serverInfo": { "name": "cimplify", "title": "Cimplify", "version": "..." } } ``` `resources.subscribe` is advertised as `false` and will flip to `true` in a future PR when deployment-status push notifications land over SSE. Until then, poll `cimplify_get_deployment_status`. The server intentionally only advertises capabilities it implements — agents that probe `capabilities.resources.subscribe` see the truth. ## Compared to the CLI The CLI (`@cimplify/cli`) and the MCP server expose the same underlying capabilities through different transports: - **CLI**: argv to process exit code + stdout. Best for humans, CI, scripts. - **MCP**: JSON-RPC tool call to typed response. Best for agents. Both go through the same backend services, the same scope checks, the same audit log. An agent that has API access to a project via the CLI has the same access via MCP, and vice versa. Choose by the consumer. For agents that don't speak MCP natively, the CLI also has a `--json` flag that returns structured output on stdout, usable from any `Bash` tool. Install once with `curl -fsSL https://cimplify.io/install | sh`. --- # Cimplify Pay URL: /docs/pay > Cimplify Pay is the hosted-checkout product. You create a session (or a payment link) on your server; the customer is redirected to `pay.cimplify.io`, which renders the full checkout flow on Cimplify infrastructure; on completion they bounce back to your `success_url`. It's the same iframe-based checkout you can embed directly; Pay just ships it on a Cimplify-owned domain so you don't have to. ## Two surfaces | URL | Page | Use it for | | --- | --- | --- | | `pay.cimplify.io/s/:sessionId` | Checkout session | You're selling something with a cart and want a full checkout (auth, address, payment). | | `pay.cimplify.io/:token` | Payment link | You've already created an order (or a balance to settle) and just need to take payment. | ## Drop-in vs Pay The [drop-in checkout](/docs/checkout/drop-in) guide and the Pay product describe the same flow from different angles: drop-in is the merchant integration story; Pay is the product. If you're writing code, start at [drop-in](/docs/checkout/drop-in). If you want the API surface, you're in the right place. ## End-to-end flow ```text Storefront Cimplify API pay.cimplify.io Customer │ │ │ │ │ POST /v1/checkout/sessions ─────────────▶│ │ │ │ │ │ │◀─ { id, url, status, expires_at } │ │ │ │ │ │ │── 302 redirect to url ──────────────────────────────────────▶│ │ │ │ │ │ │ GET /v1/checkout/sessions/:id │ │ │◀──────────────────────│ │ │ │ │ render checkout │ │ │ │◀ user pays ──────│ │ │ POST /v1/checkout │ │ │ │◀──────────────────────│ │ │ │ │ │ │ webhook: order.completed ◀── │ │ 302 success_url ▶│ ``` ## Auth flow inside Pay The checkout-session page (`/s/:sessionId`) embeds the same [CheckoutElement](/docs/link/checkout-element) you can integrate directly; auth, address, and payment selection live in one iframe. The payment-link page (`/:token`) takes a different approach because the order already exists. It first asks the customer to choose between **guest checkout** and **signing in to Link**. Either branch lands on the same provider selection UI; signing in just pre-fills saved methods. | Branch | Component | What the customer does | | --- | --- | --- | | Choice screen | `AuthChoice` | Picks guest or Link. | | Guest | `GuestFlow` | Enters phone or card without an account. | | Link | `LinkFlow` | OTP into Link, picks a saved method. | | Confirmation | `Success` | Sees the paid order and (if guest) is invited to enroll in Link. | ## Theming Both surfaces accept the standard [ElementAppearance](/docs/checkout/appearance). Pass it on session create and Cimplify stores it; on the customer's next visit the page renders with your theme. ## Branding the page The hosted page renders a small `BusinessHeader` with your business name and logo (read from your business settings). The footer reads "Powered by Cimplify". For full white-labelling, use the embedded iframe instead and host it on your own domain. ## Webhooks Pay relies on the same webhook events as any other checkout integration: `order.completed` and `payment.succeeded` are the two you almost always wire up. See [webhooks](/docs/concepts/webhooks). ## Next - [**Checkout sessions**](/docs/pay/sessions): Create-redirect-webhook flow - [**Payment links**](/docs/pay/payment-links): Token-based pay-an-order URLs --- # Quickstart URL: /docs/quickstart > From zero to a working storefront in 60 seconds. `cimplify init` scaffolds a Next.js app wired to the local mock; the same code points at your live business when you bring keys. Three integration tiers, all in the same SDK: **scaffolded template** (most agents pick this), **typed client** for custom flows, **REST** for non-JS backends. Already know which tier you want and just need the command list? The [TL;DR](/docs/tldr) page has the ten commands from empty shell to live custom domain. ## Tier 1: Scaffold a storefront The `init` command creates a Next.js project from one of six industry templates, installs deps, wires the cart drawer, mock-seeded catalogue, and the testing harness. ```bash curl -fsSL https://cimplify.io/install | sh # install once cimplify init my-store --template retail cd my-store bun dev ``` Templates: `bakery` · `restaurant` · `retail` · `services` · `grocery` · `fashion`. Each ships its own `lib/brand.ts` (single source of truth for every visible string), brand-validated via [BrandSchema](/docs/templates/brand). Open `http://localhost:3000`; products, cart, checkout, account, and chat are all wired. Open `http://localhost:8787` for the mock API admin surface. ### Run the test harness ```bash bun run check # typecheck + lint + brand + cart-flow + contract bun run check:brand # schema invariants on lib/brand.ts bun run check:cart # add → dedupe → remove against in-process mock bun run check:contract # validates SDK ↔ mock contract end-to-end ``` ## Tier 2: Typed client (custom UI) For bespoke storefronts. Every method returns `Result` and never throws. ### 1. Install ```bash bun add @cimplify/sdk ``` ### 2. Construct a client ```ts title="lib/cimplify.ts" import { createCimplifyClient } from '@cimplify/sdk' export const client = createCimplifyClient({ publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY!, }) ``` ### 3. Browse, cart, checkout ```ts // Catalogue const products = await client.catalogue.getProducts({ limit: 24 }) if (!products.ok) throw products.error // Cart: flat add-to-cart payload (variant + add-ons go on the body, not nested) await client.cart.addItem({ item_id: 'prod_studio-tee-natural', quantity: 1, variant_id: 'var_studio-tee-natural_size_s', add_on_options: ['addopt_gift_wrap'], }) const cart = await client.cart.get() if (!cart.ok) throw cart.error // Checkout: flat ProcessArgs body (top-level fields, not wrapped) const result = await client.checkout.process({ cart_id: cart.value.id, customer: { name: 'Jane Doe', email: 'jane@example.com', phone: '+233244000000', }, order_type: 'delivery', payment_method: 'mobile_money', mobile_money_details: { phone_number: '+233244000000', provider: 'mtn' }, }) if (!result.ok) throw result.error console.log('Order:', result.value.order_id, 'bill_token:', result.value.bill_token) ``` ### 4. React components (90+) ```tsx title="app/layout.tsx" 'use client' import { CimplifyProvider, CartDrawerProvider, CartDrawer, } from '@cimplify/sdk/react' import { client } from '@/lib/cimplify' export default function Root({ children }: { children: React.ReactNode }) { return ( {children} router.push('/checkout')} /> ) } ``` ## Tier 3: REST For server-to-server integrations from any language. Every endpoint lives under `/api/v1` and accepts either `X-API-Key: sk_…` for secret keys or `X-Public-Key: pk_…` for client keys. ```bash title="cURL" curl https://api.cimplify.io/api/v1/catalogue/products \ -H "X-Public-Key: pk_test_..." \ -H "X-Business-Id: bus_..." ``` ## Where to next - [**TypeScript SDK**](/docs/sdk) All 13 services: catalogue, cart, checkout, scheduling, … - [**React SDK**](/docs/sdk/react) 90+ components, 30+ hooks, drawer, full pages - [**CLI**](/docs/cli) init, login, deploy, env, domains, mock - [**Testing harness**](/docs/testing) Pre-baked vitest suites + the in-process mock oracle --- # TypeScript SDK URL: /docs/sdk > Type-safe wrapper around the Cimplify REST API. Single dependency, browser-safe, framework-agnostic core, with a React layer and a Server Components layer on top. ## Install ```bash bun add @cimplify/sdk # bun (recommended) npm install @cimplify/sdk yarn add @cimplify/sdk ``` ## Construct a client ```ts import { createCimplifyClient } from '@cimplify/sdk' export const client = createCimplifyClient({ publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY!, // Optional: // baseUrl: 'https://api.cimplify.io', // fetch: customFetch, // injected fetch (testing harness uses this) }) ``` The same client construction works in browsers, Node, Bun, Deno, Workers, and React Server Components. For server-side keys (`sk_…`) prefer [getServerClient](/docs/sdk/server). ## Result type: never throws Every method returns `Result`. Always check `.ok`: ```ts const r = await client.catalogue.getProducts() if (!r.ok) { console.error(r.error.code, r.error.message) return } console.log(r.value.items) ``` ## Service surface | Service | Surface | Page | | --- | --- | --- | | `client.catalogue` | Products, variants, categories, collections, bundles, composites, add-ons, deals, quotes | [Catalogue →](/docs/sdk/catalogue) | | `client.cart` | Add / update / remove items, apply coupon, get cart | [Cart →](/docs/sdk/cart) | | `client.checkout` | Process checkout, payment authorization, poll status | [Checkout →](/docs/sdk/checkout) | | `client.auth` | OTP request/verify, profile, logout | [Auth →](/docs/sdk/auth) | | `client.orders` | List, get, cancel, attach customer, verify payment | [Orders →](/docs/sdk/orders) | | `client.scheduling` | Variant-aware slots, availability, bookings, reschedule, cancel | [Scheduling →](/docs/sdk/scheduling) | | `client.subscriptions` | List, cancel, pause, resume, skip-next | [Subscriptions →](/docs/sdk/subscriptions) | | `client.activity` | Event recording, recommendations, dismissable messages | [Activity →](/docs/sdk/activity) | | `client.fx` | Spot rate, locked quote | [FX →](/docs/sdk/fx) | | `client.places` | Address autocomplete and details | [Places →](/docs/sdk/places) | | `client.uploads` | Init / upload / confirm flow | [Uploads →](/docs/sdk/uploads) | | `client.support` | Chat-widget conversations, send / list messages | [Support →](/docs/sdk/support) | ## Public exports | Subpath | Use | | --- | --- | | `@cimplify/sdk` | Browser-safe typed client + domain types + `Result` | | `@cimplify/sdk/react` | 90+ components + 30+ hooks | | `@cimplify/sdk/server` | Server Components helpers, cache tags, revalidate* | | `@cimplify/sdk/utils` | Money helpers, formatting, slug helpers | | `@cimplify/sdk/advanced` | Lower-level escape hatches | | `@cimplify/sdk/mock` | Programmatic mock for tests | | `@cimplify/sdk/mock/msw` | MSW handlers wrapping the in-process mock | | `@cimplify/sdk/testing` | Test harness: zod schemas, `createTestClient`, fixtures, `assertX` helpers | | `@cimplify/sdk/testing/suite` | Pre-baked vitest suites: `createBrandSuite`, `createCartFlowSuite`, `createContractSuite` | | `@cimplify/sdk/styles.css` | Pre-compiled Tailwind utilities for the React components | ## Cross-cutting concepts - [**Result<T, E>**](/docs/concepts/result) Why methods never throw, how to narrow - [**Money type**](/docs/concepts/money) Branded string, parsePrice / formatPrice - [**Idempotency**](/docs/concepts/idempotency) Auto-keys for write methods, replay safety - [**Error handling**](/docs/sdk/errors) CimplifyError shape, codes, retryable flag --- # Storefront templates URL: /docs/templates > Six production-shape Next.js storefronts you scaffold with one command. Each one ships with cart drawer wiring, mock-seeded data, a brand-as-config layer, and pre-baked test suites, so the loop from `init` to `deploy` stays inside an afternoon. ## Scaffold ```bash # Install once (native binary, ~20ms cold start) curl -fsSL https://cimplify.io/install | sh cimplify init my-store --template retail ``` That writes a Next 16 App Router project, installs dependencies, and boots the local mock. From there, edit `lib/brand.ts` for content and `app/globals.css` for the palette; the rest of the storefront re-renders against those two files. ## The six templates | Template | Industry shape | Industry-specific surface | | --- | --- | --- | | `bakery` | Pastries, cakes, custom orders | Product modal sheet, custom-order CTA | | `restaurant` | Menu + takeout / dine-in | `/reservations` page, modifiers | | `retail` | Variant-heavy electronics / general | Full `/products/[slug]` + JSON-LD | | `services` | Booked appointments | `/book` calendar widget | | `grocery` | High-SKU staples | Quick-add cards, basket-first UX | | `fashion` | Editorial apparel | `/lookbook`, `/size-guide`, Playwright snapshots | ## What every template ships with - Next 16 App Router scaffold with `cacheComponents: true` - `app/layout.tsx` with metadata, Organization JSON-LD, and providers wired - `components/providers.tsx`: `CimplifyProvider` + `CartDrawerProvider` + the SDK `CartDrawer` - `app/cart`, `app/checkout`, `app/orders/[id]`, `app/account/*` using SDK pages - `lib/brand.ts`: single source of truth for every visible string - `app/globals.css @theme`: palette, radius, fonts in one place - `__tests__/{brand,cart-flow,contract}.test.ts`: three-line suite wrappers - Mock seed paired to the brand via `brand.mock.seed` - `sitemap.ts`, `robots.ts`, `llms.txt` generated from `brand` ## The 60-second loop ```bash cimplify init my-store --template retail cd my-store bun dev # mock + Next dev together # edit lib/brand.ts bun run check # typecheck + lint + brand + cart-flow + contract suites ``` `bun dev` boots the in-process Hono mock alongside Next so the full storefront (catalogue, cart, checkout) works offline. The check script runs in <15s, fast enough that an agent edits and tests in the same turn. ## File layout ```bash my-store/ ├── app/ │ ├── layout.tsx │ ├── page.tsx # home │ ├── shop/ # CataloguePage │ ├── products/[slug]/ # ProductPage (retail / fashion) │ ├── cart/, checkout/, orders/[id]/ │ ├── account/ # CimplifyAccount │ ├── about/, faq/, terms/, privacy/, … │ └── globals.css # @theme tokens ├── components/ │ ├── providers.tsx │ ├── header.tsx, footer.tsx, hero.tsx │ └── cart-pill.tsx, cart-drawer.tsx ├── lib/ │ ├── brand.ts # ⭐ content │ └── cart.ts └── __tests__/ ├── brand.test.ts # 3 lines ├── cart-flow.test.ts # 3 lines └── contract.test.ts # 3 lines ``` ## Next - [**Brand schema**](/docs/templates/brand) The full `lib/brand.ts` contract - [**Customizing**](/docs/templates/customizing) Eject components, extend the schema --- # Testing URL: /docs/testing > The SDK ships an in-process Hono mock that mirrors the production lens, plus pre-baked vitest suites and zod schemas as the single source of truth. Boot the mock, run the suites, get a 30-second feedback loop: no live API, no shared state. ## Why a test harness Agents (and humans) need to know in seconds whether an edit broke the SDK / backend contract. Hitting `api.cimplify.io` from CI is too slow, too flaky, and too leaky. The SDK's mock is the oracle: it's implemented from the same shape contracts as the production lens (~99% parity) and runs in the same process as your tests. ## The three suites | Suite | Catches | Runtime | | --- | --- | --- | | `createBrandSuite` | Missing fields, placeholder copy, invalid email/locale/currency, seed mismatch | < 2 s | | `createCartFlowSuite` | SDK / mock contract drift, add/dedupe/remove regressions | ~ 5 s | | `createContractSuite` | Outbound payload schema, inbound response schema, checkout shape | ~ 3 s | ## Three-line tests Templates wire each suite in three lines so the SDK owns the cases. When a new contract test ships, every storefront inherits it on `bun update @cimplify/sdk`. ```ts title="__tests__/cart-flow.test.ts" import { createCartFlowSuite } from "@cimplify/sdk/testing/suite"; import { brand } from "../lib/brand"; createCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId }); ``` ## Run it ```bash bun run check # typecheck + lint + all three suites (~15 s) bun run check:brand # the brand suite alone bun run check:cart # cart-flow alone bun run check:contract # contract alone ``` ## What gets validated - **Outbound**: every `cart.addItem` body is `safeParse`d against `AddItemPayloadSchema`. Missing `variant_id`, naked `productId`, etc. fail the test with a precise field path. - **Inbound**: every cart response is `assertCart`ed. `display_attributes`, malformed money, and missing pricing fields surface as structured issues. - **Checkout**: `CheckoutResponseSchema` verifies `bill_token`, `order_id`, optional `client_secret`, `next_action`. - **Brand**: `BrandSchema` covers identity, contact, hero, FAQ, policies, account copy, mock pairing. ## Where each piece lives | Import | For | | --- | --- | | `@cimplify/sdk/testing` | Schemas, `assertX` helpers, `createTestClient`, fixtures | | `@cimplify/sdk/testing/suite` | The three pre-baked vitest suites | | `@cimplify/sdk/testing/msw` | MSW handlers wrapping the same mock; for component tests | | `@cimplify/sdk/mock` | Programmatic mock for your own tooling | ## Next - [**Test client**](/docs/testing/test-client) `createTestClient` + fixtures - [**Suites**](/docs/testing/suites) Brand, cart-flow, contract - [**Schemas**](/docs/testing/schemas) The zod registry and `SchemaViolationError` - [**MSW**](/docs/testing/msw) Same mock, MSW transport --- # TL;DR — zero to production URL: /docs/tldr > Ten commands from an empty shell to a live storefront at a custom domain. The full developer journey through the Cimplify CLI in one page. If you want the full narrative — installing, scaffolding, local dev, auth, projects, env, deploy, domains — read the [Quickstart](/docs/quickstart) and the [CLI reference](/docs/cli). This page is the scannable command list. ## Drive it from an agent Paste this into Claude Code, Cursor, or any MCP-aware assistant. The agent fans out through the [MCP tool surface](/docs/mcp) or shells `cimplify --json`; both terminate on the first non-zero exit code. ```text title="One-shot prompt" Set up a Cimplify storefront called and deploy it to . Follow the sequence at https://cimplify.dev/docs/tldr step-by-step. After every command, echo the JSON envelope and stop on the first non-zero exit code, naming the code from https://cimplify.dev/docs/cli#exit-codes. Use cimplify_get_deployment_status (MCP) or `cimplify status --json` (shell) to poll, never `sleep`. Idempotency: re-running any step is safe. ``` A pre-prompted library for common tasks (rebrand, add a section, migrate SDK versions) lives at [Agent prompts](/docs/agent-prompts). ## The ten commands ```bash title="From zero to production" # 0. Install the CLI (one-time, ~5s) curl -fsSL https://cimplify.io/install | sh # 1. Scaffold a Next.js storefront from an embedded template cimplify init my-store --template retail cd my-store # 2. Local dev: mock API on :8787 + Next dev on :3000 bun dev # 3. Authorize the CLI (browser PKCE; --api-key for CI) cimplify login # 4. Create the cloud project + link this directory cimplify projects create my-store cimplify link # writes .cimplify/project.json # 5. Push env vars to the preview scope cimplify env push --scope=preview # non-destructive; --overwrite to replace scope # 6. Preview deploy (git push + build + poll + URL printed on success) cimplify deploy # 7. Bring a custom domain (DNS records printed by `add`) cimplify domains add shop.example.com cimplify domains verify shop.example.com # 8. Promote and route the domain to production cimplify deploy --prod cimplify domains attach shop.example.com --env=production --primary ``` End state: `https://shop.example.com` resolves to the production deployment of your linked project. ## What each step does, in one line | # | Command | What happens on the wire | Verify | | --- | --- | --- | --- | | 0 | `curl ... install \| sh` | Worker serves `install.sh` → detects OS+arch → downloads matching binary from the latest `cli-v*` GitHub release → verifies sha256 → `~/.local/bin/cimplify`. | `cimplify --version` prints a semver. | | 1 | `cimplify init` | Materializes one of six embedded templates (`bakery` · `restaurant` · `retail` · `services` · `grocery` · `fashion`) and runs `bun install`. | Directory exists with `lib/brand.ts`, `package.json`, and `node_modules/`. | | 2 | `bun dev` | `concurrently` runs `cimplify-mock --seed retail` (real backend, seeded data) + `next dev`. | `http://localhost:3000` renders the storefront; `curl localhost:8787/v1/catalogue/products` returns products. | | 3 | `cimplify login` | PKCE on a loopback redirect. Token persisted at `~/.config/cimplify/auth.json` (mode 0600). `--api-key dk_…` for CI. | `cimplify whoami --json` returns `{ ok: true, data: { account, business } }`. | | 4 | `cimplify projects create` / `link` | `POST /v1/businesses//projects` creates project + Freestyle git repo. `link` writes `.cimplify/project.json`. | `.cimplify/project.json` exists; `cimplify status --json` resolves the project. | | 5 | `cimplify env push` | Reads `.env.local`, `POST` to `/env-vars/bulk-set` with `overwrite=false`. Public `NEXT_PUBLIC_*` keys are not encrypted. | `cimplify env ls --scope=preview --json` lists the expected keys. | | 6 | `cimplify deploy` | Mints a short-TTL Freestyle clone token → `git push` → `POST /deploy` → polls `/progress`. Prints `Deployed: ` on success. Exit codes: 0 active · 2 superseded · 1 failed · 12 timeout. | Exit 0 and a preview URL of the form `https://--.cimplify.app`. | | 7 | `cimplify domains add` / `verify` | `add` returns DNS records (TXT verify token + CNAME to a `cimplify.app` host). `verify` resolves and marks the domain verified. | `cimplify domains ls --json` shows the domain as `verified=true`. | | 8 | `cimplify deploy --prod` / `domains attach` | `--prod` flips `env_scope: production` through the same code path. `attach` writes a `ProjectDomain` row binding the verified domain to the prod scope. | `https://` resolves; `cimplify status --prod --json` shows the production deployment as `active`. | ## Resume mid-flow Every step is idempotent and the state lives outside your shell. If a session drops: ```bash cimplify whoami # step 3 done? cimplify status # steps 4–8: project linked, latest deploy, primary domain cimplify env ls # step 5: what's already pushed cimplify domains ls # step 7: verified / attached state ``` Re-running any command above is safe — `deploy` dedupes on `(project, git_sha, env_scope)`, `env push` defaults to non-destructive, `domains add` is a no-op if the domain already exists. ## Typical timing | Step | Time | | --- | --- | | Install + init + bun install | ~60s | | `bun dev`, see the storefront | ~10s | | Rebrand (`lib/brand.ts`, `app/globals.css` `@theme`) | minutes to hours | | Login + projects create + link | ~30s | | First preview deploy | ~60–90s build | | Domain verify (after DNS) | minutes (DNS TTL) | | Promote to prod | ~60–90s | ## Headless / agent flow Every command above accepts `--json` (single envelope on stdout, no progress chatter) and `--yes` (auto-confirm prompts). Distinct exit codes per error class: `NOT_LINKED=4`, `NOT_LOGGED_IN=20`, `NETWORK_ERROR=10`, `AUTH_FAILED=21`, `INVALID_INPUT=30`, `ABORTED=3`, `INTERACTIVE_REQUIRED=7`, `NOT_FOUND=9`, `TIMEOUT=12`. Agents can also drive the same flow through MCP. Every CLI command has a 1:1 tool (`cimplify_create_project`, `cimplify_set_env`, `cimplify_deploy`, `cimplify_get_deployment_status`, `cimplify_add_domain`, etc.). See [MCP server](/docs/mcp) for connection details. ## Next - [**Quickstart**](/docs/quickstart) — three integration tiers (scaffolded template, typed client, REST) - [**CLI reference**](/docs/cli) — every subcommand with flags and JSON envelopes - [**Deploy**](/docs/cli/deploy) — preview vs production, rollback, logs - [**Domains**](/docs/cli/domains) — verify, attach, promote primary --- # Universal Commerce Protocol (UCP) URL: /docs/ucp > A signed, capability-discoverable protocol AI agents use to browse, price, check out, and pay across any Cimplify business with verifiable consent. UCP is the agent-facing surface of every Cimplify business. Agents fetch a manifest, sign their requests, and check out on behalf of a principal with a cryptographic consent proof. There is no human in the loop, but every action is auditable end-to-end. ## The agentic commerce landscape Commerce is being reshaped by AI agents that browse, decide, and pay on behalf of humans. The industry is converging on a set of open patterns for this: - **Google's Agent Payments Protocol (AP2)**: an open spec for agent-to-merchant payments with verifiable consent, capability discovery, and auditable mandate trails. AP2 also markets under the "Universal Commerce Protocol" framing. - **Stripe's Agent Toolkit**: agent-aware SDK helpers, `restricted_keys` scoped to specific actions, and shared receipts. - **Anthropic's Model Context Protocol (MCP)**: generic protocol for exposing tools/resources to LLMs. Commerce surfaces can be exposed as MCP servers. - **Visa Intelligent Commerce / Mastercard Agent Commerce Credentials**: card-network mandates that bind a specific agent to a specific scope on the rails. Cimplify's UCP is our take on the same problem, **specifically tuned for the businesses you operate on Cimplify**: every business gets a UCP manifest at a stable URL by default, and every storefront the SDK builds is automatically reachable by compliant agents. UCP is designed to interoperate with AP2 (the wire formats overlap intentionally) and to be exposed as an MCP server for agents that prefer that envelope. You don't need to think about this if you're just building a storefront; UCP runs on the same backend as the public REST API, with the same data shapes. But if you're building an agent that transacts, this is your contract surface. ## Why a separate protocol from the public API Public REST assumes a browser session and a human who authenticates with OTP. UCP assumes a service-to-service identity with cryptographic consent on every mutation. The two share data shapes (cart, checkout, order) but differ in: - **Authentication**: UCP requests are HMAC-signed; public REST uses session cookies + OTP. - **Authorization**: UCP enforces consent scope at every mutation; public REST scopes by session. - **Discovery**: UCP publishes a typed manifest at a stable URL; public REST is documented but not introspectable. - **Replay protection**: UCP timestamps + body hashing prevent replay; public REST relies on idempotency keys. You can think of UCP as Cimplify's equivalent of an OpenAPI spec served at a stable URL, with built-in auth and consent semantics that fit the agent operator model. ## How it works 1. **Discover**: fetch `https://api.cimplify.io/ucp/v1/` to get the business's manifest: capabilities, supported payment bindings, shipping config, contact, hours. 2. **Authenticate**: sign every request with HMAC-SHA256 over `timestamp.method.path.body_hash`. The signature goes in `X-Request-Signature: t=,v1=` and the body hash in `X-Body-Hash: `. 3. **Identify**: every request also carries an `UCP-Agent` header with an `AgentIdentity` payload: who the agent is, who it acts for, and any session it's part of. 4. **Consent**: for any action that moves money or creates a binding obligation, attach a `ConsentProof` signed by the principal: scope (max amount, currency, expiry), action type, signature. 5. **Transact**: call the capability endpoints exposed in the manifest. Cart, price, checkout. Same data shapes as the public REST API. ## The manifest ```http GET https://api.cimplify.io/ucp/v1/sweet-bakery ``` ```json { "business": { "handle": "sweet-bakery", "name": "Sweet Bakery", "contact": { "email": "hello@sweetbakery.test", "phone": "+233244000000" } }, "capabilities": { "checkout": { "binding": "mobile_money", "currencies": ["GHS"], "min_amount": "5.00", "max_amount": "5000.00" }, "shipping": { "modes": ["pickup", "local_delivery"], "delivery_radius_km": 15 } }, "endpoints": { "cart": "/ucp/v1/sweet-bakery/cart", "checkout": "/ucp/v1/sweet-bakery/checkout" } } ``` ## Request signing Every UCP request must be signed. The signing input is: ``` timestamp + "." + method + "." + path + "." + body_hash ``` Where `body_hash` is `SHA256(body)` for `POST`/`PUT`/`PATCH`, or empty for read methods. The signature is sent as: ```http X-Request-Signature: t=1715000000,v1= X-Body-Hash: UCP-Agent: ``` The server verifies the signature and rejects requests older than 5 minutes. ## Consent proofs For mutations that bind the principal (checkout, recurring billing, refunds), the agent includes a `ConsentProof`: ```json { "action": "purchase", "scope": { "max_amount": "120.00", "currency": "GHS", "valid_until": "2026-05-12T18:00:00Z", "business_handle": "sweet-bakery" }, "signature": "", "issued_at": "2026-05-10T19:30:00Z" } ``` The principal's signature is verified against a public key registered with Cimplify's key directory. Scope is enforced server-side: an agent with a `max_amount: 120.00` proof cannot check out a 200 GHS cart. ## Example: agent checkout ```ts // 1. Discover what a business sells. const manifest = await fetch( 'https://api.cimplify.io/ucp/v1/sweet-bakery', ).then((r) => r.json()) // 2. Build a cart on behalf of the principal. const cart = await ucp.post(`${manifest.endpoints.cart}`, { items: [{ product_id: 'prod_sourdough', quantity: 2 }], }) // 3. Check out with a verifiable consent proof. const order = await ucp.post(`${manifest.endpoints.checkout}`, { cart_id: cart.id, consent_proof: { action: 'purchase', scope: { max_amount: '120.00', currency: 'GHS', business_handle: 'sweet-bakery', }, signature: principal.sign(challenge), issued_at: new Date().toISOString(), }, }) ``` ## Interoperability Cimplify UCP is designed to bridge cleanly to the broader agentic-commerce stack: - **AP2 manifests**: Cimplify can serve the AP2 manifest format at the same path on request (`Accept: application/vnd.ap2+json`). Capability shapes are nearly identical; we map our internal types to AP2 names at the edge. - **MCP servers**: every Cimplify business can expose its UCP capabilities as a Model Context Protocol server. Agents that speak MCP can call cart/checkout/order as MCP tools without learning UCP signatures. - **Stripe agent toolkit**: Cimplify's mobile-money and card-rails settlements layer can validate Stripe `restricted_keys` for agents that already have one. - **Card-network credentials**: when an agent presents a Visa Intelligent Commerce or Mastercard Agent Commerce credential, UCP accepts it as a substitute for the principal-signed `ConsentProof` with equivalent scope. The wire format is intentionally boring: HMAC + JSON. Your agent code stays portable across networks; you only re-sign per business. ## Status UCP is in **developer preview**. The manifest format is stable; the auth scheme may evolve before GA. Production agents should pin to a UCP version header. --- # Activity URL: /docs/api-reference/activity > The activity API records lightweight session signals (product views, searches, cart events) so that the storefront can show relevant recommendations, intent-based incentives, and contextual messages. Every call is scoped to the active session token. ## POST /api/v1/activity/events Submit a batch of activity events. The server records them, recomputes the visitor’s `intent`, and returns the updated message set. ### Body | Field | Description | | --- | --- | | `events` | Array of typed events. Tags: `product_view`, `category_view`, `search`, `cart_add`, `cart_remove`. | | `website_id` | Optional analytics website ID. | | `domain` | Optional originating domain. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/activity/events \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{ "domain": "shop.example.com", "events": [ { "type": "product_view", "product_id": "prod_abc123", "product_name": "Espresso", "category_id": "cat_drinks", "price": 4.5, "page_path": "/products/espresso" }, { "type": "cart_add", "product_id": "prod_abc123", "quantity": 1 } ] }' ``` ### Response ```json { "success": true, "data": { "processed": 2, "intent": "considering", "messages": [ { "code": "free_delivery_threshold", "title": "Add GHS 12 more for free delivery", "severity": "info" } ] } } ``` ## GET /api/v1/activity/state Read the current session activity snapshot, computed intent, active incentive, and the active message set. ```bash title="cURL" curl https://api.cimplify.io/api/v1/activity/state \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "activity": { "viewed_products": [{ "product_id": "prod_abc123", "count": 3 }], "viewed_categories": [{ "category_id": "cat_drinks", "count": 1 }], "search_count": 0 }, "intent": "considering", "messages": [ { "code": "free_delivery_threshold", "title": "Add GHS 12 more for free delivery" } ], "incentive": { "template_id": "free_ship_v2", "agent_id": null } } } ``` Anonymous sessions return `activity: null`, `intent: “baseline”`, and an empty `messages` array. ## GET /api/v1/activity/recommendations Personalised product picks based on viewed categories, with optional `limit` (max 50, default 10). ```bash title="cURL" curl "https://api.cimplify.io/api/v1/activity/recommendations?limit=12" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "recommendations": [ { "product": { "id": "prod_xyz", "name": "Latte" }, "reason": "from_category:cat_drinks" } ], "intent": "considering", "incentive": null } } ``` ## POST /api/v1/activity/messages/dismiss Suppress a specific message for the rest of the session. The body uses **`code`**, the identifier returned in the `messages` array. There is no `message_id` field. ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/activity/messages/dismiss \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"code": "free_delivery_threshold"}' ``` ### Response ```json { "success": true, "data": { "dismissed": "free_delivery_threshold", "messages": [] } } ``` - [**Cart**](/docs/api-reference/cart) Cart writes already emit cart-add / cart-remove events automatically. - [**Products**](/docs/api-reference/catalogue/products) Source of recommended products. --- # Auth URL: /docs/api-reference/auth > Customer-facing OTP login. Requesting an OTP delivers a 4–6 digit code over SMS or email; verifying it issues a session cookie that subsequent calls (cart, checkout, orders) recognise as the same customer. ## GET /api/v1/auth/status Returns the current session, useful for hydrating storefronts that don’t want to keep client-side identity state. ### Request ```bash title="cURL" curl https://api.cimplify.io/api/v1/auth/status \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "is_authenticated": true, "customer": { "id": "cus_01H…", "name": "Ama Mensah", "email": "ama@example.com", "phone": "+233241234567" } } } ``` Anonymous sessions return `is_authenticated: false` and `customer: null`. ## POST /api/v1/auth/request-otp Send a one-time passcode to a phone number or email. The server picks the channel from `contact_type`; if omitted, it’s inferred from the format of `contact`. ### Body | Field | Type | Description | | --- | --- | --- | | `contact` | string | E.164 phone (`+2332…`) or email. Required. | | `contact_type` | string | Optional. `phone` or `email`. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/auth/request-otp \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"contact": "+233241234567", "contact_type": "phone"}' ``` ### Response ```json { "success": true, "data": { "delivery_channel": "sms", "expires_in_seconds": 300, "next_resend_in_seconds": 30 } } ``` ## POST /api/v1/auth/verify-otp Exchange the OTP for an authenticated session. The field is **`otp_code`**, not `code`. On success the response sets a session cookie and returns the resolved customer. ### Body | Field | Type | Description | | --- | --- | --- | | `contact` | string | Same value as the request-OTP call. Required. | | `otp_code` | string | 4–6 digit code. Required. | | `contact_type` | string | Optional `phone` / `email`. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/auth/verify-otp \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{ "contact": "+233241234567", "otp_code": "847362" }' ``` ### Response ```json { "success": true, "data": { "customer": { "id": "cus_01H…", "name": "Ama Mensah", "email": "ama@example.com", "phone": "+233241234567" }, "session": { "expires_at": "2026-05-08T10:30:00Z" } } } ``` ### Errors - `400 VALIDATION_ERROR`: `otp_code` empty, fewer than 4 or more than 6 chars. - `401 UNAUTHORIZED`: code expired, mismatched, or already redeemed. ## POST /api/v1/auth/logout Invalidate the active session. Returns `{ success: true, data: null }`. ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/auth/logout \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## POST /api/v1/auth/profile Update one or more fields on the active customer record. At least one of `name`, `email`, `phone` must be supplied. ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/auth/profile \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"name": "Ama N. Mensah"}' ``` ### Response ```json { "success": true, "data": { "id": "cus_01H…", "name": "Ama N. Mensah", "email": "ama@example.com", "phone": "+233241234567" } } ``` - [**Orders**](/docs/api-reference/orders) List orders for the authenticated customer. - [**Subscriptions**](/docs/api-reference/subscriptions) Manage recurring billing for the logged-in customer. --- # Cart URL: /docs/api-reference/cart > The cart is server-owned and bound to the caller’s session. Items are added by `item_id` with an optional `variant_id` and add-on selections. The same cart is read on the storefront and on checkout; there is no client-side cart sync. ## GET /api/v1/cart Returns the current cart enriched with line items, pricing breakdown, applied coupon, and totals. ### Request ```bash title="cURL" curl https://api.cimplify.io/api/v1/cart \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "id": "cart_01H…", "items": [ { "id": "ci_01H…", "item_id": "prod_abc123", "name": "Espresso", "variant_id": "var_double", "variant_name": "Double", "quantity": 2, "unit_price": "6.00", "line_total": "12.00", "add_on_options": ["opt_oat"], "special_instructions": null } ], "pricing": { "subtotal": "12.00", "discount_total": "0.00", "tax_total": "1.06", "total_price": "13.06", "currency": "GHS" }, "coupon": null } } ``` ## DELETE /api/v1/cart Removes every line item and clears the applied coupon. ```bash title="cURL" curl -X DELETE https://api.cimplify.io/api/v1/cart \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/cart/items Returns just the line items array, the same shape as `data.items` on `GET /cart`. ```bash title="cURL" curl https://api.cimplify.io/api/v1/cart/items \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## POST /api/v1/cart/items Add a line item. The body is flat; there is no nested `configuration` or `product` wrapper. Use the `Idempotency-Key` header to dedupe retries. ### Body | Field | Type | Description | | --- | --- | --- | | `item_id` | string | Product ID. Required. | | `quantity` | integer | 1 to 100. Default 1. | | `variant_id` | string | Selected variant. | | `add_on_options` | string[] | Add-on option IDs. | | `special_instructions` | string | Free-form note shown to the merchant. | | `bundle_selections` | object[] | For bundle products: `{ component_id, variant_id?, quantity }`. | | `composite_selections` | object[] | For composites: `{ component_id, quantity, variant_id?, add_on_option_id? }`. | | `scheduled_start` | datetime | For service products: booking start in ISO 8601. | | `scheduled_end` | datetime | Booking end. | | `staff_id` | string | Preferred staff for the booking. | | `quote_id` | string | Pre-issued quote to lock pricing. | | `billing_plan_id` | string | For subscription products. | ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/cart/items \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Idempotency-Key: 5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" \ -H "Content-Type: application/json" \ -d '{ "item_id": "prod_abc123", "quantity": 2, "variant_id": "var_double", "add_on_options": ["opt_oat"] }' ``` ### Errors Returns `400 VALIDATION_ERROR` if `item_id` is empty, `quantity < 1`, or `quantity > 100`. Returns `404 NOT_FOUND` when the product or variant doesn’t exist for the active business. ## PATCH /api/v1/cart/items/\{cart_item_id\} Update the quantity of a line item already in the cart. ```bash title="cURL" curl -X PATCH https://api.cimplify.io/api/v1/cart/items/ci_01H… \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"quantity": 3}' ``` ## DELETE /api/v1/cart/items/\{cart_item_id\} Remove a single line item. ```bash title="cURL" curl -X DELETE https://api.cimplify.io/api/v1/cart/items/ci_01H… \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/cart/count Returns the total quantity across line items as an integer. ```json { "success": true, "data": 4 } ``` ## GET /api/v1/cart/total Returns the cart total as a Money string. ```json { "success": true, "data": "13.06" } ``` ## POST /api/v1/cart/coupons Apply a coupon code to the cart. Replaces any previously applied coupon. ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/cart/coupons \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"coupon_code": "WELCOME10"}' ``` ## DELETE /api/v1/cart/coupons/current Remove the active coupon. Idempotent. ```bash title="cURL" curl -X DELETE https://api.cimplify.io/api/v1/cart/coupons/current \ -H "X-Public-Key: pk_test_your_publishable_key" ``` - [**Next: Checkout**](/docs/api-reference/checkout) Submit the cart for payment with a flat checkout body. - [**Catalogue: Products**](/docs/api-reference/catalogue/products) Where `item_id` values come from. --- # Catalogue URL: /docs/api-reference/catalogue > The catalogue surface covers everything visible to a shopper: products and variants, categories, collections, bundles, composites, and the modifier groups (add-ons) attached to them. All endpoints live under `/api/v1/catalogue` and accept an optional `location_id` query parameter for location-specific pricing and availability. ## Quick reference | Method | Endpoint | Description | | --- | --- | --- | | GET | `/api/v1/catalogue` | Combined catalogue snapshot | | GET | `/api/v1/catalogue/products` | List products with filters | | GET | `/api/v1/catalogue/products/{id}` | Get a product | | GET | `/api/v1/catalogue/products/{id}/variants` | List variants | | POST | `/api/v1/catalogue/products/{id}/variants/find` | Find variant by axes | | GET | `/api/v1/catalogue/products/{id}/add-ons` | Modifier groups for a product | | GET | `/api/v1/catalogue/categories` | List categories | | GET | `/api/v1/catalogue/collections` | List collections | | GET | `/api/v1/catalogue/bundles` | List bundles | | GET | `/api/v1/catalogue/composites` | List composites | | POST | `/api/v1/catalogue/composites/{id}/calculate-price` | Price a build-your-own selection | | POST | `/api/v1/catalogue/quotes` | Create a price quote | | POST | `/api/v1/catalogue/deals/validate` | Validate a discount code | ## Product types | Type | Description | | --- | --- | | `product` | Physical or digital good. | | `service` | Time-bookable; pairs with the scheduling endpoints. | | `bundle` | Fixed combination of components. | | `composite` | Build-your-own with component groups. | ## Sections - [**Products**](/docs/api-reference/catalogue/products): List, retrieve, look up by slug, fetch deals, billing plans, schedules - [**Variants**](/docs/api-reference/catalogue/variants): Variant axes, find-by-axes, get-by-id - [**Categories**](/docs/api-reference/catalogue/categories): Category trees, products in a category, attributes, deals - [**Collections**](/docs/api-reference/catalogue/collections): Curated groupings with their products and attributes - [**Bundles**](/docs/api-reference/catalogue/bundles): Fixed-component bundles - [**Composites**](/docs/api-reference/catalogue/composites): Build-your-own products with calculate-price - [**Add-Ons**](/docs/api-reference/catalogue/add-ons): Modifier groups attached to products --- # Checkout URL: /docs/api-reference/checkout > Convert the active cart into an order, run payment, and return everything the caller needs to confirm or redirect. The body is **flat**: fields like `cart_id`, `customer`, and `payment_method` sit at the top level. There is no `checkout_data` wrapper. ## POST /api/v1/checkout Process a cart checkout. The server validates the cart, charges the chosen payment method, creates an order, and emits an order created event. On success the response includes a guest `bill_token`; store it client-side for unauthenticated order lookups. ### Body | Field | Type | Description | | --- | --- | --- | | `cart_id` | string | Cart to charge. Required. | | `customer` | object | `{ name, email, phone, notes?, save_details }`. Email or phone required. | | `order_type` | string | One of `delivery`, `pickup`, `dine-in`, `walk-in` (kebab-case). | | `address_info` | object | Delivery / pickup details. All fields optional. | | `payment_method` | string | Provider channel, e.g. `mobile_money`, `card`, `cash`. | | `mobile_money_details` | object | `{ phone_number, provider, provider_other? }` when paying via MoMo. | | `special_instructions` | string | Note attached to the order. | | `idempotency_key` | string | Client key to dedupe retries. Same effect as the header. | | `pay_currency` | string | ISO 4217 code if the customer is paying in a non-cart currency. | | `fx_quote_id` | string | Pre-locked FX quote from `POST /fx/quotes`. | | `metadata` | object | Pass-through, e.g. `{ success_url, cancel_url }`. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/checkout \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Idempotency-Key: 5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" \ -H "Content-Type: application/json" \ -d '{ "cart_id": "cart_01H…", "customer": { "name": "Ama Mensah", "email": "ama@example.com", "phone": "+233241234567", "save_details": true }, "order_type": "delivery", "address_info": { "street_address": "12 Independence Ave", "city": "Accra", "country": "GH" }, "payment_method": "mobile_money", "mobile_money_details": { "phone_number": "+233241234567", "provider": "mtn" }, "metadata": { "success_url": "https://shop.example.com/orders/success", "cancel_url": "https://shop.example.com/cart" } }' ``` ### Response ```json { "success": true, "data": { "order_id": "ord_01H…", "order_number": "ORD-1284", "bill_token": "bt_n3k…", "payment_status": "pending", "payment_reference": "ref_kjz…", "redirect_url": "https://pay.example.com/checkout/sess_01H…", "amount": "13.06", "currency": "GHS" } } ``` Persist `bill_token` for guests; it’s the proof-of-ownership query parameter on `GET /api/v1/orders/:id?token=…`. Authenticated customers don’t need it; the server matches the order to their account. ### Errors - `400 VALIDATION_ERROR`: missing customer contact, empty `payment_method`, invalid email/phone. - `404 NOT_FOUND`: `cart_id` doesn’t exist or doesn’t belong to this session. - `409 CONFLICT`: replay of a previously-completed `idempotency_key` with a mismatched body. - `503 SERVICE_UNAVAILABLE`: payment provider rejected or timed out; safe to retry with the same key. ## POST /api/v1/payments/authorization Submit a payment authorization (e.g. 3DS challenge result, MoMo OTP) for an in-flight checkout. The `reference` comes from the original checkout response. ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/payments/authorization \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{ "reference": "ref_kjz…", "auth_type": "otp", "value": "847362" }' ``` ### Response ```json { "success": true, "data": { "reference": "ref_kjz…", "status": "approved", "next_action": null } } ``` - [**Next: Orders**](/docs/api-reference/orders) Look up the order, poll payment status, fire cancellations. - [**FX**](/docs/api-reference/fx) Lock a quote before checkout to honour a specific rate. --- # FX URL: /docs/api-reference/fx > Indicative FX rates and lockable cross-currency quotes. Pass the resulting `quote_id` as `fx_quote_id` on `/checkout` to settle in a non-cart currency at the locked rate. ## GET /api/v1/fx/rate Indicative mid-market rate between two ISO 4217 currencies. Useful for display only; it’s not binding. To pay in a specific currency at a specific rate, lock a quote first. ### Query parameters | Param | Description | | --- | --- | | `from` | 3-letter source currency. Required. | | `to` | 3-letter destination currency. Required. | ### Request ```bash title="cURL" curl "https://api.cimplify.io/api/v1/fx/rate?from=USD&to=GHS" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "from": "USD", "to": "GHS", "rate": "12.4150", "as_of": "2026-05-07T18:30:00Z", "source": "indicative" } } ``` ## POST /api/v1/fx/quotes Lock a rate for a specific amount and TTL. The returned `quote_id` is honoured on `/checkout` via the `fx_quote_id` field. `amount` accepts a decimal string or number. ### Body | Field | Description | | --- | --- | | `from` | 3-letter source currency. | | `to` | 3-letter destination currency. | | `amount` | Amount in source currency, > 0. String or number. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/fx/quotes \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Idempotency-Key: 5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" \ -H "Content-Type: application/json" \ -d '{ "from": "USD", "to": "GHS", "amount": "100.00" }' ``` ### Response ```json { "success": true, "data": { "quote_id": "fxq_01H…", "from": "USD", "to": "GHS", "amount": "100.00", "rate": "12.4150", "converted_amount": "1241.50", "expires_at": "2026-05-07T18:35:00Z" } } ``` ### Errors - `400 VALIDATION_ERROR`: non-3-letter currency code, `amount ≤ 0`. - `503 SERVICE_UNAVAILABLE`: rate provider temporarily unreachable. - [**Checkout**](/docs/api-reference/checkout) Pass `fx_quote_id` to charge at the locked rate. --- # Inventory URL: /docs/api-reference/inventory > Stock and availability lookups, scoped per location. Inventory is tracked at either the product or the variant level depending on the product’s `inventory_type`. ## GET /api/v1/inventory/products/\{product_id\}/stock Returns the on-hand stock for a product across one or more locations. Pass `?location_id=…` to scope to a single location. ### Request ```bash title="cURL" curl "https://api.cimplify.io/api/v1/inventory/products/prod_abc123/stock?location_id=loc_01H…" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "product_id": "prod_abc123", "inventory_type": "tracked", "locations": [ { "location_id": "loc_01H…", "quantity_on_hand": 42, "quantity_reserved": 3, "quantity_available": 39 } ] } } ``` ## GET /api/v1/inventory/variants/\{variant_id\}/stock Stock for a single variant. Same response shape as the product endpoint, with `variant_id` in place of `product_id`. ```bash title="cURL" curl "https://api.cimplify.io/api/v1/inventory/variants/var_double/stock?location_id=loc_01H…" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/inventory/products/\{product_id\}/availability Boolean check: can we sell `quantity` of this product at `location_id` right now? Pass `?quantity=…` (required) and optionally `?location_id=…`. ### Request ```bash title="cURL" curl "https://api.cimplify.io/api/v1/inventory/products/prod_abc123/availability?quantity=2&location_id=loc_01H…" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "product_id": "prod_abc123", "is_available": true, "quantity_requested": 2, "quantity_available": 39 } } ``` ## GET /api/v1/inventory/variants/\{variant_id\}/availability Same shape as the product availability endpoint, scoped to a variant. ```bash title="cURL" curl "https://api.cimplify.io/api/v1/inventory/variants/var_double/availability?quantity=1&location_id=loc_01H…" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## Inventory types | Type | Meaning | | --- | --- | | `none` | No tracking; always available. | | `tracked` | Tracked at the product level. | | `variant_level` | Tracked per variant; product-level call returns `null` totals. | - [**Variants**](/docs/api-reference/catalogue/variants) Read variants and their attributes from the catalogue. - [**Cart**](/docs/api-reference/cart) Adding to cart triggers a separate availability check. --- # Link URL: /docs/api-reference/link > /v1/link/*: customer-scoped Link API. Hosted on api.cimplify.io, separate from the per-business storefront API. The Cimplify Link API is **customer-scoped** and **cross-business**: saved addresses, saved mobile money, and account preferences belong to a customer, not a business. It runs on its own host (`api.cimplify.io`) on a different path prefix than the storefront API. | Surface | Host | Path prefix | |---|---|---| | Storefront | `storefronts.cimplify.io` | `/api/v1/*` | | **Link** | **`api.cimplify.io`** | **`/v1/link/*`** | Authentication is via a customer session token (`Authorization: Bearer …`), established by the OTP flow at `/v1/link/auth/*`. ## Conventions - **POST for updates.** `POST /v1/link//:id` updates an existing record with a partial body. Both create and update share the verb; payload distinguishes them. - **Response envelope**: same as the storefront API: `{ success: true, data: … }` on 2xx, `{ success: false, error: { code, message, retryable } }` on errors. - **Idempotency**: pass `Idempotency-Key: ` on writes that must replay safely. ## Auth ### POST `/v1/link/auth/request-otp` ```bash curl -X POST https://api.cimplify.io/v1/link/auth/request-otp \ -H "Content-Type: application/json" \ -d '{"contact":"+233244000000","contact_type":"phone"}' ``` ```json { "success": true, "message": "OTP sent" } ``` ### POST `/v1/link/auth/verify-otp` ```bash curl -X POST https://api.cimplify.io/v1/link/auth/verify-otp \ -H "Content-Type: application/json" \ -d '{ "contact":"+233244000000", "contact_type":"phone", "otp_code":"123456" }' ``` ```json { "success": true, "session_token": "lnk_…", "refresh_token": "rfr_…", "account_id": "acc_…", "customer_id": "cus_…", "message": "Verified" } ``` Carry `Authorization: Bearer lnk_…` on every subsequent call. ### POST `/v1/link/auth/logout` ```bash curl -X POST https://api.cimplify.io/v1/link/auth/logout \ -H "Authorization: Bearer lnk_…" ``` ## Profile + data ### GET `/v1/link/data` The one-shot read most callers want: customer + addresses + mobile money + preferences in one trip. ```bash curl https://api.cimplify.io/v1/link/data \ -H "Authorization: Bearer lnk_…" ``` ```json { "customer": { "id":"cus_…", "name":"Jane Doe", "email":"jane@example.com", "phone":"+233244000000", ... }, "addresses": [ ... ], "mobile_money": [ ... ], "preferences": { ... }, "default_address": { ... } | null, "default_mobile_money": { ... } | null } ``` ### POST `/v1/link/check-status` Look up whether a contact is already enrolled in Link, before asking for OTP. ```bash curl -X POST https://api.cimplify.io/v1/link/check-status \ -H "Content-Type: application/json" \ -d '{"contact":"+233244000000"}' ``` ```json { "is_link_customer": true } ``` ## Enrollment ### POST `/v1/link/enroll` Idempotency-keyed. Once enrolled, calling again returns the existing enrollment. ```bash curl -X POST https://api.cimplify.io/v1/link/enroll \ -H "Authorization: Bearer lnk_…" \ -H "Idempotency-Key: 91f3…" \ -d '{"contact":"+233244000000","name":"Jane Doe"}' ``` ### POST `/v1/link/enroll-and-link-order` Atomic enrollment + order attachment. Useful in a guest-checkout flow that wants to upgrade the customer in the same transaction as the order finalization. ```bash curl -X POST https://api.cimplify.io/v1/link/enroll-and-link-order \ -H "Authorization: Bearer lnk_…" \ -d '{ "order_id": "ord_…", "business_id": "bus_…", "address": { "label":"Home", "street_address":"…", "city":"Accra", "region":"GA" }, "mobile_money": { "phone_number":"+233244000000", "provider":"mtn", "label":"My MTN" }, "order_type": "delivery" }' ``` ## Preferences ### GET `/v1/link/preferences` Returns `NOT_ENROLLED` if the customer hasn't enrolled. ### POST `/v1/link/preferences` Partial update. Sends only the fields you want to change. ```bash curl -X POST https://api.cimplify.io/v1/link/preferences \ -H "Authorization: Bearer lnk_…" \ -d '{ "preferred_order_type":"delivery", "default_address_id":"addr_…", "notify_on_order": true }' ``` ## Addresses ### GET `/v1/link/addresses` Returns an array of `CustomerAddress`. ### POST `/v1/link/addresses` ```bash curl -X POST https://api.cimplify.io/v1/link/addresses \ -H "Authorization: Bearer lnk_…" \ -H "Idempotency-Key: 4a82…" \ -d '{ "label":"Home", "street_address":"12 Independence Ave", "apartment":"Flat 3", "city":"Accra", "region":"Greater Accra", "country":"GH" }' ``` The first address you create is marked `is_default: true` automatically. ### POST `/v1/link/addresses/:id` Partial update. ### DELETE `/v1/link/addresses/:id` ### POST `/v1/link/addresses/:id/default` Promote to default. Server clears `is_default` on all other addresses atomically. ### POST `/v1/link/addresses/:id/track-usage` Increment `usage_count` and stamp `last_used_at`. Used by checkout to surface "frequently used" addresses. ## Mobile money ### GET `/v1/link/mobile-money` Array of `CustomerMobileMoney`. ### POST `/v1/link/mobile-money` ```bash curl -X POST https://api.cimplify.io/v1/link/mobile-money \ -H "Authorization: Bearer lnk_…" \ -d '{ "phone_number":"+233244000000", "provider":"telecel", "label":"My main account" }' ``` `provider` accepts: `mtn`, `vodafone`, `telecel`, `airtel`, `airteltigo`, `mpesa` (plus a few legacy spellings). The server normalises to the short codes Paystack expects (`mtn` / `vod` / `atl` / `mpesa`) when constructing payment requests. ### DELETE `/v1/link/mobile-money/:id` ### POST `/v1/link/mobile-money/:id/default` ### POST `/v1/link/mobile-money/:id/track-usage` ### POST `/v1/link/mobile-money/:id/verify` Run the provider's verification flow to confirm the phone number is reachable on the chosen network. ## Sessions ### GET `/v1/link/sessions` Array of `LinkSession` records. ### DELETE `/v1/link/sessions` Revoke every session for this customer. Returns `{ success: true, revoked: }`. ### DELETE `/v1/link/sessions/:id` Revoke one specific session. ## Element-iframe variants `/v1/link/elements/*` is a parallel auth surface for the Cimplify Link iframes hosted at `link.cimplify.io`. It exists because cross-origin iframes can't use cookies the same way the SDK does; Elements use a postMessage-based token bootstrap and explicit `refresh`. Storefront-side SDK code uses the regular `/v1/link/auth/*` flow. | | `/v1/link/auth/*` | `/v1/link/elements/*` | |---|---|---| | Used by | `@cimplify/sdk` `client.link.*` | `link.cimplify.io/elements/*` iframes | | Auth | `Authorization: Bearer` | postMessage init + explicit refresh | | Refresh | Implicit (cookies) | `POST /v1/link/elements/refresh` | ## Contract snapshots All Link wire shapes are emitted as JSON Schema by the backend contract pipeline. The TS-side companion in `packages/sdk/scripts/diff-contracts.ts` compares against the SDK's zod-derived schemas to catch drift at PR time. Covered types: - `CustomerAddress`, `CustomerMobileMoney`, `CustomerLinkPreferences`, `LinkData`, `LinkCustomer` - `RequestOtpRequest`, `VerifyOtpRequest`, `LinkAuthResponse` - `CreateAddressRequest`, `UpdateAddressRequest`, `CreateMobileMoneyRequest` - `EnrollRequest`, `EnrollAndLinkOrderRequest`, `UpdatePreferencesRequest` ## Related - [SDK: client.link](/docs/sdk/link): the typed TS surface - [Cimplify Link overview](/docs/link): the embedded iframe Elements - [Testing: createTestClient](/docs/testing/test-client): Link mock parity --- # Orders URL: /docs/api-reference/orders > Read and manage orders created by checkout. Authenticated customers get their own orders; guests can access individual orders by passing the order’s `bill_token` as a query parameter. ## GET /api/v1/orders List orders for the authenticated customer. Supports `status`, `limit`, `offset`, and `location_id` filters. Requires a customer session. ### Request ```bash title="cURL" curl "https://api.cimplify.io/api/v1/orders?status=confirmed&limit=20" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": [ { "id": "ord_01H…", "order_number": "ORD-1284", "status": "confirmed", "order_type": "delivery", "items": [ { "product_id": "prod_abc123", "product_name": "Espresso", "variant_name": "Double", "quantity": 2, "unit_price": "6.00", "total": "12.00" } ], "subtotal": "12.00", "tax_amount": "1.06", "total_price": "13.06", "currency": "GHS", "customer_name": "Ama Mensah", "created_at": "2026-05-07T16:42:18Z" } ] } ``` ## GET /api/v1/orders/\{order_id\} Fetch one order. Authenticated customers are matched against the order’s `customer_id`; guests must pass the `token` query parameter (the `bill_token` from the checkout response). On a mismatch the server returns `404 NOT_FOUND` (never `403`) to avoid leaking order existence. ### Request (guest) ```bash title="cURL" curl "https://api.cimplify.io/api/v1/orders/ord_01H…?token=bt_n3k…" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Request (authenticated) ```bash title="cURL" curl https://api.cimplify.io/api/v1/orders/ord_01H… \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/orders/\{order_id\}/payment-status Poll the live payment status for an order. Same access rules as `GET /orders/:id`; guests must include `?token=…`. ```bash title="cURL" curl "https://api.cimplify.io/api/v1/orders/ord_01H…/payment-status?token=bt_n3k…" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "order_id": "ord_01H…", "payment_status": "succeeded", "payment_reference": "ref_kjz…", "amount": "13.06", "currency": "GHS", "settled_at": "2026-05-07T16:43:02Z" } } ``` ## POST /api/v1/orders/\{order_id\}/cancel Cancel an order. Honours `Idempotency-Key`. Optional `reason` body (max 500 chars). Guests must include `?token=…`. ```bash title="cURL" curl -X POST "https://api.cimplify.io/api/v1/orders/ord_01H…/cancel?token=bt_n3k…" \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Idempotency-Key: 5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" \ -H "Content-Type: application/json" \ -d '{"reason": "ordered the wrong item"}' ``` ## POST /api/v1/orders/\{order_id\}/customer Attach an authenticated customer to a previously-guest order. Useful for “guest-to-account” flows where the visitor signs up after checkout. Body: `{ customer_id }`. ```bash title="cURL" curl -X POST "https://api.cimplify.io/api/v1/orders/ord_01H…/customer?token=bt_n3k…" \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"customer_id": "cus_01H…"}' ``` ## POST /api/v1/orders/\{order_id\}/verify-payment Re-verify the payment with the underlying provider, e.g. after a redirect-flow handoff. Returns the canonical payment status. ```bash title="cURL" curl -X POST "https://api.cimplify.io/api/v1/orders/ord_01H…/verify-payment?token=bt_n3k…" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## Order statuses | Status | Description | | --- | --- | | `pending` | Awaiting confirmation / payment to settle. | | `confirmed` | Confirmed by the business. | | `preparing` | In the kitchen / fulfilment queue. | | `ready` | Ready for pickup or out for delivery. | | `completed` | Fulfilled. | | `cancelled` | Cancelled by customer or business. | - [**Checkout**](/docs/api-reference/checkout) Where orders, and the `bill_token`, come from. - [**Webhooks**](/docs/api-reference/webhooks) React to order lifecycle events server-side. --- # Places URL: /docs/api-reference/places > Address autocomplete and place lookup, fronted by Google Places. Reuse a `sessionToken` across consecutive autocomplete calls and the final details call to keep billing on a single Places session. The session field is intentionally `sessionToken` (camelCase) on both endpoints. The server renames it from `session_token` via `#[serde(rename = "sessionToken")]`. ## POST /api/v1/places/autocomplete Predictions for a partial address. Inputs shorter than 3 characters short-circuit to an empty `predictions` array without hitting the upstream provider. ### Body | Field | Type | Description | | --- | --- | --- | | `input` | string | User-typed query. Required. | | `sessionToken` | string | Caller-generated UUID; reuse it for the matching `/details` call. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/places/autocomplete \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{ "input": "12 Independence", "sessionToken": "5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" }' ``` ### Response ```json { "success": true, "data": { "predictions": [ { "place_id": "ChIJ…", "description": "12 Independence Avenue, Accra, Ghana", "main_text": "12 Independence Avenue", "secondary_text": "Accra, Ghana" } ] } } ``` ## POST /api/v1/places/details Resolve a `place_id` to a structured address, lat/lng, and viewport. Pass the same `sessionToken` used for the autocomplete call to share billing on a single Places session. ### Body | Field | Type | Description | | --- | --- | --- | | `place_id` | string | From `predictions[].place_id`. Required. | | `sessionToken` | string | Optional. Same token as the autocomplete request. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/places/details \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{ "place_id": "ChIJ…", "sessionToken": "5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" }' ``` ### Response ```json { "success": true, "data": { "place_id": "ChIJ…", "formatted_address": "12 Independence Avenue, Accra, Ghana", "components": { "street_address": "12 Independence Avenue", "city": "Accra", "region": "Greater Accra", "country": "GH", "postal_code": null }, "location": { "lat": 5.5557, "lng": -0.1963 } } } ``` ## Errors - `500 INTERNAL`: Places integration not configured for this environment (no Google credentials). - `503 SERVICE_UNAVAILABLE`: upstream Places returned an error. - [**Checkout**](/docs/api-reference/checkout) Drop the resolved address into `address_info` on the checkout body. --- # Scheduling URL: /docs/api-reference/scheduling > Surface available time slots, create bookings via the cart, and manage existing bookings. As of 0.44.30 slot and availability lookups are **variant-aware**: pass `variant_id` when a service has per-variant duration, buffer, or capacity overrides. `duration_minutes` was removed from `GET /scheduling/slots` in 0.44.30. The slot duration is derived from the service (or its variant). Pass `variant_id` to opt into variant-specific durations. ## GET /api/v1/scheduling/services List all bookable services for the active business. ```bash title="cURL" curl https://api.cimplify.io/api/v1/scheduling/services \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/scheduling/services/\{service_id\} Get a single service with its variants, duration defaults, capacity, staff, and resource bindings. ```bash title="cURL" curl https://api.cimplify.io/api/v1/scheduling/services/svc_01H… \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/scheduling/slots Return all bookable slots on a single calendar day. Pass `variant_id` to honour per-variant duration / buffer / capacity overrides. ### Query parameters | Param | Type | Description | | --- | --- | --- | | `service_id` | string | Required. | | `date` | string | ISO date `YYYY-MM-DD`. Required. | | `variant_id` | string | Variant override. Optional. | | `participant_count` | integer | Group size. Defaults to 1. | | `location_id` | string | Required when service has multiple locations. | ### Request ```bash title="cURL" curl "https://api.cimplify.io/api/v1/scheduling/slots?service_id=svc_01H…&date=2026-05-09&variant_id=var_60min" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "service_id": "svc_01H…", "date": "2026-05-09", "duration_minutes": 60, "slots": [ { "start": "2026-05-09T09:00:00Z", "end": "2026-05-09T10:00:00Z", "is_available": true, "remaining_capacity": 2 }, { "start": "2026-05-09T10:00:00Z", "end": "2026-05-09T11:00:00Z", "is_available": false, "remaining_capacity": 0 } ] } } ``` ## GET /api/v1/scheduling/slots/check Verify a single slot before adding the booking to cart. Useful after the user clicks “Reserve” to catch races against other customers. ### Query parameters | Param | Description | | --- | --- | | `service_id` | Required. | | `slot_time` | ISO 8601 datetime. Required. | | `duration_minutes` | Optional ad-hoc override (still accepted on this endpoint). | | `participant_count` | Optional, defaults to 1. | ```bash title="cURL" curl "https://api.cimplify.io/api/v1/scheduling/slots/check?service_id=svc_01H…&slot_time=2026-05-09T09:00:00Z" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/scheduling/availability Day-level availability map across a date range. Use it to render a month calendar with bookable / blocked days. Like `/slots`, accepts `variant_id`. ### Query parameters | Param | Description | | --- | --- | | `service_id` | Required. | | `start_date` | ISO date. Required. | | `end_date` | ISO date. Required. | | `variant_id` | Variant override. Optional. | | `location_id` | Optional. | | `participant_count` | Optional, defaults to 1. | ```bash title="cURL" curl "https://api.cimplify.io/api/v1/scheduling/availability?service_id=svc_01H…&start_date=2026-05-01&end_date=2026-05-31&variant_id=var_60min" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/scheduling/bookings List the authenticated customer’s bookings (past and upcoming). ```bash title="cURL" curl https://api.cimplify.io/api/v1/scheduling/bookings \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/scheduling/bookings/\{booking_id\} Fetch one booking by ID. ```bash title="cURL" curl https://api.cimplify.io/api/v1/scheduling/bookings/bk_01H… \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## POST /api/v1/scheduling/bookings/\{booking_id\}/cancel Cancel a booking. Idempotent via `Idempotency-Key`. Optional `reason` body. ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/scheduling/bookings/bk_01H…/cancel \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Idempotency-Key: 5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" \ -H "Content-Type: application/json" \ -d '{"reason": "schedule conflict"}' ``` ## POST /api/v1/scheduling/bookings/reschedule Move a booking to a new time. The booking is identified by the order it lives on (`order_id` + `line_item_id`). ### Body | Field | Description | | --- | --- | | `order_id` | Required. | | `line_item_id` | Required. | | `new_start_time` | ISO 8601 datetime. Required. | | `new_end_time` | ISO 8601 datetime. Required. | | `new_staff_id` | Optional. | | `reason` | Optional free text. | | `reschedule_type` | Optional `customer`, `business`, or `system`. | ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/scheduling/bookings/reschedule \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{ "order_id": "ord_01H…", "line_item_id": "li_01H…", "new_start_time": "2026-05-10T14:00:00Z", "new_end_time": "2026-05-10T15:00:00Z", "reason": "customer requested later slot" }' ``` - [**Cart**](/docs/api-reference/cart) Add a service line item with `scheduled_start` / `scheduled_end`. - [**Orders**](/docs/api-reference/orders) Bookings live as line items on the order they were created from. --- # Subscriptions URL: /docs/api-reference/subscriptions > Read and manage the authenticated customer’s subscriptions. All endpoints require an active customer session (cookie set by `/auth/verify-otp`). ## GET /api/v1/subscriptions List subscriptions belonging to the authenticated customer (max 100). ### Request ```bash title="cURL" curl https://api.cimplify.io/api/v1/subscriptions \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": [ { "id": "sub_01H…", "customer_id": "cus_01H…", "status": "active", "billing_plan_id": "bp_monthly", "current_period_start": "2026-05-01T00:00:00Z", "current_period_end": "2026-06-01T00:00:00Z", "cancel_at_period_end": false, "amount": "29.99", "currency": "GHS" } ] } ``` ## GET /api/v1/subscriptions/\{subscription_id\} Subscription detail with the underlying billing plan, items, and the next renewal preview. ```bash title="cURL" curl https://api.cimplify.io/api/v1/subscriptions/sub_01H… \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "subscription": { "id": "sub_01H…", "status": "active", "amount": "29.99" }, "plan": { "id": "bp_monthly", "name": "Monthly", "interval": "month" }, "items": [ { "product_id": "prod_box", "quantity": 1, "unit_price": "29.99" } ], "next_invoice": { "scheduled_at": "2026-06-01T00:00:00Z", "amount_due": "29.99" } } } ``` ## POST /api/v1/subscriptions/\{subscription_id\}/cancel Cancel the subscription at the end of the current period. Optional `reason` in the body (max 500 chars). Returns an empty success envelope. ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/subscriptions/sub_01H…/cancel \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"reason": "switching plans"}' ``` ## POST /api/v1/subscriptions/\{subscription_id\}/pause Pause renewals indefinitely. The current period continues until its end. ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/subscriptions/sub_01H…/pause \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## POST /api/v1/subscriptions/\{subscription_id\}/resume Resume a paused subscription on its existing schedule. ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/subscriptions/sub_01H…/resume \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## POST /api/v1/subscriptions/\{subscription_id\}/skip Skip the next renewal cycle. Subsequent renewals carry on as scheduled. ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/subscriptions/sub_01H…/skip \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Idempotency-Key: 5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" ``` ## Errors - `401 UNAUTHORIZED`: no customer session. - `404 NOT_FOUND`: subscription doesn’t exist or belongs to a different customer (returned in both cases to avoid existence leaks). - [**Auth**](/docs/api-reference/auth) Get a customer session via OTP. - [**Orders**](/docs/api-reference/orders) Each renewal materialises a new order. --- # Support URL: /docs/api-reference/support > Customer-facing support / chat-widget API. Each session has exactly one widget conversation; the server resolves it from the session identity, so callers never have to track a `conversation_id`. ## POST /api/v1/support/conversation Find or create the customer’s widget conversation. Returns the conversation metadata plus the most recent 50 messages. ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/support/conversation \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ### Response ```json { "success": true, "data": { "conversation": { "id": "thr_01H…", "status": "open", "ai_enabled": true, "message_count": 4, "created_at": "2026-05-07T16:21:00Z", "last_message_at": "2026-05-07T16:42:18Z" }, "messages": [ { "id": "msg_01H…", "sender_type": "customer", "content": "Hi, where is my order?", "content_type": "text", "attachments": [], "metadata": {}, "created_at": "2026-05-07T16:21:00Z" } ] } } ``` ## POST /api/v1/support/conversation/messages Send a message as the customer. The server resolves the thread by widget identity from the session, so the body is **just** `{ content, client_id? }`; no thread / conversation ID required. ### Body | Field | Type | Description | | --- | --- | --- | | `content` | string | Message text. 1–4000 chars after trim. | | `client_id` | string | Widget-supplied idempotency key. Replaying it returns 409. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/support/conversation/messages \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{ "content": "Has my order shipped yet?", "client_id": "msg-client-01H…" }' ``` ### Response ```json { "success": true, "data": { "id": "msg_01H…", "sender_type": "customer", "content": "Has my order shipped yet?", "content_type": "text", "attachments": [], "metadata": {}, "created_at": "2026-05-07T16:42:18Z" } } ``` ### Errors - `400 BAD_REQUEST`: empty `content` after trimming, or longer than 4000 chars. - `409 CONFLICT`: same `client_id` already accepted on this thread. ## GET /api/v1/support/conversation/messages Poll for messages. Use the `after` ISO timestamp as a cursor; `limit` caps at 100 (default 50). Returns messages in ascending timestamp order. ### Query parameters | Param | Description | | --- | --- | | `after` | ISO 8601 timestamp; only newer messages are returned. | | `limit` | 1–100, default 50. | ```bash title="cURL" curl "https://api.cimplify.io/api/v1/support/conversation/messages?after=2026-05-07T16:42:00Z&limit=50" \ -H "X-Public-Key: pk_test_your_publishable_key" ``` ## GET /api/v1/support/conversation/ws WebSocket upgrade for real-time messages. The server sends a `resync` frame on connect with `latest_message_at`; clients compare against their local high-water mark and call `GET /messages?after=…` if behind. Pings are exchanged every 30 seconds; the server closes idle clients after 90 seconds without a pong. - [**Auth**](/docs/api-reference/auth) Authenticated customers get a stable thread keyed to their account, not just the anonymous session. - [**Uploads**](/docs/api-reference/uploads) Attach files to messages by uploading first, then referencing the URL. --- # Uploads URL: /docs/api-reference/uploads > Two-step presigned upload flow. `init` returns a short-lived URL the client PUTs the file bytes to directly; `confirm` finalises the upload and returns the canonical CDN URL. ## POST /api/v1/uploads/init Reserve an upload slot. Honours `Idempotency-Key`; replaying the same key returns the original `upload_url` instead of provisioning a new one. ### Body | Field | Type | Description | | --- | --- | --- | | `filename` | string | Original filename, max 500 chars. | | `content_type` | string | MIME type, max 255 chars. | | `size_bytes` | integer | Declared file size in bytes. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/uploads/init \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Idempotency-Key: 5b1f9d80-2c70-4f2c-bf61-a0e6fa6b02a1" \ -H "Content-Type: application/json" \ -d '{ "filename": "receipt.pdf", "content_type": "application/pdf", "size_bytes": 184321 }' ``` ### Response ```json { "data": { "upload_id": "up_01H…", "upload_url": "https://uploads.cimplify.io/p/01H…?signature=…", "expires_in_secs": 600 } } ``` PUT the file bytes to `upload_url` with the same `Content-Type` declared above. The URL is single-use and expires. ### Step 2: PUT the bytes ```bash title="cURL" curl -X PUT "https://uploads.cimplify.io/p/01H…?signature=…" \ -H "Content-Type: application/pdf" \ --data-binary @receipt.pdf ``` ## POST /api/v1/uploads/confirm Finalise the upload. The server checks that the bytes landed and returns the canonical record. ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/uploads/confirm \ -H "X-Public-Key: pk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"upload_id": "up_01H…"}' ``` ### Response ```json { "data": { "id": "up_01H…", "url": "https://cdn.cimplify.io/up/01H…/receipt.pdf", "filename": "receipt.pdf", "content_type": "application/pdf", "size_bytes": 184321 } } ``` ## Errors - `400 VALIDATION_ERROR`: filename or content_type exceeds limits, `size_bytes` negative. - `400 BAD_REQUEST`: `confirm` called before the bytes arrived. - `404 NOT_FOUND`: `upload_id` doesn’t exist or has expired. - [**Support**](/docs/api-reference/support) Reference uploaded file URLs in chat messages. --- # Webhooks URL: /docs/api-reference/webhooks > Subscribe a server endpoint to lifecycle events emitted by Cimplify. Webhook administration is a server-side surface; all calls require a secret API key (`sk_…`) on `X-API-Key`. ## POST /api/v1/webhooks Register a new endpoint. The response includes a `secret`; persist it immediately, it is shown only once and is required for signature verification. ### Body | Field | Type | Description | | --- | --- | --- | | `url` | string | HTTPS endpoint URL. Required. | | `events` | string[] | Event types to subscribe to. Required. | | `description` | string | Optional human-readable label. | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/api/v1/webhooks \ -H "X-API-Key: sk_test_your_secret_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhooks/cimplify", "events": ["order.created", "order.completed", "payment.completed"] }' ``` ### Response ```json { "success": true, "data": { "id": "whk_01H…", "url": "https://your-server.com/webhooks/cimplify", "events": ["order.created", "order.completed", "payment.completed"], "secret": "whsec_xyz789…", "status": "active", "created_at": "2026-05-07T10:30:00Z" } } ``` ## GET /api/v1/webhooks List all webhooks configured for the calling business. ```bash title="cURL" curl https://api.cimplify.io/api/v1/webhooks \ -H "X-API-Key: sk_test_your_secret_key" ``` ## DELETE /api/v1/webhooks/\{webhook_id\} Remove a webhook subscription. Idempotent. ```bash title="cURL" curl -X DELETE https://api.cimplify.io/api/v1/webhooks/whk_01H… \ -H "X-API-Key: sk_test_your_secret_key" ``` ## Event types | Event | Description | | --- | --- | | `order.created` | New order placed via checkout. | | `order.updated` | Order status or line items changed. | | `order.completed` | Order fulfilled. | | `order.cancelled` | Order cancelled. | | `payment.completed` | Payment settled. | | `payment.failed` | Payment provider returned a failure. | | `payment.refunded` | Refund processed. | | `booking.created` | Service booking added to an order. | | `booking.rescheduled` | Booking moved to a new time. | | `booking.cancelled` | Booking cancelled. | | `subscription.renewed` | Subscription renewal succeeded. | | `subscription.cancelled` | Subscription cancelled. | | `inventory.low_stock` | Stock dropped below the configured threshold. | ## Payload format Webhooks are `POST` requests with a JSON body. The `type` matches the subscription event name; `data` contains a snapshot of the affected resource. ```json { "id": "evt_01H…", "type": "order.created", "created_at": "2026-05-07T16:42:18Z", "data": { "id": "ord_01H…", "order_number": "ORD-1284", "status": "pending", "total_price": "13.06", "currency": "GHS" } } ``` ## Signature verification Every webhook includes an `X-Cimplify-Signature` header. It is the hex-encoded HMAC-SHA256 of the raw request body using the `secret` returned at registration. Verify in constant time. ```typescript import crypto from 'crypto' export function verifyWebhook(payload: string, signature: string, secret: string): boolean { const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex') const sig = Buffer.from(signature) const exp = Buffer.from(expected) return sig.length === exp.length && crypto.timingSafeEqual(sig, exp) } ``` ## Retries Cimplify retries non-2xx responses with exponential backoff (1m, 5m, 15m, 1h, 6h, 24h) for up to 24 hours. Make your handler idempotent; the same `evt_…` id may arrive more than once. - [**Orders**](/docs/api-reference/orders) Source of `order.*` events. - [**Scheduling**](/docs/api-reference/scheduling) Source of `booking.*` events. --- # Appearance API URL: /docs/checkout/appearance > Every Cimplify Element iframe (`AuthElement`, `CheckoutElement`, `AccountElement`, the hosted Pay page) accepts an `appearance` object that controls theme, accent color, font, and corner radius. The same shape works across all integration tiers. ## The type Defined as `ElementAppearance` in `@cimplify/sdk`: ```ts interface ElementAppearance { theme?: "light" | "dark"; variables?: { primaryColor?: string; // any CSS color fontFamily?: string; // CSS font-family value borderRadius?: string; // CSS length, e.g. "0.5rem" }; } ``` ## Defaults From `packages/link/src/lib/appearance.ts`: | Field | Default | | --- | --- | | `theme` | `"light"` | | `primaryColor` | `#059669` (Emerald 600) | | `fontFamily` | `"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` | | `borderRadius` | `0.85rem` | ## Validation The Element runs values through `CSS.supports()` before applying them; invalid values silently fall back to the default rather than rendering broken styles. Specifically: - `primaryColor` must be a valid CSS color (`CSS.supports("color", value)`); otherwise the default emerald is used. - `borderRadius` must be a valid CSS length (`CSS.supports("border-radius", value)`); otherwise `0.85rem`. - `fontFamily` is rejected if it's longer than 160 chars, blank, or contains `;`, `{`, or `}` (basic CSS-injection guard). - Any value outside `"light"` | `"dark"` is treated as `"light"`. ## Setting it ### React **Memoize the appearance object.** Iframe options are locked in at mount; passing a new reference triggers a console warning and is ignored to avoid remount churn. ```tsx import { useMemo } from "react"; import { CimplifyCheckout } from "@cimplify/sdk/react"; export function Checkout() { const appearance = useMemo( () => ({ theme: "dark" as const, variables: { primaryColor: "#7c3aed", // violet-600 fontFamily: "'Geist', sans-serif", borderRadius: "0.5rem", }, }), [], ); return ( { /* … */ }} /> ); } ``` ### Vanilla / Elements ```ts const elements = createElements(client, businessId, { appearance: { theme: "dark", variables: { primaryColor: "#7c3aed", borderRadius: "0.5rem", }, }, }); ``` ### Embedded iframe (raw) Pass it inside the `init` postMessage: ```ts iframe.contentWindow!.postMessage({ type: "init", businessId, publicKey, appearance: { theme: "light", variables: { primaryColor: "#059669", borderRadius: "0.85rem" }, }, }, "https://link.cimplify.io"); ``` ### Hosted Pay session Pass it on session create; Cimplify stores it and applies it when the page loads: ```json { "cart_id": "crt_…", "appearance": { "theme": "light", "variables": { "primaryColor": "#0a2540" } } } ``` ## CSS variables exposed Internally, the appearance is resolved into CSS custom properties on the iframe's root. The full set (for reference, not direct customization): | Variable | Driven by | | --- | --- | | `--cimplify-primary-color` | `variables.primaryColor` | | `--cimplify-primary-color-alpha` | 10% alpha mix of the primary | | `--cimplify-border-radius` | `variables.borderRadius` | | `--cimplify-bg` / `--cimplify-text` | theme + sane defaults | | `--cimplify-surface` / `--cimplify-border` | theme | | `--cimplify-error` / `--cimplify-warning` | fixed | These are bridged into shadcn / Tailwind tokens (`--primary`, `--background`, `--ring`, etc.) so the iframe's entire UI re-themes from a single primary color. ## Limits - You can't inject custom CSS or class names into the iframe. - You can't reorder or hide individual fields; for that level of control, use [headless checkout](/docs/checkout/headless). - Font files have to be reachable from the customer's browser (i.e. system font stack or a webfont served from your own CDN with permissive CORS). ## Next - [**Controlled Elements**](/docs/checkout/elements): React surface that consumes `appearance` - [**Headless checkout**](/docs/checkout/headless): Bring your own theming --- # Drop-in checkout (hosted Pay) URL: /docs/checkout/drop-in > The fastest path to a paid order: create a checkout session on your server, then redirect the customer to the URL it returns. Cimplify hosts the entire checkout UI at `pay.cimplify.io/s/` (auth, address, payment method, compliance). You get a webhook (or success-URL redirect) when payment lands. ## Flow ```text 1. Customer adds items to cart on your storefront. 2. Your server hits POST /v1/checkout/sessions with the cart_id and a secret key. 3. The API returns { id, url, status, expires_at }. 4. You 302 the customer to `url` (or open it in a popup). 5. Customer completes payment on pay.cimplify.io. 6. Cimplify redirects them to your success_url with ?order_id=... &session_id=... AND fires order.completed / payment.succeeded webhooks. ``` ## Create a session Authenticate with a **secret** key (`sk_…`). The session is bound to whatever business that key belongs to. ```bash title="cURL" curl https://api.cimplify.io/v1/checkout/sessions \ -H "Authorization: Bearer $CIMPLIFY_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "cart_id": "crt_01J5...", "public_key": "pk_live_…", "success_url": "https://store.example.com/orders/thanks", "cancel_url": "https://store.example.com/cart", "default_order_type": "delivery", "submit_label": "Pay GH₵29.99" }' ``` ## Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `cart_id` | string | yes | An existing cart belonging to the same business as the API key. | | `public_key` | string | no | The `pk_…` key to embed in the hosted page; defaults to the business's primary public key. | | `order_types` | string[] | no | Subset of `delivery \| pickup \| dine_in`. Defaults to whatever the business supports. | | `default_order_type` | string | no | Pre-select an order type. | | `currency` | string | no | ISO 4217. Defaults to the cart currency. | | `success_url` | string | no | Cimplify redirects here on success with `order_id` and `session_id` query params. | | `cancel_url` | string | no | Shown as a "Return to store" button on the hosted page. | | `appearance` | object | no | [ElementAppearance](/docs/checkout/appearance). | | `submit_label` | string | no | Override the Pay button copy. | | `metadata` | object | no | Free-form JSON echoed on the resulting order. | ## Response ```json { "id": "cs_01J5BGM...", "url": "https://pay.cimplify.io/s/cs_01J5BGM...", "status": "open", "expires_at": "2026-05-07T17:00:00Z" } ``` ## Redirect the customer ```ts title="Next.js Route Handler" // app/api/checkout/route.ts import { redirect } from "next/navigation"; export async function POST(request: Request) { const { cartId } = await request.json(); const r = await fetch("https://api.cimplify.io/v1/checkout/sessions", { method: "POST", headers: { Authorization: `Bearer ${process.env.CIMPLIFY_SECRET_KEY!}`, "Content-Type": "application/json", }, body: JSON.stringify({ cart_id: cartId, success_url: `${process.env.STORE_URL}/orders/thanks`, cancel_url: `${process.env.STORE_URL}/cart`, }), }); if (!r.ok) { return Response.json({ error: await r.text() }, { status: 500 }); } const { url } = await r.json() as { url: string }; redirect(url); } ``` ## Reading the success redirect Cimplify appends `order_id` and `session_id` to your `success_url`. Treat the redirect as a UI hint only; the source of truth is the webhook. Don't fulfill orders from the redirect alone. ```ts title="app/orders/thanks/page.tsx" export default async function ThanksPage({ searchParams, }: { searchParams: Promise<{ order_id?: string; session_id?: string }>; }) { const { order_id } = await searchParams; if (!order_id) return

Awaiting confirmation…

; const r = await getServerClient().orders.get(order_id); if (!r.ok) return

Order not found.

; return

Thanks! Order #{r.value.order_number}

; } ``` ## Webhooks Wire the `order.completed` and `payment.succeeded` events to your fulfillment system. See [webhooks](/docs/concepts/webhooks) for signing and replay. ## Session status | Status | Meaning | | --- | --- | | `open` | Customer has not completed yet. URL is usable. | | `completed` | Payment captured; an order exists. | | `expired` | Past `expires_at`. Issue a new session. | ## Next - [**Pay sessions reference**](/docs/pay/sessions): Full API surface for the hosted product - [**Embedded iframe**](/docs/checkout/embedded): Keep the customer on your domain --- # Controlled Elements (React) URL: /docs/checkout/elements > React wrappers around the Cimplify Element iframes. Drop a component into your tree, pass a few props, and you have a working checkout. The iframe still does all the rendering; you just orchestrate it with React. ## Two ways in `@cimplify/sdk/react` ships two layers: - ``: a single component that renders the unified checkout iframe (auth + address + payment + submit) and exposes lifecycle callbacks. This is what `pay.cimplify.io` itself uses. - `` + piecewise `` / `` / ``: for layouts where you want auth above the fold and address/payment below, or want to interleave Cimplify-rendered fields with your own. ## Single-component checkout ```tsx import { CimplifyClient } from "@cimplify/sdk"; import { CimplifyCheckout } from "@cimplify/sdk/react"; const client = new CimplifyClient({ publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY!, credentials: "include", }); export function CheckoutScreen({ cartId }: { cartId: string }) { return ( { console.log(status, ctx.display_text); }} onComplete={(result) => { if (result.success) { window.location.assign(`/orders/${result.order!.id}`); } }} onError={(err) => console.error(err.code, err.message)} /> ); } ``` ## `CimplifyCheckout` props | Prop | Type | Required | Notes | | --- | --- | --- | --- | | `client` | `CimplifyClient` | yes | The same client you use elsewhere; the iframe inherits its public key. | | `businessId` | `string` | no | Auto-resolved from the public key when omitted. | | `cartId` | `string` | no | Auto-resolved via `client.cart.get()` when omitted. | | `locationId` | `string` | no | Pin the order to a specific store location. | | `orderTypes` | `("delivery" \| "pickup" \| "dine_in")[]` | no | Defaults to `["pickup", "delivery"]`. | | `defaultOrderType` | same as above | no | Pre-selected order type. | | `submitLabel` | `string` | no | Override the Pay button copy. | | `enrollInLink` | `boolean` | no | Default `true`. Saves the customer's details for 1-click checkout next time. | | `appearance` | [`ElementAppearance`](/docs/checkout/appearance) | no | Memoize this; reference changes after mount are warned about and ignored. | | `linkUrl` | `string` | no | Override the Link host (development). | | `demoMode` | `boolean` | no | Skip the API; for screenshots and storybook. | | `onComplete` | `(result: ProcessCheckoutResult) => void` | yes | Fires once on terminal success/failure. | | `onStatusChange` | `(status, ctx) => void` | no | See [checkout lifecycle](/docs/concepts/checkout-lifecycle). | | `onError` | `(err: { code, message }) => void` | no | Non-terminal initialization or runtime errors. | ## Piecewise Elements Use `` when you want Auth, Address, and Payment to live in different parts of your layout. The provider creates a single `CimplifyElements` controller; child components share token and customer state through it. ```tsx import { ElementsProvider, AuthElement, AddressElement, PaymentElement, useCheckout, } from "@cimplify/sdk/react"; function CheckoutForm({ cartId }: { cartId: string }) { const { process, isLoading } = useCheckout(); return ( <> console.log("signed in", d.customerId)} onRequiresOtp={(d) => console.log("OTP sent to", d.contactMasked)} onError={(e) => console.error(e)} /> {/* … */}} /> {/* … */}} /> ); } export default function CheckoutScreen({ cartId }: { cartId: string }) { return ( ); } ``` ## Component reference ### `` | Prop | Type | Notes | | --- | --- | --- | | `prefillEmail` | `string` | Pre-fill the contact field. | | `onAuthenticated` | `(data: AuthenticatedData) => void` | Carries `token`, `accountId`, `customerId`, `customer`. | | `onRequiresOtp` | `(data: { contactMasked }) => void` | OTP dispatched. | | `onReady` / `onError` | `() => void` | Lifecycle hooks. | ### `` | Prop | Type | Notes | | --- | --- | --- | | `mode` | `"shipping" \| "billing"` | Default `shipping`. | | `onChange` | `({ address: AddressInfo, saveToLink }) => void` | Fires on edit and on saved-address selection. | ### `` | Prop | Type | Notes | | --- | --- | --- | | `amount` | `number` | Cents; drives display only. | | `currency` | `CurrencyCode` | Defaults to cart currency. | | `onChange` | `({ paymentMethod: PaymentMethodInfo, saveToLink }) => void` | | ## No `CheckoutElement` / `AccountElement` wrappers For the unified checkout iframe, use `` (it already wraps that route). For the Link account portal, mount the iframe directly; see [AccountElement](/docs/link/account-element). ## Hooks | Hook | Returns | | --- | --- | | `useElements()` | `CimplifyElements \| null`. The underlying controller. | | `useElementsReady()` | `boolean`. True once the businessId resolves. | | `useCheckout()` | `{ submit, process, isLoading }`. Submit fires `elements.submitCheckout`; process fires `elements.processCheckout`. | ## Next - [**Appearance API**](/docs/checkout/appearance): Theme the iframe - [**Vanilla Elements**](/docs/checkout/vanilla): Same controller without React --- # Embedded checkout iframe URL: /docs/checkout/embedded > The same checkout UI as the hosted Pay page, but you mount it as an iframe on your own domain. No SDK needed; just an ` ``` ## Init handshake The iframe posts `{ type: "ready", height }` as soon as it's interactive. Reply with an `init` message containing the public key, appearance, and order options. ```ts const iframe = document.getElementById("cimplify-checkout") as HTMLIFrameElement; const LINK_ORIGIN = "https://link.cimplify.io"; window.addEventListener("message", (event) => { if (event.origin !== LINK_ORIGIN) return; if (event.data?.type !== "ready") return; iframe.contentWindow!.postMessage({ type: "init", businessId: "biz_01J5…", publicKey: "pk_live_…", appearance: { theme: "light", variables: { primaryColor: "#059669", borderRadius: "0.85rem" }, }, orderTypes: ["delivery", "pickup"], defaultOrderType: "delivery", renderSubmitButton: true, submitLabel: "Pay GH₵29.99", }, LINK_ORIGIN); iframe.style.height = event.data.height + "px"; }); ``` ## Auto-resize The iframe emits `height_change` whenever its content resizes. Apply it verbatim. ```ts window.addEventListener("message", (event) => { if (event.origin !== LINK_ORIGIN) return; if (event.data?.type === "height_change") { iframe.style.height = event.data.height + "px"; } }); ``` ## Pushing the cart Send `set_cart` to render an order summary alongside the form (the embedded UI auto-switches to a two-column layout when a cart is present). ```ts iframe.contentWindow!.postMessage({ type: "set_cart", cart: { items: [ { name: "Jollof bowl", quantity: 1, unit_price: "29.99", total_price: "29.99", line_type: "simple", }, ], subtotal: "29.99", tax_amount: "0.00", total_discounts: "0.00", service_charge: "0.00", total: "29.99", currency: "GHS", }, }, LINK_ORIGIN); ``` ## Starting checkout With `renderSubmitButton: true` the iframe renders its own Pay button and emits `request_submit` when pressed. Reply with `process_checkout`. ```ts window.addEventListener("message", (event) => { if (event.origin !== LINK_ORIGIN) return; if (event.data?.type === "request_submit") { iframe.contentWindow!.postMessage({ type: "process_checkout", cart_id: cart.id, order_type: "delivery", enroll_in_link: true, }, LINK_ORIGIN); } if (event.data?.type === "checkout_complete") { if (event.data.success) { window.location.href = `/orders/${event.data.order.id}`; } else { console.error(event.data.error); } } }); ``` ## Security - Always check `event.origin === "https://link.cimplify.io"` before trusting a message. - Always send messages to `"https://link.cimplify.io"` as the second arg of `postMessage`, never `"*"`. - The iframe's `sandbox` attribute must include `allow-same-origin` (the iframe needs cookies for Link sessions) and `allow-popups` (some payment providers redirect via popup). - Don't set the `nonce` from a predictable value; use `crypto.randomUUID()`. ## When to graduate Once you're writing more than ~30 lines of postMessage glue, switch to [Controlled Elements](/docs/checkout/elements) (React) or [Vanilla Elements](/docs/checkout/vanilla) (any framework); the SDK's `CimplifyElements` controller handles all of the above for you. ## Next - [**Element events**](/docs/concepts/element-events): Full message catalog - [**Vanilla Elements**](/docs/checkout/vanilla): SDK-managed mount, no React --- # Headless checkout URL: /docs/checkout/headless > Drive the checkout API directly. No iframe, no Cimplify-rendered fields; every screen, every input, every microcopy is yours. Use this when the iframe's look or layout can't accommodate your brand, or when you're building a non-web surface (native app, kiosk, voice). ## Two starting points - **High-level:** `` from `@cimplify/sdk/react`, a full, opinionated checkout screen built from the regular React component primitives. Style it via Tailwind / classNames; ejectable. - **Fully custom:** Call `client.checkout.process(...)` from whatever UI you build. The SDK handles polling, status, and recovery; you handle every pixel. ## Using `` The fastest way to a fully-custom-looking checkout. `CheckoutPage` renders the whole flow as React components (no iframe), reads the cart from `useCart()`, and dispatches to `client.checkout.process` on submit. ```tsx import { CimplifyProvider, CheckoutPage } from "@cimplify/sdk/react"; import { createCimplifyClient } from "@cimplify/sdk"; const client = createCimplifyClient({ publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY!, }); export default function Checkout() { return ( ); } ``` Style it with Tailwind classes on the wrapping container, override sub-components via the registry (`cimplify add checkout-page` to eject), or skip it entirely and go fully custom. ## Fully custom Build whatever UI you want and call `client.checkout.process` on submit. The body is flat; see the [checkout SDK reference](/docs/sdk/checkout) for the full `CheckoutFormData` shape. ```tsx async function handleSubmit(form: MyFormState) { const cart = await client.cart.get(); if (!cart.ok) { setError(cart.error.message); return; } const r = await client.checkout.process({ cart_id: cart.value.id, order_type: form.orderType, // "delivery" | "pickup" | "dine_in" payment_method: form.paymentMethod, // "mobile_money" | "card" customer: { name: form.name, email: form.email, phone: form.phone, save_details: form.rememberMe, }, address_info: form.orderType === "delivery" ? { street_address: form.street, city: form.city, region: form.region, } : undefined, mobile_money_details: form.paymentMethod === "mobile_money" ? { phone_number: form.momoNumber, provider: form.momoProvider, } : undefined, special_instructions: form.notes, }); if (!r.ok) { setError(`${r.error.code}: ${r.error.message}`); return; } // r.value: { order_id, order_number, payment_status, requires_authorization, next_action, ... } if (r.value.requires_authorization) { // Show OTP / PIN screen, then call client.checkout.submitAuthorization(...) } else { router.push(`/orders/${r.value.order_id}`); } } ``` ## `next_action` The response carries a discriminated `next_action` describing what to do next: redirect to a 3DS / provider page, poll for status, or nothing. Branch on the type; the SDK reference covers each variant. ```ts switch (r.value.next_action?.type) { case "redirect": window.location.assign(r.value.next_action.url); break; case "poll": pollUntilDone(r.value.order_id); break; case "none": default: router.push(`/orders/${r.value.order_id}`); } ``` ## Polling For payment methods that settle asynchronously, use `client.checkout.pollPaymentStatus`. It re-queries the order until terminal state. ```ts async function pollUntilDone(orderId: string) { for (let i = 0; i < 30; i++) { const r = await client.checkout.pollPaymentStatus(orderId); if (!r.ok) throw new Error(r.error.message); if (r.value.payment_status === "success") return r.value; if (r.value.payment_status === "failed") throw new Error("Payment failed"); await new Promise(res => setTimeout(res, 2000)); } throw new Error("Timed out"); } ``` ## Trade-offs vs Elements | You get | You give up | | --- | --- | | Full visual / UX control. | You implement OTP, address validation, payment provider error handling. | | Native-app friendly (no iframe). | You handle PCI / OTP compliance scope. | | Server-side rendering / a11y / i18n on your terms. | No automatic theme via Appearance API. | | Custom telemetry hooks anywhere. | You wire up the full lifecycle UI. | ## Next - [**checkout SDK reference**](/docs/sdk/checkout): Every field on `CheckoutFormData` - [**Controlled Elements**](/docs/checkout/elements): When you want to keep the iframe --- # Vanilla Elements URL: /docs/checkout/vanilla > The framework-agnostic checkout surface. Use this from Vue, Svelte, plain HTML, or anywhere else React isn't the right tool. The same controller backs the React wrappers; they're just convenience around `CimplifyElements`. ## What ships From `@cimplify/sdk`: ```ts import { createCimplifyClient, createElements, // factory CimplifyElements, // the controller class CimplifyElement, // a single mounted element ELEMENT_TYPES, // "auth" | "address" | "payment" | "checkout" | "account" EVENT_TYPES, // "ready" | "authenticated" | "change" | … MESSAGE_TYPES, // postMessage type constants } from "@cimplify/sdk"; ``` ## Bootstrap ```ts const client = createCimplifyClient({ publicKey: "pk_live_…", }); // businessId is optional; the controller resolves it from the public key. const elements = createElements(client, "biz_01J5…", { appearance: { theme: "light", variables: { primaryColor: "#059669", borderRadius: "0.85rem" }, }, }); ``` ## Mounting an element Every element type lives at `link.cimplify.io/elements/`. The controller creates the iframe, hooks up postMessage, and routes events. ```ts const checkout = elements.create(ELEMENT_TYPES.CHECKOUT, { orderTypes: ["delivery", "pickup"], defaultOrderType: "delivery", submitLabel: "Pay GH₵29.99", }); checkout.on(EVENT_TYPES.READY, () => console.log("checkout iframe ready")); checkout.on(EVENT_TYPES.ERROR, (err) => console.error(err)); checkout.mount("#cimplify-checkout"); // selector or HTMLElement ``` ## Element types | Constant | Iframe path | What it renders | | --- | --- | --- | | `ELEMENT_TYPES.AUTH` | `/elements/auth` | Single-line OTP sign-in. | | `ELEMENT_TYPES.ADDRESS` | `/elements/address` | Address picker / form. | | `ELEMENT_TYPES.PAYMENT` | `/elements/payment` | Alias of `checkout`. | | `ELEMENT_TYPES.CHECKOUT` | `/elements/checkout` | Full unified checkout (auth + address + payment + submit). | | `ELEMENT_TYPES.ACCOUNT` | `/elements/account/*` | Logged-in account portal. | ## Event types Subscribe via `element.on(eventType, handler)`. `EVENT_TYPES` covers: | Event | Payload | Fires on | | --- | --- | --- | | `READY` | `{ height }` | iframe ready | | `AUTHENTICATED` | `AuthenticatedData` | OTP success (Auth) | | `REQUIRES_OTP` | `{ contactMasked }` | OTP dispatched (Auth) | | `CHANGE` | `{ address }` or `{ paymentMethod }` | field changes (Address / Payment) | | `ORDER_TYPE_CHANGED` | `{ orderType }` | delivery/pickup toggle (Checkout) | | `REQUEST_SUBMIT` | `{}` | in-iframe Pay button pressed | | `ERROR` | `{ code, message }` | any failure | ## Pushing the cart ```ts checkout.setCart({ items: [ { name: "Jollof bowl", quantity: 1, unit_price: "29.99", total_price: "29.99", line_type: "simple" }, ], subtotal: "29.99", tax_amount: "0.00", total_discounts: "0.00", service_charge: "0.00", total: "29.99", currency: "GHS", }); ``` ## Processing checkout `processCheckout` returns an `AbortablePromise`; call `.abort()` to cancel mid-flow. The promise settles only on terminal `checkout_complete` or timeout. ```ts const task = elements.processCheckout({ cart_id: cart.id, order_type: "delivery", enroll_in_link: true, on_status_change: (status, ctx) => { setStatusLine(ctx.display_text ?? status); }, }); // Cancel button: cancelBtn.addEventListener("click", () => task.abort()); const result = await task; if (result.success) { location.assign(`/orders/${result.order!.id}`); } else { console.error(result.error?.code, result.error?.message); } ``` ## In-iframe submit button When you create the element with no explicit `renderSubmitButton`, the iframe renders its own Pay button. Listen for `REQUEST_SUBMIT` and call `processCheckout`: ```ts checkout.on(EVENT_TYPES.REQUEST_SUBMIT, async () => { const result = await elements.processCheckout({ cart_id: cart.id, order_type: "delivery", }); // … }); ``` ## Cleanup Call `destroy()` when leaving the page or unmounting the host. This removes the iframe, clears all event handlers, and removes the postMessage listener. ```ts // On route leave: elements.destroy(); // tears down all elements at once // or per-element: checkout.destroy(); ``` ## Full example (vanilla TS) ```ts import { createCimplifyClient, createElements, ELEMENT_TYPES, EVENT_TYPES, } from "@cimplify/sdk"; const client = createCimplifyClient({ publicKey: "pk_live_…" }); const elements = createElements(client); const checkout = elements.create(ELEMENT_TYPES.CHECKOUT, { defaultOrderType: "delivery", }); checkout.on(EVENT_TYPES.READY, async () => { const cart = await client.cart.get(); if (cart.ok) { checkout.setCart(toCheckoutCart(cart.value)); } }); checkout.on(EVENT_TYPES.REQUEST_SUBMIT, async () => { const cart = await client.cart.get(); if (!cart.ok) return; const result = await elements.processCheckout({ cart_id: cart.value.id, order_type: "delivery", }); if (result.success) { location.assign(`/orders/${result.order!.id}`); } else { showError(result.error?.message ?? "Checkout failed"); } }); checkout.mount("#checkout-container"); // Later: window.addEventListener("beforeunload", () => elements.destroy()); ``` ## Next - [**React wrappers**](/docs/checkout/elements): Same controller, but as components - [**Element events**](/docs/concepts/element-events): The full postMessage protocol --- # Component registry URL: /docs/cli/components > 67 ejectable components ship in the registry. `cimplify list` shows every component; `cimplify add ` copies one's source into your project. After ejection it's yours; you stop receiving SDK updates for that component, in exchange for full control over JSX, state, and behavior. This is the **shadcn pattern**: registry-as-source-code, not registry-as-package. You own ejected components; bumping `@cimplify/sdk` no longer updates them. ## List ```bash # All ejectable components cimplify list # Filter (pipe to grep) cimplify list | grep selector cimplify list | grep card ``` ## Add ```bash # Eject a single component into ./components/ cimplify add cart-summary # → writes ./components/cart-summary.tsx cimplify add variant-selector cimplify add product-customizer ``` The CLI resolves the registry entry, copies its source (renaming imports as needed), and writes it under `components/`. Replace the SDK import in the page or layout that used the original component with the local copy. ## When to eject | Eject when… | Don't eject when… | | --- | --- | | `classNames` / `render*` overrides can't reach the part you need to change. | A class override or a per-element `render*` slot would do the job. | | You need merchant-specific logic that isn't generic enough to upstream. | You're tempted to monkey-patch; fork the SDK upstream instead. | | You're deeply restructuring (e.g. custom cart layout that breaks the SDK's contract). | You want SDK bug-fixes for free. | ## Example flow: restyle the cart drawer ```bash title="Eject" cimplify add cart-drawer # → ./components/cart-drawer.tsx ``` ```tsx title="components/providers.tsx (swap the import)" "use client"; import { CimplifyProvider } from "@cimplify/sdk/react"; import { CartDrawerProvider } from "@cimplify/sdk/react"; // import { CartDrawer } from "@cimplify/sdk/react"; // ← was import { CartDrawer } from "@/components/cart-drawer"; // ← now // ... ``` ```tsx title="components/cart-drawer.tsx (restyle freely)" // Edit the JSX, swap the close button, change the empty-state copy, // add a per-merchant "free shipping" banner, etc. The state machine, cart // hooks, and onCheckout contract still flow through the SDK; you only own // the rendering. "use client"; import { useCart, useCartDrawer } from "@cimplify/sdk/react"; // ... (the ejected source) ``` ## What's in the registry The registry mirrors the React SDK exports. Pages, customizers, cards, primitives: every non-trivial component has an entry. `cimplify list` is the source of truth; the table below groups the most-used ones. | Group | Registry entries | | --- | --- | | Pages | `catalogue-page`, `product-page`, `cart-page`, `checkout-page`, `search-page`, `collection-page`, `order-detail-page`, `order-history-page`, `deals-page`, `bookings-page`, `booking-page`, `account` | | Cart | `cart-drawer`, `cart-summary` | | Cards | `product-card`, `food-product-card`, `retail-product-card`, `wholesale-product-card`, `digital-product-card`, `standard-service-card`, `compact-service-card`, `schedule-service-card`, `rental-service-card`, `accommodation-card`, `lease-service-card`, `subscription-card`, `booking-card`, `booking-list` | | Layouts | `default-product-layout`, `food-product-layout`, `wholesale-product-layout`, `service-product-layout`, `digital-product-layout` | | Customizers | `product-customizer`, `variant-selector`, `add-on-selector`, `bundle-selector`, `composite-selector`, `billing-plan-selector`, `customer-input-fields` | | Filters & grids | `product-grid`, `category-grid`, `category-filter`, `search-input`, `store-nav`, `recently-viewed`, `recommendation-carousel`, `collection-page` | | Scheduling | `slot-picker`, `date-slot-picker`, `staff-picker`, `resource-picker`, `availability-badge` | | Primitives | `price`, `price-range`, `quantity-selector`, `deal-banner`, `sale-badge`, `discount-input`, `delivery-estimate`, `volume-pricing`, `location-picker`, `currency-selector`, `session-message-banner`, `product-image-gallery`, `product-sheet`, `order-summary`, `order-history`, `chat-widget` | ## Ejection golden rules 1. Read the ejected file end-to-end before editing. 2. Don't change the state shape or the cart payload contract; the customizer / cart pricing math has been iterated on heavily, and the mock will reject mismatched payloads. 3. Restyle via Tailwind classes / wrap nodes / add per-merchant copy. 4. Run `bun run check:cart` and `bun run check:contract` after. ## Where next - [**Component catalog**](/docs/sdk/react/components) Real names, props, examples. - [**Hooks reference**](/docs/sdk/react-hooks) Drive ejected components from data hooks. - [**CLI overview**](/docs/cli) Full subcommand index. --- # Deploys & projects URL: /docs/cli/deploy > Create a project, link your CWD to it, and ship. Every deploy is a git SHA + a build; rollbacks are just re-deploys of an older SHA. ## Projects ```bash # List projects in the linked business cimplify projects ls # Create a new project cimplify projects create my-store # Link CWD to a project cimplify link # Writes .cimplify/project.json (commit this file). # Unlink cimplify unlink ``` Linking writes `.cimplify/project.json` in the current directory. The file contains the project ID and the linked business; commit it so every developer / CI run deploys to the same project. ## Deploy ```bash # Preview deploy of the current branch HEAD cimplify deploy # Promote to production cimplify deploy --prod # Deploy a specific SHA (must already exist on the remote) cimplify deploy --ref 3f9a2b1 --no-push # Don't wait for the build (fire and forget) cimplify deploy --no-poll ``` ### Flags | Flag | Description | | --- | --- | | `--prod` | Deploy to production. Default is preview. | | `--ref ` | Override the git ref. Defaults to `HEAD` of the current branch. | | `--no-push` | Skip `git push`. Assumes the remote already has the SHA. | | `--allow-dirty` | Skip the dirty-tree confirmation prompt. | | `--no-poll` | Return after enqueue; don't stream build progress. | By default `cimplify deploy` pushes the current branch to the remote, enqueues a build, and streams its progress until the build terminates. The exit code reflects the build's terminal status. ## Rollback ```bash # List recent deploys cimplify status # Re-deploy a previous deployment's SHA cimplify rollback dpl_a1b2c3d4 ``` `rollback` takes a deployment ID, not a SHA. It enqueues a fresh build of the same SHA so you can roll forward again later if you need to. ## Status & logs ```bash # Latest deployment + state for the linked project cimplify status # Stream logs for the latest deployment cimplify logs --follow # Or scope to a specific deployment cimplify logs --deployment dpl_a1b2c3d4 ``` ### Logs flags | Flag | Description | | --- | --- | | `--deployment ` | Override which deployment to read. Defaults to the latest. | | `--follow` | Poll until the deployment terminates. | ## dev with remote env Run your project's `dev` script with the production env scope wired in. Useful for sanity-checking config drift before a production deploy. ```bash # Local dev with local .env.local cimplify dev # Local dev with production env vars cimplify dev --remote ``` ## Typical workflow ```bash title="From PR to production" # Working on a feature branch cimplify deploy # preview deploy from HEAD # After review: ship git checkout main && git pull cimplify deploy --prod # Something broke; roll back cimplify status # find the previous good deploy cimplify rollback dpl_ # Triage with logs cimplify logs --deployment dpl_ --follow ``` ## Where next - [**env**](/docs/cli/env) `push`, `pull`, scopes. - [**domains**](/docs/cli/domains) Add, verify, attach, primary. - [**login**](/docs/cli/login) Browser-first PKCE. - [**CLI overview**](/docs/cli) Full index. --- # cimplify doctor URL: /docs/cli/doctor > Run 14 pre-flight checks across your storefront and the linked project. Each row reports a verdict (`ok` / `warn` / `fail` / `skip`) and a fix hint. Exits `1` on any fail so CI can branch on `$?`. `doctor` is the action-oriented counterpart to [`introspect`](/docs/cli/introspect). Where `introspect` reports state, `doctor` issues verdicts: "your `cacheComponents` flag is missing — add it to `next.config.ts`", "your token is rejected — run `cimplify login` again." It's the right command to run before a deploy, in CI, or as the first step in an agent prompt that's about to mutate the project. ## Usage ```bash cimplify doctor # full health check (local + 2 remote pings) cimplify doctor --offline # skip the remote checks cimplify doctor --json # machine-readable envelope ``` ## What it checks ### Local (always run, ~ms) | Check | Fails when | Warns when | | --- | --- | --- | | `cli` | (never) | — | | `package_json` | no `./package.json` | — | | `node_modules` | missing | — | | `lockfile` | no `bun.lock(b)` and no other lockfile | a non-bun lockfile (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`) is present | | `sdk_pin` | `@cimplify/sdk` not in dependencies | pinned to `*` or `latest` | | `brand_file` | — | no `lib/brand.ts` in a project | | `next_config` | `cacheComponents` is explicitly `false` | `cacheComponents` is not set | | `globals_css` | — | `@theme` block missing | | `mock_seed` | — | no `cimplify-mock --seed ` in `scripts.dev` | | `git` | — | working tree is dirty (deploy hint) | | `auth_present` | no token file at `~/.config/cimplify/auth.json` | — | | `project_linked` | no `.cimplify/project.json` | — | ### Remote (skipped on `--offline`, ~1 s total) | Check | Fails when | | --- | --- | | `auth_valid` | `GET /v1/auth/me` returns 401 or the host is unreachable | | `project_resolves` | `GET /v1/businesses/{bid}/projects/{pid}` returns 404 | `project_resolves` skips automatically when `project_linked` failed or `auth_valid` failed — there's nothing useful to ask the server about. ## Human output ``` Cimplify doctor ✓ cli 0.2.9 ✓ package_json my-store ✓ node_modules installed ✓ lockfile bun.lock ✓ sdk_pin @cimplify/sdk ^0.45.4 ✓ brand_file lib/brand.ts · Currents Electronics ✓ next_config cacheComponents: true ✓ globals_css app/globals.css · @theme block present ✓ mock_seed seed: retail ! git working tree dirty on main → commit before `cimplify deploy` (or pass --allow-dirty) ✓ auth_present dev@example.com ✓ project_linked proj_abc ✓ auth_valid live token · business biz_xyz ✗ project_resolves project not found on server → run `cimplify projects ls` to see available projects, then `cimplify link ` 14 checks · 12 ok · 1 warn · 1 fail ``` Verdict glyphs: `✓` = ok, `!` = warn, `✗` = fail, `-` = skip. ## JSON envelope ```json { "ok": true, "data": { "summary": { "total": 14, "ok": 12, "warn": 1, "fail": 1, "skip": 0 }, "checks": [ { "name": "cli", "level": "ok", "message": "0.2.9" }, { "name": "git", "level": "warn", "message": "working tree dirty on main", "fix": "commit before `cimplify deploy` (or pass --allow-dirty)" }, { "name": "project_resolves", "level": "fail", "message": "project not found on server", "fix": "run `cimplify projects ls` to see available projects, then `cimplify link `" } ] } } ``` Every check has the same shape: `{ name, level: "ok" | "warn" | "fail" | "skip", message, fix? }`. The `fix` field is the actionable hint; agents should prefer it over the message when generating next steps. ## Exit codes | Condition | Exit | | --- | --- | | All checks `ok` / `warn` / `skip` | `0` | | Any check `fail` | `1` | Warnings never fail the run — they're advisory (e.g., a dirty git tree pre-deploy). Only `fail` rows block. For CI: `cimplify doctor --json --offline > doctor.json && jq '.data.summary.fail' doctor.json` returns the count of failures. ## Flags | Flag | Notes | | --- | --- | | `--offline` | Skip the two remote checks (`auth_valid`, `project_resolves`). Useful in CI sandboxes with no outbound network. | | `--json` | Single envelope on stdout; suppresses colorized output. | --- # Domains URL: /docs/cli/domains > Domains live at the **business** scope: add and verify them once, then attach them to one or more projects per env (`preview` or `production`). One verified domain can serve any project in your business. ## Add → verify → attach ```bash title="From zero to live" # 1. Register the domain at the business scope cimplify domains add shop.example.com # Output: DNS records to set at your registrar # 2. After setting DNS, trigger verification cimplify domains verify shop.example.com # 3. Attach to the linked project for production cimplify domains attach shop.example.com --env=production --primary ``` ## List ```bash # All business-scoped domains cimplify domains ls # Only domains attached to the linked project cimplify domains ls --attached ``` ## Add ```bash cimplify domains add shop.example.com ``` Registers the domain at the business scope and prints DNS records (usually a CNAME for a subdomain, or A / AAAA records for an apex). The domain isn't usable until you verify it. ## Verify ```bash cimplify domains verify shop.example.com ``` Triggers a re-check against the DNS records the domain was registered with. Run this after you've set the records at your registrar; propagation can take a few minutes. ## Attach ```bash # Bind a verified domain to the linked project for an env cimplify domains attach shop.example.com --env=production # Mark it primary at the same time cimplify domains attach shop.example.com --env=production --primary # Wire a preview env (e.g. preview.shop.example.com) cimplify domains attach preview.shop.example.com --env=preview ``` A domain attached to `--env=production` serves `cimplify deploy --prod` builds. Attaching to `--env=preview` binds the domain to preview deploys. ` --primary` sets the canonical host for that env (others redirect to it). ## Detach & primary ```bash # Detach from the linked project cimplify domains detach shop.example.com --env=production # Skip the confirmation prompt cimplify domains detach shop.example.com --env=production --yes # Promote an attached domain to primary for its env cimplify domains primary shop.example.com --env=production ``` ## Remove ```bash cimplify domains rm shop.example.com ``` Removes the domain from the business scope. Detach it from every project first. ## Subcommand reference | Command | Description | | --- | --- | | `domains ls [--attached]` | List domains. `--attached` filters to the linked project. | | `domains add ` | Register a domain; prints DNS records to set. | | `domains verify ` | Trigger verification after DNS is configured. | | `domains rm ` | Remove a domain from the business scope. | | `domains attach [--env=] [--primary]` | Bind a verified domain to the linked project for an env. | | `domains detach [--env=] [--yes]` | Unbind a domain from the linked project. | | `domains primary [--env=]` | Promote an attached domain to primary for its env. | ## DNS records `add` prints the records you need to create at your registrar. The exact set depends on whether you're attaching a subdomain or an apex. | Domain type | Records | | --- | --- | | `shop.example.com` (subdomain) | CNAME to the printed Cimplify edge host. | | `example.com` (apex) | A / AAAA records to the printed IPs (or ALIAS / ANAME if your registrar supports it). | | Verification | A TXT record at `_cimplify-challenge.`. | ## Where next - [**deploy**](/docs/cli/deploy) Promote with `--prod` after the domain is live. - [**env**](/docs/cli/env) Wire `NEXT_PUBLIC_SITE_URL` to your domain. --- # Env vars URL: /docs/cli/env > Manage environment variables for the linked project across three scopes: `development`, `preview`, `production`. Push your `.env.local` in one command, or mutate single keys with `add` / `rm`. ## Scopes | Scope | Used for | | --- | --- | | `development` | Local `cimplify dev --remote`. | | `preview` | Default for `cimplify deploy` (no `--prod`). Used for PR previews. | | `production` | Used by `cimplify deploy --prod`. | ## List ```bash # List vars in the preview scope (default) cimplify env ls # Pick a scope cimplify env ls --scope=production # Reveal non-secret values cimplify env ls --show ``` Secret values are redacted by default; `--show` reveals only public ones (those with `NEXT_PUBLIC_`-style names or marked `--public` on add). ## Pull (download) ```bash # Write public env vars to .env.local cimplify env pull # From a different scope, to a different file cimplify env pull --scope=production --out=.env.production.local ``` Only public vars are written. Secrets stay on the server. Use `pull` to bootstrap a new dev machine without copying values manually. ## Push (upload) ```bash # Push .env.local to the preview scope cimplify env push # To a specific scope cimplify env push --scope=production # From a different file cimplify env push --file=.env.production.local --scope=production # Replace the entire scope (deletes server-only vars) cimplify env push --scope=preview --overwrite ``` Without `--overwrite`, `push` upserts each var; keys missing from the file stay on the server. With `--overwrite`, the scope is replaced exactly. ## Add & rm ```bash # Add a public var cimplify env add NEXT_PUBLIC_SITE_URL=https://shop.example.com --public # Add a secret cimplify env add STRIPE_SECRET_KEY=sk_live_... --secret --scope=production # Remove (with confirmation) cimplify env rm STRIPE_SECRET_KEY --scope=production # Skip the prompt cimplify env rm STRIPE_SECRET_KEY --scope=production --yes ``` ## All flags | Flag | Applies to | Description | | --- | --- | --- | | `--scope ` | all | One of `development \| preview \| production`. | | `--show` | `ls` | Reveal non-secret values. | | `--out ` | `pull` | Output path. Default `.env.local`. | | `--file ` | `push` | Input path. Default `.env.local`. | | `--overwrite` | `push` | Replace the entire scope. Without it, `push` upserts. | | `--secret` | `add` | Mark `is_secret = true`. | | `--public` | `add` | Mark `is_secret = false`. | | `--yes` | `rm` | Skip confirmation. | ## Workflow ```bash title="Wire envs for a new project" # 1. Edit .env.local with the values you need # NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io # NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=pk_test_... # NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=biz_... # NEXT_PUBLIC_SITE_URL=https://preview-shop.example.com # 2. Push to preview cimplify env push --scope=preview # 3. Verify cimplify env ls --scope=preview --show # 4. Mirror to production with prod-specific overrides cimplify env push --scope=production cimplify env add NEXT_PUBLIC_SITE_URL=https://shop.example.com --public --scope=production ``` `.env.local` is gitignored by every Cimplify template. Don't commit secrets; push them to scopes instead. ## Where next - [**deploy**](/docs/cli/deploy) Push and ship after env push. - [**domains**](/docs/cli/domains) Wire a custom domain. - [**Environments**](/docs/concepts/environments) Test mode vs live mode. --- # cimplify explain URL: /docs/cli/explain > Print canonical guidance on Cimplify concepts — cart, products, bundles, composites, services, errors, exit codes — straight from the terminal. 20 topics, bundled into the CLI binary at build time, no network calls. `explain` is the offline, terse, agent-friendly counterpart to the docs site. Every topic body is a curated snapshot of the relevant `cimplify.dev` page — frontmatter and JSX stripped — bundled into the CLI binary at build time. No network calls, no rate limits, no broken-link risk. Use it mid-edit when you need the rule on one specific thing: "what's the cart payload?", "what does `INVALID_INPUT` mean?", "how do bundles differ from composites?". ## Usage ```bash cimplify explain # list every topic cimplify explain cart # print the cart guide cimplify explain bundles # print the bundles + composites section cimplify explain --json # topics list as JSON cimplify explain cart --json # full topic body in a JSON envelope ``` ## Topics Twenty topics ship in the binary, grouped: ### Storefront patterns | Topic | Covers | | --- | --- | | `brand` | Single source of truth for visible strings (`lib/brand.ts`) | | `eject` | When and how to eject SDK components into your project | ### Catalogue | Topic | Covers | | --- | --- | | `products` | Product shape, fetch by id or slug | | `variants` | Variant axes, picking by axis selections | | `bundles` | Fixed bundles **and** configurable composites (same section) | | `composites` | Alias of `bundles` | | `add-ons` | Product add-ons / customizations (gift wrap, engraving, …) | | `quotes` | Quote-first pricing — lock the price before add-to-cart | | `discounts` | Deals and promotional pricing | ### Transactional | Topic | Covers | | --- | --- | | `cart` | Flat add-to-cart payload + quote-then-add flow | | `checkout` | Flat `ProcessArgs` body for `/checkout`, payment methods | | `orders` | Order shape and status flow | | `subscriptions` | Recurring billing, plan changes, cancellation | ### Scheduling | Topic | Covers | | --- | --- | | `services` | Bookable services + variant-aware availability | | `bookings` | Slot lookup, creating bookings, reschedule and cancel | ### Surface conventions | Topic | Covers | | --- | --- | | `errors` | `CimplifyError` codes and remediation | | `uploads` | Presigned upload flow: init → PUT → confirm | | `exit-codes` | CLI exit codes (`NOT_LINKED=4`, `TIMEOUT=12`, …) | | `agent-prompts` | Copy-paste prompts for common Cimplify workflows | | `seeds` | Available mock seeds and how to switch between them | ## Output shape ### Human mode ``` cimplify explain bundles ## Bundles and composites **Bundles** are fixed product groupings with a single combined price. **Composites** are build-your-own: the customer picks one option per component and the price is computed from those selections. ```ts const bundles = await client.catalogue.getBundles() const bundle = await client.catalogue.getBundleById('bun_xxx') … ``` Source: https://cimplify.dev/docs/sdk/catalogue#bundles-and-composites (snapshot in cli-v0.2.9) ``` Every topic ends with a footer pointing to the canonical doc URL on `cimplify.dev` and the CLI version the snapshot was taken in. If the docs evolve, upgrade the CLI (`cimplify update`) to refresh the snapshots. ### JSON mode **No topic** — list every available topic: ```json { "ok": true, "data": { "topics": [ { "name": "brand", "description": "…", "source_url": "https://cimplify.dev/docs/templates/brand" }, { "name": "cart", "description": "…", "source_url": "https://cimplify.dev/docs/sdk/cart" } ] } } ``` **With a topic** — full body: ```json { "ok": true, "data": { "topic": "cart", "title": "Cart", "body": "## Add an item\n\nThe add-to-cart payload is **flat**…", "source_url": "https://cimplify.dev/docs/sdk/cart", "snapshot_version": "0.2.9" } } ``` ## Exit codes | Condition | Exit | | --- | --- | | Success (list or topic printed) | `0` | | Unknown topic | `9` (`NOT_FOUND`) | The `NOT_FOUND` error includes a `remediation` hint pointing back to `cimplify explain` (no args) so agents can self-correct. ## When to use what | Need | Tool | | --- | --- | | Quick rule mid-edit ("what's the cart shape?") | `cimplify explain cart` | | Discover live API surface | `cimplify explain` (list) | | Search the full docs corpus | the [MCP `cimplify_search_docs` tool](/docs/mcp) | | Read prose on `cimplify.dev` | browser / [llms.txt](/llms.txt) | `explain` is intentionally a fixed, curated set. Searching arbitrary doc paths from the CLI is deferred — use the MCP tool or fetch `https://cimplify.dev/llms.mdx` directly. --- # cimplify init URL: /docs/cli/init > Scaffold a working Next.js App Router storefront from one of seven industry templates. After `bun install`, `bun dev` boots the local mock API alongside Next, and you have a real shop in 60 seconds. ## Usage ```bash cimplify init [dir] [-t