# 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. Pass `cacheOptions: { revalidate: 3600, tags: [tags.collections()] }` to the SDK read (from `@cimplify/sdk/server`). Do NOT use `'use cache'` / `cacheTag` / `cacheLife`. Cimplify storefronts run on CF Workers where Cache Components is broken; use the ISR Previous Model. - 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`. It 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, so 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. ``` ## Add a hero image and reference it in the homepage ```text title="Upload brand assets → use them in source" Add the image at <./Downloads/hero.jpg> as the homepage hero for the storefront in <./my-store>. Reference: https://cimplify.dev/docs/cli/assets - `cp <./Downloads/hero.jpg> public/hero/main.jpg` - `cimplify assets upload public/hero/ --folder hero --json`: captures the manifest entry for `hero/main.jpg`. - Edit `app/page.tsx`: replace the hero `` `src` with `assetUrl("hero/main.jpg")` from `@cimplify/sdk`. - Do NOT touch existing `` calls that point at Cloudinary or other external hosts; the loader passes them through unchanged. - Run `bun run check`. Open `http://localhost:3000`; verify the hero image renders. - Commit `cimplify-assets.json` along with the source change so the manifest is reviewable. ``` ## Audit caching and asset delivery ```text title="Find caching and asset regressions on a live storefront" Audit the live storefront at for caching and asset-delivery regressions. Sources of truth: - https://cimplify.dev/docs/sdk/optimization (page + asset caching) - https://cimplify.dev/docs/templates/seo-and-resource-hints (responsive images) - Page cache: curl /, /shop, and a PDP 2–3× each; `X-Cimplify-Edge-Cache` must warm to HIT and `Cache-Control` must carry `s-maxage`. A BYPASS or missing `s-maxage` means the page lost its `revalidate` or carries a Cookie. - Asset cache: pull a `/_next/static/*` chunk; it must be `public, max-age=31536000, immutable`. - Images: flag raw `` from a third-party CDN, and Cloudinary URLs with no `/upload//` segment (full-resolution originals). - Report a table (route/asset, observed header, expected, severity). Do not change source unless asked. ``` ## 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 for which tools to call, what to echo, and 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 storefront APIs use `/api/v1/*` and JSON request bodies. Public browser calls use `cpk_*` keys; server calls use `csk_*` keys; customer-scoped calls attach bearer session tokens. ## Base URL | Environment | Base URL | | --- | --- | | `Production` | `https://storefronts.cimplify.io/api/v1` | | `Sandbox` | `https://storefronts.cimplify.io/api/v1` with a `cpk_test_...` or `csk_test_...` key | ## Authentication Identify your business and authorize the request with one of the following header combinations. | Header | Value | When to use | | --- | --- | --- | | `X-API-Key` | `cpk_live_…` / `cpk_test_…` | Browser, storefront, mobile. Read catalogue, manage cart, run checkout. | | `X-API-Key` | `csk_live_…` / `csk_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://storefronts.cimplify.io/api/v1/catalogue/products \ -H "X-API-Key: csk_test_your_secret_key" \ -H "X-Business-Id: biz_abc123" ``` ## Response envelope Storefront responses put the payload on `data`. Some platform endpoints also include `status` and `meta`. The SDK unwraps `data` and returns a typed `Result`. ```json { "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://storefronts.cimplify.io/api/v1/cart/items \ -H "X-API-Key: cpk_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 checkout endpoint. ## 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 | | [CimplifyCheckout (React)](/docs/checkout/elements) | Checkout iframe inside your React tree | 1 component, ~30 LOC | You stay on the React side of the iframe | | [Vanilla checkout](/docs/checkout/vanilla) | Checkout 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 checkout. Yes → Do you want Cimplify-rendered checkout UI? Yes → CimplifyCheckout (``). 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`; embedded checkout can use the active SDK cart session. - Server-side, every tier funnels into `POST /v1/checkout` via the same `CheckoutFormData` body. - [Appearance API](/docs/checkout/appearance) works the same way for hosted Pay and embedded checkout. ## What differs | Concern | Drop-in | Embedded checkout | 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` - [**CimplifyCheckout**](/docs/checkout/elements): React component that renders checkout --- # 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, with 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 repo provision / connect / info`](/docs/cli/repo) | Attach a managed or external git repo to the project. | | [`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 inspect`](/docs/cli/inspect) | Read-only snapshot of the storefront's catalogue + merchandising state via the public key. Designed for agents (`--json`). | | [`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. | ### Storefront assets | Command | What it does | | --- | --- | | [`cimplify assets upload `](/docs/cli/assets) | Upload brand/storefront images to the Cimplify CDN; writes `cimplify-assets.json`; idempotent by SHA-256. | | [`cimplify assets ls`](/docs/cli/assets) | List manifest entries with their stable CDN URLs. | | [`cimplify assets rm `](/docs/cli/assets) | Remove a manifest entry (manifest-only; remote blob stays in v1). | ## 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, embedded checkout, or full headless control. 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 addresses, payment methods, orders, checkout acceleration, and customer account APIs." 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 | `cpk_live_` | `csk_live_` | Real data and real payments | | Test | `cpk_test_` | `csk_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 Cimplify's shopper identity and saved-details layer: saved addresses, saved payment methods, cross-merchant sessions, and checkout acceleration. Cimplify Link gives shoppers a faster checkout across Cimplify-powered merchants. It is not a generic auth-as-a-service product; it is the customer identity and saved-details layer used by checkout. ## Product Surfaces - **Checkout acceleration**: checkout can recognize a verified Cimplify shopper and load saved addresses/payment methods after verification. - **Hosted OAuth**: `auth.cimplify.io` verifies shoppers for storefront sign-in and checkout. - **Direct API/SDK access**: `client.link.*` powers custom account pages and dashboard-style views in a merchant storefront. ## How Shoppers Join Link Enrollment happens when a shopper checks out and opts to remember their details. Later checkouts can use those saved details after the shopper verifies through Cimplify. Privacy invariant: saved details, account names, and membership-specific UI become available only after verification. ## How Merchants Opt In Merchants opt in by using a checkout integration that supports Cimplify Link: - [Drop-in checkout](/docs/checkout/drop-in) - [CimplifyCheckout for React](/docs/checkout/elements) - [Vanilla checkout controller](/docs/checkout/vanilla) - [Raw checkout iframe](/docs/checkout/embedded) Pass `enroll_in_link: false` to checkout processing only when the merchant does not want to offer saved details for future 1-click checkout. ## Authentication Model There are two auth paths. Pick the one that matches the product surface: | Surface | Host | Used by | What happens | | --- | --- | --- | --- | | Storefront OAuth | `auth.cimplify.io` | `CimplifySignInButton`, `startSignIn`, checkout sign-in | Hosted passkey/OTP/consent. Returns an OAuth code, then the merchant callback sets storefront session cookies. | | Direct Link API | `api.cimplify.io/v1/link/auth/*` | Cimplify-hosted customer surfaces, custom account surfaces, SDK `client.link.*` | Request/verify OTP directly and receive Link access/refresh tokens. | For merchant storefront checkout, use Storefront OAuth. The checkout component collects the contact inline, sends that contact as a login hint, and loads saved details only after Cimplify verifies the shopper. ## Smallest Checkout Embed ```tsx import { CimplifyClient } from "@cimplify/sdk"; import { CimplifyCheckout } from "@cimplify/sdk/react"; const client = new CimplifyClient({ publicKey: "cpk_live_…", credentials: "include", }); ``` Also add the OAuth callback routes from [Sign in with Cimplify](/docs/sdk/auth#route-handlers). Those routes complete Cimplify saved-details sign-in during checkout. The shopper dashboard at `link.cimplify.io` is a Cimplify-hosted customer product, not a merchant integration surface. Merchant account pages should use SDK-rendered account components backed by storefront auth and `client.link.*`. ## Next - [**CheckoutElement**](/docs/link/checkout-element): The checkout iframe - [**SDK: Link**](/docs/sdk/link): Direct `client.link.*` API --- # 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 → git 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; contact capture, saved-details sign-in, address, and payment selection live in one checkout 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` | Signs in to Cimplify Link, then 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. **Sign in with Cimplify** is pre-wired too: the account button and `/auth/*` routes ship in the template. Shoppers sign in with a quick redirect and land back signed in. See [Sign in with Cimplify](/docs/sdk/auth). ### 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: csk_…` for secret keys or `X-API-Key: cpk_…` for client keys. ```bash title="cURL" curl https://storefronts.cimplify.io/api/v1/catalogue/products \ -H "X-API-Key: cpk_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 - [**Sign in with Cimplify**](/docs/sdk/auth) Pre-wired redirect sign-in: passkey or one-time code, cross-storefront SSO - [**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 (`csk_…`) 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 on the ISR Previous Model (`cacheComponents` off, `export const revalidate` per page, `cacheOptions` on SDK reads) - `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/ # SDK-rendered account pages │ ├── 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 + 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 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 API keys plus bearer customer sessions where needed. - **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://storefronts.cimplify.io/api/v1/activity/events \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/activity/state \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/activity/recommendations?limit=12" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/activity/messages/dismiss \ -H "X-API-Key: cpk_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. --- # Storefront Auth URL: /docs/api-reference/auth > Raw customer OTP endpoints on the storefront API. New storefronts should prefer SDK OAuth sign-in; these endpoints remain available for direct API integrations. Use the SDK flow in [Sign in with Cimplify](/docs/sdk/auth) for normal storefront sign-in and embedded checkout. This page documents the raw `/api/v1/auth/*` endpoints exposed by the storefront API. All endpoints use the storefront response envelope: ```json { "data": { "...": "..." } } ``` ## GET `/api/v1/auth/status` Returns the authenticated customer for the active bearer session. ```bash title="cURL" curl https://storefronts.cimplify.io/api/v1/auth/status \ -H "X-API-Key: cpk_test_your_publishable_key" \ -H "Authorization: Bearer " ``` ```json { "data": { "is_authenticated": true, "customer": { "id": "cus_01H...", "name": "Ama Mensah", "email": "ama@example.com", "phone": "+233241234567" }, "session_expires_at": "2026-05-31T12:00:00Z" } } ``` Guest sessions return `is_authenticated: false` and omit `customer`. ## POST `/api/v1/auth/request-otp` Send a one-time code to a phone number or email. | Field | Type | Description | | --- | --- | --- | | `contact` | string | E.164 phone number or email. | | `contact_type` | `"phone"` or `"email"` | Optional. Defaults to `phone` when omitted. | ```bash title="cURL" curl -X POST https://storefronts.cimplify.io/api/v1/auth/request-otp \ -H "X-API-Key: cpk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{"contact": "+233241234567", "contact_type": "phone"}' ``` ```json { "data": { "message": "OTP sent to +233****4567", "expires_in": 300, "is_new_account": false } } ``` ## POST `/api/v1/auth/verify-otp` Exchange the code for a customer session token. The SDK stores `session_token` with `client.setAccessToken(...)` and sends it as `Authorization: Bearer ...` on later customer-scoped calls. | Field | Type | Description | | --- | --- | --- | | `contact` | string | Same contact used for `request-otp`. | | `otp_code` | string | 4-6 character one-time code. | | `contact_type` | `"phone"` or `"email"` | Optional. Defaults to `phone` when omitted. | ```bash title="cURL" curl -X POST https://storefronts.cimplify.io/api/v1/auth/verify-otp \ -H "X-API-Key: cpk_test_your_publishable_key" \ -H "Content-Type: application/json" \ -d '{ "contact": "+233241234567", "contact_type": "phone", "otp_code": "847362" }' ``` ```json { "data": { "message": "Successfully authenticated", "account_id": "acc_01H...", "session_token": "eyJ...", "refresh_token": "eyJ...", "customer": { "id": "cus_01H...", "name": "Ama Mensah", "email": "ama@example.com", "phone": "+233241234567" } } } ``` ### Errors | HTTP | Code | Meaning | | --- | --- | --- | | `400` | `VALIDATION_ERROR` | Contact or `otp_code` failed validation. | | `401` | `INVALID_OTP` | Code expired, mismatched, or already redeemed. | | `429` | `RATE_LIMITED` | Too many attempts. | ## POST `/api/v1/auth/logout` Clear the local session in the caller. Direct API clients should discard the current bearer token when they receive `action: "discard_token"`. ```bash title="cURL" curl -X POST https://storefronts.cimplify.io/api/v1/auth/logout \ -H "X-API-Key: cpk_test_your_publishable_key" \ -H "Authorization: Bearer " ``` ```json { "data": { "message": "Successfully logged out", "action": "discard_token" } } ``` ## POST `/api/v1/auth/profile` Update one or more fields on the authenticated customer profile. | Field | Type | Description | | --- | --- | --- | | `name` | string | Optional display name. | | `email` | string | Optional email. | | `phone` | string | Optional phone. | ```bash title="cURL" curl -X POST https://storefronts.cimplify.io/api/v1/auth/profile \ -H "X-API-Key: cpk_test_your_publishable_key" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"name": "Ama N. Mensah"}' ``` ```json { "data": { "success": true, "message": "Profile updated successfully" } } ``` ## Related - [**SDK auth**](/docs/sdk/auth): OAuth sign-in and callback routes - [**Link API**](/docs/api-reference/link): Customer-scoped saved details - [**Orders**](/docs/api-reference/orders): Orders for the authenticated 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://storefronts.cimplify.io/api/v1/cart \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/cart \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/cart/items \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/cart/items \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/cart/items/ci_01H… \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/cart/items/ci_01H… \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/cart/coupons \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/cart/coupons/current \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/checkout \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/payments/authorization \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/fx/rate?from=USD&to=GHS" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/fx/quotes \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/inventory/products/prod_abc123/stock?location_id=loc_01H…" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/inventory/variants/var_double/stock?location_id=loc_01H…" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/inventory/products/prod_abc123/availability?quantity=2&location_id=loc_01H…" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/inventory/variants/var_double/availability?quantity=1&location_id=loc_01H…" \ -H "X-API-Key: cpk_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 API URL: /docs/api-reference/link > /v1/link/*: customer-scoped Link API for saved addresses, saved mobile money, preferences, sessions, and cross-merchant account data. The Cimplify Link API is customer-scoped and cross-business. Saved addresses, saved mobile money, preferences, sessions, and Link account data belong to the customer, then checkout scopes what it displays to the current merchant. | Surface | Host | Path prefix | | --- | --- | --- | | Storefront API | `storefronts.cimplify.io` | `/api/v1/*` | | Link API | `api.cimplify.io` | `/v1/link/*` | `@cimplify/sdk` talks to this host through `client.link.*`. ## Conventions - Authenticated calls use `Authorization: Bearer `. - Update routes use `POST /v1/link//:id` with a partial body. - Mutating routes accept `Idempotency-Key`. - Raw responses use the platform envelope: ```json { "data": { "...": "..." }, "meta": { "timestamp": "2026-05-31T12:00:00Z" }, "status": { "code": 200, "success": true } } ``` The SDK unwraps `data` and returns `Result`. ## 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 { "data": { "success": true, "message": "OTP sent to +233****0000", "expires_in": 300, "is_new_account": false }, "status": { "code": 200, "success": true } } ``` ### 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 { "data": { "success": true, "session_token": "eyJ...", "refresh_token": "eyJ...", "account_id": "acc_...", "customer_id": "cus_...", "name": "Jane Doe", "email": "jane@example.com", "phone": "+233244000000", "message": "Login successful" }, "status": { "code": 200, "success": true } } ``` `verify-otp` also sets the HttpOnly `cim_session` refresh cookie for `.cimplify.io`. Carry `session_token` as a bearer token on API calls. ### POST `/v1/link/auth/refresh` Refresh rotates the long-lived Link session and returns a new bearer token. It accepts the refresh credential from either source: - `cim_session` cookie, for browser calls from Cimplify-owned domains using `credentials: "include"`. - JSON body `{ "session_token": "" }`, for server-side workers and explicit SDK calls. ```bash curl -X POST https://api.cimplify.io/v1/link/auth/refresh \ -H "Content-Type: application/json" \ -d '{"session_token":"eyJ_refresh..."}' ``` The response shape matches `verify-otp` and includes a rotated `refresh_token`. ### POST `/v1/link/auth/logout` ```bash curl -X POST https://api.cimplify.io/v1/link/auth/logout \ -H "Authorization: Bearer eyJ..." ``` ```json { "data": { "success": true, "message": "Logged out successfully", "action": "discard_token" }, "status": { "code": 200, "success": true } } ``` ## Profile and Data ### GET `/v1/link/data` The one-shot read most callers want: customer, addresses, mobile money, preferences, and defaults. ```bash curl https://api.cimplify.io/v1/link/data \ -H "Authorization: Bearer eyJ..." ``` ```json { "data": { "customer": { "id": "cus_...", "name": "Jane Doe", "email": "jane@example.com", "phone": "+233244000000" }, "addresses": [], "mobile_money": [], "preferences": {}, "default_address": null, "default_mobile_money": null }, "status": { "code": 200, "success": true } } ``` ### GET `/v1/link/profile` Returns the current Link customer profile. ### POST `/v1/link/profile` Updates one or more profile fields and returns the updated customer. ```bash curl -X POST https://api.cimplify.io/v1/link/profile \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{"name":"Jane A. Doe"}' ``` ### POST `/v1/link/check-status` Returns `{ "is_link_customer": boolean }` for a submitted contact. Use this in explicit Link account flows after the shopper enters a contact. Embedded checkout uses OAuth `login_hint` and loads saved details after verification. ## 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 eyJ..." \ -H "Idempotency-Key: 91f3..." \ -H "Content-Type: application/json" \ -d '{"contact":"+233244000000","name":"Jane Doe"}' ``` ### POST `/v1/link/enroll-and-link-order` Atomic enrollment plus order attachment. ```bash curl -X POST https://api.cimplify.io/v1/link/enroll-and-link-order \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -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 | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/v1/link/preferences` | Read preferences. | | `POST` | `/v1/link/preferences` | Partially update preferences. | ```bash curl -X POST https://api.cimplify.io/v1/link/preferences \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{ "preferred_order_type":"delivery", "default_address_id":"addr_...", "notify_on_order": true }' ``` ## Addresses | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/v1/link/addresses` | List saved addresses. | | `POST` | `/v1/link/addresses` | Create an address. | | `POST` | `/v1/link/addresses/:id` | Partially update an address. | | `DELETE` | `/v1/link/addresses/:id` | Delete an address. | | `POST` | `/v1/link/addresses/:id/default` | Promote to default. | | `POST` | `/v1/link/addresses/:id/track-usage` | Increment usage metadata. | ```bash curl -X POST https://api.cimplify.io/v1/link/addresses \ -H "Authorization: Bearer eyJ..." \ -H "Idempotency-Key: 4a82..." \ -H "Content-Type: application/json" \ -d '{ "label":"Home", "street_address":"12 Independence Ave", "apartment":"Flat 3", "city":"Accra", "region":"Greater Accra", "country":"GH" }' ``` ## Mobile Money | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/v1/link/mobile-money` | List saved mobile-money methods. | | `POST` | `/v1/link/mobile-money` | Create a saved method. | | `DELETE` | `/v1/link/mobile-money/:id` | Delete a saved method. | | `POST` | `/v1/link/mobile-money/:id/default` | Promote to default. | | `POST` | `/v1/link/mobile-money/:id/track-usage` | Increment usage metadata. | | `POST` | `/v1/link/mobile-money/:id/verify` | Run provider verification. | ```bash curl -X POST https://api.cimplify.io/v1/link/mobile-money \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{ "phone_number":"+233244000000", "provider":"telecel", "label":"My main account" }' ``` `provider` accepts `mtn`, `vodafone`, `telecel`, `airtel`, `airteltigo`, and `mpesa`. ## Orders | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/v1/link/orders` | List the customer's Link-visible orders. | | `GET` | `/v1/link/orders/:order_id` | Read one order. | Both routes accept optional `business_id` query scope for merchant-embedded account views. ## Sessions | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/v1/link/sessions` | List active sessions. | | `DELETE` | `/v1/link/sessions` | Revoke every session. | | `DELETE` | `/v1/link/sessions/:id` | Revoke one session. | ## Checkout Cleanup Checkout authenticates shoppers through Storefront OAuth. The Link API keeps one first-party cleanup route for clearing Link-owned session state when a checkout or dashboard flow signs out: | Method | Path | Purpose | | --- | --- | --- | | `POST` | `/v1/link/elements/logout` | Clear the Cimplify-owned `cim_session` cookie and revoke the supplied refresh token when present. | ## Related - [SDK: client.link](/docs/sdk/link): typed TypeScript surface - [Cimplify Link overview](/docs/link): product surfaces and auth model - [Checkout events](/docs/concepts/element-events): raw checkout iframe protocol --- # 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://storefronts.cimplify.io/api/v1/orders?status=confirmed&limit=20" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/orders/ord_01H…?token=bt_n3k…" \ -H "X-API-Key: cpk_test_your_publishable_key" ``` ### Request (authenticated) ```bash title="cURL" curl https://storefronts.cimplify.io/api/v1/orders/ord_01H… \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/orders/ord_01H…/payment-status?token=bt_n3k…" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/orders/ord_01H…/cancel?token=bt_n3k…" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/orders/ord_01H…/customer?token=bt_n3k…" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/orders/ord_01H…/verify-payment?token=bt_n3k…" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/places/autocomplete \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/places/details \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/scheduling/services \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/scheduling/services/svc_01H… \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/scheduling/slots?service_id=svc_01H…&date=2026-05-09&variant_id=var_60min" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/scheduling/slots/check?service_id=svc_01H…&slot_time=2026-05-09T09:00:00Z" \ -H "X-API-Key: cpk_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://storefronts.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-API-Key: cpk_test_your_publishable_key" ``` ## GET /api/v1/scheduling/bookings List the authenticated customer’s bookings (past and upcoming). ```bash title="cURL" curl https://storefronts.cimplify.io/api/v1/scheduling/bookings \ -H "X-API-Key: cpk_test_your_publishable_key" ``` ## GET /api/v1/scheduling/bookings/\{booking_id\} Fetch one booking by ID. ```bash title="cURL" curl https://storefronts.cimplify.io/api/v1/scheduling/bookings/bk_01H… \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/scheduling/bookings/bk_01H…/cancel \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/scheduling/bookings/reschedule \ -H "X-API-Key: cpk_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. --- # Storefront assets URL: /docs/api-reference/storefront-assets > Business-scoped asset surface for storefront media: upload, list, inspect, and delete CDN-hosted files (images, videos, 3D models, fonts, PDFs). The CLI's `cimplify assets` wraps this. These endpoints back `cimplify assets`. They're business-scoped (auth via your `dk_live_…` key) and only ever surface **public, confirmed** uploads. The customer-runtime path ([Uploads](/docs/api-reference/uploads)) is a separate surface and never appears here. The flow: 1. **`POST /v1/businesses/{business_id}/assets/init`**: reserve an upload slot, get a presigned PUT URL. 2. **PUT the bytes** directly to the presigned URL. 3. **`POST /v1/businesses/{business_id}/assets/confirm`**: finalise; returns the canonical public URL. 4. **`GET /v1/businesses/{business_id}/assets`**: list the business's confirmed storefront assets. 5. **`GET /v1/businesses/{business_id}/assets/{upload_id}`**: fetch one. 6. **`DELETE /v1/businesses/{business_id}/assets/{upload_id}`**: delete the row and best-effort delete the blob. ## What you can upload Content-agnostic up to **50 MB** per file. Any MIME accepted: `image/*`, `video/mp4`, `model/gltf-binary`, `font/woff2`, `application/pdf`, anything else. The platform stores raw bytes and serves them via the public CDN (`storefrontassetscdn.cimplify.io`). Transcoding, thumbnailing, and adaptive video streaming are not part of this surface; those live in Drive (Phase 10). ## POST /v1/businesses/{business_id}/assets/init Reserve an upload slot and get a presigned PUT URL. ### Body | Field | Type | Description | | --- | --- | --- | | `folder` | string | Top-level folder, max 200 chars. No `..`, `\`, or newlines. | | `filename` | string | Filename, max 500 chars. Same character rules. | | `content_type` | string | MIME type, max 255 chars. | | `size_bytes` | integer | Declared size, must be > 0 and ≤ 52,428,800 (50 MB). | ### Request ```bash title="cURL" curl -X POST https://api.cimplify.io/v1/businesses/biz_xxx/assets/init \ -H "Authorization: Bearer dk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "folder": "hero", "filename": "main.jpg", "content_type": "image/jpeg", "size_bytes": 482915 }' ``` ### Response ```json { "data": { "upload_id": "upl_01H...", "upload_url": "https://...presigned...", "expires_in_secs": 600, "public_url_preview": "https://storefrontassetscdn.cimplify.io/assets/biz_xxx/hero/main.jpg" } } ``` PUT the file bytes to `upload_url` with the same `Content-Type` you declared. The URL is single-use and expires. ## POST /v1/businesses/{business_id}/assets/confirm Finalise after the PUT lands. The server verifies the blob exists and returns the canonical public URL. ### Body | Field | Type | Description | | --- | --- | --- | | `upload_id` | string | The id returned by `init`. | ### Response ```json { "id": "upl_01H...", "url": "https://storefrontassetscdn.cimplify.io/assets/biz_xxx/hero/main.jpg", "filename": "main.jpg", "content_type": "image/jpeg", "size_bytes": 482915 } ``` ## GET /v1/businesses/{business_id}/assets List the business's confirmed storefront assets. Paginated; default `limit=50`, max `100`. ### Query parameters | Param | Type | Default | Description | | --- | --- | --- | --- | | `page` | integer | 1 | 1-indexed page number. | | `limit` | integer | 50 | Page size; capped at 100. | | `folder` | string | — | Exact-match filter on the `folder_path` set at init. | | `mime_prefix` | string | — | Prefix-match on `content_type`, e.g. `image/`, `video/`, `model/`. | ### Request ```bash title="cURL" curl "https://api.cimplify.io/v1/businesses/biz_xxx/assets?folder=hero&mime_prefix=video/&page=1&limit=50" \ -H "Authorization: Bearer dk_live_your_key" ``` ### Response ```json { "data": { "assets": [ { "id": "upl_01H...", "url": "https://storefrontassetscdn.cimplify.io/assets/biz_xxx/hero/promo.mp4", "filename": "promo.mp4", "folder_path": "hero", "content_type": "video/mp4", "size_bytes": 12345678, "created_at": "2026-05-13T12:00:00Z" } ], "pagination": { "page": 1, "limit": 50, "total": 1 } } } ``` Ordered by `created_at DESC`. Customer-uploaded files (the private upload surface) are never included. ## GET /v1/businesses/{business_id}/assets/{upload_id} Fetch a single confirmed storefront asset. ```bash title="cURL" curl https://api.cimplify.io/v1/businesses/biz_xxx/assets/upl_01H... \ -H "Authorization: Bearer dk_live_your_key" ``` Returns the same row shape as one entry from the list endpoint. `404` if the id doesn't exist, doesn't belong to the business, or refers to a non-public/non-confirmed upload. ## DELETE /v1/businesses/{business_id}/assets/{upload_id} Hard-delete the row and best-effort delete the underlying blob. **Idempotent**: `204 No Content` whether or not the row existed. ```bash title="cURL" curl -X DELETE https://api.cimplify.io/v1/businesses/biz_xxx/assets/upl_01H... \ -H "Authorization: Bearer dk_live_your_key" ``` The DB delete is the consistency point; the API stops returning the asset immediately. The blob delete is best-effort; an object-storage failure leaves an orphan blob (storage cost only, not user-visible). A future Drive Phase 1 sweeper reclaims orphans. ## Errors - `400 VALIDATION_ERROR`: `folder`/`filename`/`content_type` violates the length/character rules, or `size_bytes` exceeds 50 MB. - `404 NOT_FOUND`: unknown `upload_id`, wrong business, or the upload isn't a confirmed public asset. ## Related - [`cimplify assets`](/docs/cli/assets): the CLI that wraps these endpoints. - [Uploads](/docs/api-reference/uploads): the **customer-runtime** path for files uploaded during checkout (private bucket, signed download URLs). --- # Subscriptions URL: /docs/api-reference/subscriptions > Read and manage the authenticated customer’s subscriptions. All endpoints require an active customer bearer session. ## GET /api/v1/subscriptions List subscriptions belonging to the authenticated customer (max 100). ### Request ```bash title="cURL" curl https://storefronts.cimplify.io/api/v1/subscriptions \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/subscriptions/sub_01H… \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/subscriptions/sub_01H…/cancel \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/subscriptions/sub_01H…/pause \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/subscriptions/sub_01H…/resume \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/subscriptions/sub_01H…/skip \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/support/conversation \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/support/conversation/messages \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/support/conversation/messages?after=2026-05-07T16:42:00Z&limit=50" \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/uploads/init \ -H "X-API-Key: cpk_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://storefronts.cimplify.io/api/v1/uploads/confirm \ -H "X-API-Key: cpk_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 (`csk_…`) 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://storefronts.cimplify.io/api/v1/webhooks \ -H "X-API-Key: csk_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://storefronts.cimplify.io/api/v1/webhooks \ -H "X-API-Key: csk_test_your_secret_key" ``` ## DELETE /api/v1/webhooks/\{webhook_id\} Remove a webhook subscription. Idempotent. ```bash title="cURL" curl -X DELETE https://storefronts.cimplify.io/api/v1/webhooks/whk_01H… \ -H "X-API-Key: csk_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 > Cimplify checkout accepts an `appearance` object that controls theme, accent color, font, and corner radius. The same shape works for hosted Pay and embedded checkout. ## 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 checkout ```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 - [**CimplifyCheckout**](/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 (`csk_…`). 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": "cpk_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 `cpk_…` 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 --- # CimplifyCheckout (React) URL: /docs/checkout/elements > Mount the Cimplify checkout iframe from React. This is the recommended embedded checkout path for agent-built storefronts and custom merchant sites. `` gives the merchant a complete checkout on their own page: contact capture, Cimplify Link verification, saved details, address, payment, provider authorization, submit, recovery, and status updates. The SDK creates the checkout iframe and handles the message bridge, OAuth hand-off, checkout processing, aborts, and completion events. For most storefronts, this is the embedded checkout integration to use. ## Required Auth Setup Checkout uses the storefront's OAuth client when a shopper chooses to use Cimplify saved details. Add the route handlers from [Sign in with Cimplify](/docs/sdk/auth#route-handlers), then expose these browser env vars: ```bash NEXT_PUBLIC_CIMPLIFY_CLIENT_ID=cim_client_… NEXT_PUBLIC_CIMPLIFY_REDIRECT_URI=https://your-store.com/auth/callback ``` These values let checkout verify the shopper through `auth.cimplify.io` and return to the merchant page. ## Example ```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)} /> ); } ``` ## Props | Prop | Type | Required | Notes | | --- | --- | --- | --- | | `client` | `CimplifyClient` | yes | Same client you use elsewhere; the checkout iframe inherits the 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 details when the shopper opts in. | | `appearance` | [`ElementAppearance`](/docs/checkout/appearance) | no | Memoize this; reference changes after mount are warned about and ignored. | | `linkUrl` | `string` | no | Override the Link host for development. | | `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. | ## Checkout Sign-In UX Checkout collects email or phone inline. Once the contact is valid, the SDK starts Cimplify OAuth with that contact as `login_hint` and checkout-oriented copy. If the shopper already has a matching Cimplify session, checkout loads saved details without showing verification UI. If the shopper needs to verify, hosted auth opens directly to OTP/passkey for that contact. The checkout iframe only receives the access token after verification succeeds. The UI does not reveal account existence before verification. It can show a generic "Use saved details with Cimplify" action after a valid contact, then Cimplify decides whether silent sign-in, OTP, passkey, consent, or account switching is needed. ## Account Switching When the shopper is signed in during checkout, checkout shows the verified contact and a `Change` action. Choosing `Change` starts Cimplify OAuth with an account-switch prompt so the shopper can use a different Cimplify account before selecting saved details or entering new payment information. ## Agent Checklist 1. Use `` for embedded checkout. 2. Set `NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY`, `NEXT_PUBLIC_CIMPLIFY_CLIENT_ID`, and `NEXT_PUBLIC_CIMPLIFY_REDIRECT_URI`. 3. Add `/auth/callback` with both GET and POST handlers. 4. Use `cpk_test_` / `cpk_live_` for Cimplify public keys. 5. Leave shopper verification to `auth.cimplify.io`. 6. Load saved Link details only after verification completes. ## Next - [**Appearance API**](/docs/checkout/appearance): Theme checkout - [**Vanilla checkout**](/docs/checkout/vanilla): Same checkout controller without React - [**Raw checkout iframe**](/docs/checkout/embedded): Manual postMessage integration --- # Embedded checkout iframe URL: /docs/checkout/embedded > Mount the same checkout UI as Cimplify Pay on your own domain. Raw iframe embedding is possible, but saved-details sign-in requires an OAuth bridge; most storefronts should use the SDK controller. The embedded iframe is the lowest-level checkout integration. It is useful for Vue, Svelte, plain HTML, webviews, or custom platforms. If you are building a React storefront, prefer [CimplifyCheckout](/docs/checkout/elements). Guest checkout can be wired with plain `postMessage`. Cimplify saved details require handling the checkout auth request and returning a token with `set_token`. The SDK already does that bridge. ## Iframe URL ```text https://link.cimplify.io/elements/checkout?businessId=biz_…&nonce= ``` `/elements/payment` is an alias of `/elements/checkout`. ## URL Params | Param | Required | What it does | | --- | --- | --- | | `businessId` | yes | The Cimplify business this checkout belongs to. | | `nonce` | recommended | Per-iframe random string. The iframe echoes it on every message. | | `email` | no | Pre-fill the checkout contact field. | ## Minimal Embed ```html ``` ## Init Handshake The iframe posts `{ type: "ready", height, nonce }`. Reply with `init`. ```ts const iframe = document.getElementById("cimplify-checkout") as HTMLIFrameElement; const LINK_ORIGIN = "https://link.cimplify.io"; const nonce = "ab12cd34"; window.addEventListener("message", (event) => { if (event.origin !== LINK_ORIGIN) return; if (event.source !== iframe.contentWindow) return; if (event.data?.nonce !== nonce) return; if (event.data?.type === "ready") { iframe.contentWindow!.postMessage({ type: "init", businessId: "biz_01J5…", publicKey: "cpk_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 ```ts window.addEventListener("message", (event) => { if (event.origin !== LINK_ORIGIN) return; if (event.source !== iframe.contentWindow) return; if (event.data?.nonce !== nonce) 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. ```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); ``` ## Handling Saved-Details Sign-In When the shopper enters a valid contact and chooses Cimplify, checkout emits: ```ts { type: "auth_requested", loginHint: "jane@example.com", contactType: "email", mode: "checkout", nonce: "ab12cd34" } ``` A raw iframe parent must do one of these: 1. Use `@cimplify/sdk` just for the checkout controller or `startSignIn`, which handles OAuth, PKCE, `web_message`, callback exchange, and `set_token`. 2. Implement the OAuth bridge yourself. The successful end of the flow is always: ```ts iframe.contentWindow!.postMessage({ type: "set_token", token: accessTokenFromYourCallback, }, LINK_ORIGIN); ``` For a delightful checkout, do not show a separate "are you a member?" step and do not reveal account existence. Let checkout collect the contact, then start OAuth with that contact as `login_hint`. Cimplify will silently use an existing matching SSO session, or show OTP for that contact. ## Starting Checkout With `renderSubmitButton: true`, the iframe renders its own Pay button and emits `request_submit`. Reply with `process_checkout`. ```ts window.addEventListener("message", (event) => { if (event.origin !== LINK_ORIGIN) return; if (event.source !== iframe.contentWindow) return; if (event.data?.nonce !== nonce) 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`. - Always check `event.source`. - Use a random per-iframe `nonce` and verify it on incoming messages. - Send messages to `"https://link.cimplify.io"`, not `"*"`. - Include `allow-same-origin` and `allow-popups` in the sandbox. Link sessions and some payment providers need them. - Post only the short-lived access token returned by your callback. ## When to Graduate Once you are writing more than a small message handler, switch to [CimplifyCheckout](/docs/checkout/elements) or [Vanilla checkout](/docs/checkout/vanilla). The SDK controller handles auth, origin checks, nonce routing, token broadcast, checkout processing, aborts, and timeouts. ## 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 Embedded Checkout | 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` - [**CimplifyCheckout**](/docs/checkout/elements): When you want Cimplify-rendered checkout --- # Vanilla checkout URL: /docs/checkout/vanilla > The framework-agnostic checkout surface. Use this from Vue, Svelte, plain HTML, or anywhere React wrappers are not the right fit. The same CimplifyElements controller backs ``. `createElements` gives you the SDK-managed checkout controller without React. It mounts the checkout iframe, validates postMessage origin and nonce, handles Storefront OAuth, and runs checkout. ## What ships ```ts import { createCimplifyClient, createElements, CimplifyElements, CimplifyElement, ELEMENT_TYPES, EVENT_TYPES, MESSAGE_TYPES, } from "@cimplify/sdk"; ``` ## Bootstrap Configure the Cimplify public key and the storefront OAuth client. Use `cpk_test_...` in sandbox and `cpk_live_...` in production. ```ts const client = createCimplifyClient({ publicKey: "cpk_live_...", }); const elements = createElements(client, "biz_01J5...", { auth: { clientId: "cim_client_...", redirectUri: `${window.location.origin}/auth/callback`, callbackUri: "/auth/callback", }, appearance: { theme: "light", variables: { primaryColor: "#059669", borderRadius: "0.85rem" }, }, }); ``` The callback route is the same one used by storefront sign-in. It must accept `GET` for redirect sign-in and `POST` for modal or silent sign-in. See [Sign in with Cimplify](/docs/sdk/auth#route-handlers). ## Mounting Checkout The controller creates the checkout iframe and routes events. ```ts const checkout = elements.create(ELEMENT_TYPES.CHECKOUT, { orderTypes: ["delivery", "pickup"], defaultOrderType: "delivery", submitLabel: "Pay GHS 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"); ``` ## Element Type | Constant | Iframe path | What it renders | | --- | --- | --- | | `ELEMENT_TYPES.CHECKOUT` | `/elements/checkout` | Full checkout: contact, sign-in, address, payment, submit. | ## Event types Subscribe via `element.on(eventType, handler)`. | Event | Payload | Fires on | | --- | --- | --- | | `READY` | `{ height }` | iframe ready | | `AUTHENTICATED` | `AuthenticatedData` | OAuth sign-in completed | | `CHANGE` | `{ address }` or `{ paymentMethod }` | field changes | | `ORDER_TYPE_CHANGED` | `{ orderType }` | delivery/pickup/dine-in toggle | | `REQUEST_SUBMIT` | `{}` | in-iframe Pay button pressed | | `ERROR` | `{ code, message }` | startup, auth, iframe, or checkout error | During checkout, the iframe collects email/phone inline. The controller starts Cimplify OAuth with that contact as the login hint, tries silent sign-in first, and opens hosted verification only when the shopper needs to verify. ## 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 an in-flight attempt. The promise settles 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); }, }); 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 the iframe renders its own Pay button, listen for `REQUEST_SUBMIT` and run checkout from the parent: ```ts checkout.on(EVENT_TYPES.REQUEST_SUBMIT, async () => { const result = await elements.processCheckout({ cart_id: cart.id, order_type: "delivery", }); // handle result }); ``` ## Cleanup Call `destroy()` when leaving the page or unmounting the host. This removes the iframe, clears handlers, and removes the postMessage listener. ```ts elements.destroy(); // or per element: checkout.destroy(); ``` ## Full example ```ts import { createCimplifyClient, createElements, ELEMENT_TYPES, EVENT_TYPES, } from "@cimplify/sdk"; const client = createCimplifyClient({ publicKey: "cpk_live_...", }); const elements = createElements(client, undefined, { auth: { clientId: window.CIMPLIFY_CLIENT_ID, redirectUri: `${window.location.origin}/auth/callback`, callbackUri: "/auth/callback", }, }); 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.AUTHENTICATED, (data) => { console.log("Signed in customer", data.customerId); }); 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"); window.addEventListener("beforeunload", () => elements.destroy()); ``` ## Next - [**CimplifyCheckout**](/docs/checkout/elements): Same controller as a React component - [**Element events**](/docs/concepts/element-events): Raw postMessage protocol --- # cimplify assets URL: /docs/cli/assets > Manage storefront brand assets (hero images, product photos, videos, 3D models, fonts) on the Cimplify CDN. `upload` is content-agnostic up to 50 MB per file; `ls` and `rm` are server-backed so the same view works from any machine. `cimplify assets` is the dev/agent surface for getting brand and product media onto the Cimplify CDN (`storefrontassetscdn.cimplify.io`). It wraps [`/v1/businesses/{id}/assets/*`](/docs/api-reference/storefront-assets) with a small client-side manifest cache (`./cimplify-assets.json`) for build-time URL substitution. For customer-runtime uploads (receipts attached to support tickets, profile photos uploaded inside a live storefront), use the SDK's `client.uploads.upload(file)` instead; those land in the private bucket as documented in [Uploads](/docs/api-reference/uploads). ## Usage ```bash cimplify assets upload [--folder ] [--force] cimplify assets ls [--prefix ] [--type ] cimplify assets rm ``` ## `upload ` Walks the directory recursively and uploads each file to the Cimplify CDN. Writes a manifest at `./cimplify-assets.json` keyed by manifest path → entry so re-runs are idempotent: files whose SHA-256 matches the existing entry are skipped unless `--force` is passed. ```bash cimplify assets upload public/hero/ ``` Output: ``` ▸ skip hero/main.jpg (unchanged) ▸ → hero/banner-wide.jpg (482915 bytes) ✓ Uploaded 1 file (1 skipped) Manifest: /path/to/store/cimplify-assets.json ``` ### Flags | Flag | Default | Notes | | --- | --- | --- | | `--folder ` | `assets` | The top-level folder in the CDN bucket. Combined with the file's relative path under ``. | | `--force` | off | Re-upload files even if the SHA-256 matches the manifest entry. | ### What lands on the CDN A file at `public/hero/main.jpg` uploaded with `--folder hero` lands at: ``` https://storefrontassetscdn.cimplify.io/assets/{business_id}/hero/main.jpg ``` `assets/` is the `key_prefix` configured on the `storefront_assets` use; `{business_id}` is your linked business; the rest is the folder + file path you supplied. **URLs are deterministic**: the `init` call returns `public_url_preview` so agents can pre-compute the final URL before the PUT happens. ### Supported content Up to **50 MB** per file, any MIME type: - Images: `jpg`, `jpeg`, `png`, `gif`, `webp`, `avif`, `svg` - Documents: `pdf` - Video: `mp4`, `webm` (short clips: the 50 MB cap suits hero loops and product demos; full-length streaming needs Drive Phase 10) - 3D: `glb`, `gltf`, `usdz` (product viz / AR; render with `` or Three.js) - Fonts: `woff`, `woff2`, `ttf` - Anything else with a valid MIME type The server doesn't transcode or thumbnail; it stores raw bytes and serves them via the public CDN. For browser-playable adaptive video, see Drive's AV pipelines roadmap. ## `ls [--prefix ] [--type ]` Lists the business's confirmed storefront assets **from the server** (works from any machine, not just one with a local manifest). Auto-paginates. ```bash cimplify assets ls cimplify assets ls --prefix hero cimplify assets ls --type video/ cimplify assets ls --type model/ ``` Output: ``` Storefront assets (3 total): ● hero/main.jpg image/jpeg 482.0 KB upl_01H... → https://storefrontassetscdn.cimplify.io/assets/biz_xxx/hero/main.jpg ● hero/promo.mp4 video/mp4 12.34 MB upl_02J... → https://storefrontassetscdn.cimplify.io/assets/biz_xxx/hero/promo.mp4 ● products/laptop-pro.glb model/gltf-binary 4.20 MB upl_03K... → https://storefrontassetscdn.cimplify.io/assets/biz_xxx/products/laptop-pro.glb ``` ### Flags | Flag | Notes | | --- | --- | | `--prefix ` | Exact-match filter on the `folder` set at upload time. Aliased as `--folder`. | | `--type ` | Prefix-match on `content_type`, e.g. `image/`, `video/`, `model/`, `font/`. | `--json` emits the full server response (assets array + pagination) as a structured envelope. ## `rm ` Deletes the asset on the server (DB row + best-effort blob delete) and prunes the local manifest entry. Idempotent: passing an already-gone id still returns 0. ```bash # Delete by upload_id from `ls` output cimplify assets rm upl_01H... # Or by manifest key, if `./cimplify-assets.json` has it cimplify assets rm hero/old-banner.jpg ``` Output: ``` ✓ Removed hero/old-banner.jpg ``` If you're on a fresh machine without the local manifest, use the `upl_…` id from `cimplify assets ls`. ## Manifest format `./cimplify-assets.json` is a JSON object keyed by manifest path. It's a **build-time cache** for URL substitution and the `rm`-by-key convenience; the server is the source of truth, so a missing or stale manifest doesn't break `upload`/`ls`/`rm`. ```json { "hero/main.jpg": { "url": "https://storefrontassetscdn.cimplify.io/assets/biz_xxx/hero/main.jpg", "upload_id": "upl_01H...", "folder": "assets/hero", "filename": "main.jpg", "content_type": "image/jpeg", "size_bytes": 482915, "sha256": "abc123...", "uploaded_at": "2026-05-13T12:00:00.000Z" } } ``` Commit it. Storefront source code and CI agents read it for URL references; `upload` keeps it in sync. ## Wiring in storefront code Templates ship a `lib/cimplify-loader.ts` and have `images.loaderFile` set in `next.config.ts` to that file. Every `` flows through the loader; the loader detects Cimplify-hosted URLs (relative paths, the configured CDN base, known Cimplify hosts) and applies the transform contract. External hosts (Cloudinary, Unsplash, merchant S3) pass through unchanged. ```tsx import Image from "next/image"; import { assetUrl } from "@cimplify/sdk"; ... ``` Or for a typed React-hook entry point: ```tsx import { useImage } from "@cimplify/sdk/react"; function Hero() { const { src, loader } = useImage("hero/main.jpg", { format: "webp" }); return ...; } ``` For video and 3D, drop the URL into the appropriate tag; the SDK doesn't yet wrap them with polished components, but raw HTML works today: ```tsx