# List appointments Source: https://docs.cicini.com/api-reference/appointments/list-appointments /openapi.json get /api/public/v1/appointments Returns a paginated list of active appointments for the organization associated with the API key. Supports status, customer, staff, and date filters. # List customers Source: https://docs.cicini.com/api-reference/customers/list-customers /openapi.json get /api/public/v1/customers Returns a paginated list of active customers for the organization associated with the API key. # cURL cookbook Source: https://docs.cicini.com/examples/curl Practical curl recipes for Public API v1 # cURL cookbook Assume: ```bash theme={null} export CICINI_API_KEY="cic_test_…" export CICINI_BASE="https://cicini.com" # local: http://localhost:3000 ``` ## Health check auth ```bash theme={null} curl -sS -o /tmp/out.json -w "%{http_code}\n" \ -H "Authorization: Bearer $CICINI_API_KEY" \ "$CICINI_BASE/api/public/v1/customers?limit=1" # expect 200 ``` ## List first page of customers ```bash theme={null} curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ -H "Accept: application/json" \ "$CICINI_BASE/api/public/v1/customers?limit=10" | jq . ``` ## List confirmed appointments this month ```bash theme={null} curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ "$CICINI_BASE/api/public/v1/appointments?status=confirmed&startDate=2026-07-01T00:00:00.000Z&endDate=2026-08-01T00:00:00.000Z&limit=50" \ | jq '{count: (.data|length), total: .pagination.total, hasMore: .pagination.hasMore}' ``` ## Pretty-print one customer row ```bash theme={null} curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ "$CICINI_BASE/api/public/v1/customers?limit=1" \ | jq '.data[0] | {id, displayName, email, phone}' ``` ## Intentionally invalid query (expect 400) ```bash theme={null} curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ "$CICINI_BASE/api/public/v1/customers?limit=999" | jq . ``` ## Missing auth (expect 401) ```bash theme={null} curl -sS "$CICINI_BASE/api/public/v1/customers?limit=1" | jq . ``` ## Download OpenAPI ```bash theme={null} curl -sS "$CICINI_BASE/openapi.json" -o openapi.json ``` # Node.js example Source: https://docs.cicini.com/examples/node Fetch all customers with pagination using Node 18+ native fetch # Node.js example Works with Node **18+** (global `fetch`). Keep the API key in an environment variable. ```js theme={null} // list-all-customers.mjs const BASE = process.env.CICINI_BASE_URL ?? 'https://cicini.com'; const KEY = process.env.CICINI_API_KEY; if (!KEY) { console.error('Set CICINI_API_KEY'); process.exit(1); } async function listCustomersPage({ limit = 50, offset = 0 } = {}) { const url = new URL('/api/public/v1/customers', BASE); url.searchParams.set('limit', String(limit)); url.searchParams.set('offset', String(offset)); const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}`, Accept: 'application/json', }, }); const body = await res.json().catch(() => ({})); if (!res.ok) { const err = new Error(body.message || body.error || res.statusText); err.status = res.status; err.body = body; throw err; } return body; } async function listAllCustomers() { const all = []; let offset = 0; const limit = 50; for (;;) { const page = await listCustomersPage({ limit, offset }); all.push(...page.data); if (!page.pagination?.hasMore) break; offset += limit; } return all; } const customers = await listAllCustomers(); console.log(`Fetched ${customers.length} customers`); console.log(customers.slice(0, 3)); ``` ## Appointments with filters ```js theme={null} async function listAppointments(params = {}) { const url = new URL('/api/public/v1/appointments', BASE); for (const [k, v] of Object.entries(params)) { if (v != null) url.searchParams.set(k, String(v)); } const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}`, Accept: 'application/json' }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } const week = await listAppointments({ status: 'confirmed', startDate: '2026-07-14T00:00:00.000Z', endDate: '2026-07-21T00:00:00.000Z', limit: 100, }); console.log(week.pagination, week.data.length); ``` ## Notes * Use this only on a **server** (never ship `CICINI_API_KEY` to browsers). * Handle `401` / `400` separately from transport errors. * For TypeScript, generate types from OpenAPI (`openapi-typescript` against `/openapi.json`) rather than hand-copying fields. # Public vs internal Source: https://docs.cicini.com/guides/api-surface What is safe for integrations vs Cicini-only APIs # Public vs internal API surface Use this page to stay honest about what third-party integrations may call. ## Public (integration-friendly) | Surface | Auth | Purpose | | --------------------------------- | ------------------ | ----------------- | | `GET /api/public/v1/customers` | Org API key Bearer | List customers | | `GET /api/public/v1/appointments` | Org API key Bearer | List appointments | | `GET /openapi.json` | None | OpenAPI document | Contract source of truth in code: * Routes: `app/api/public/v1/**` * Zod + OpenAPI: `lib/openapi/public-v1-schemas.ts`, `lib/openapi/public-v1-registry.ts` ## Internal (not for third-party docs) Do **not** build partner integrations on these without an explicit product contract: | Surface | Why it is internal | | ------------------------------------------------ | ------------------------------------------------------------- | | `/api/dashboard/*`, most `/api/*` session routes | NextAuth cookie/session; CSRF and role checks for the web app | | `/api/admin/*` | Super-admin only | | `/api/stripe/*`, webhooks | Platform billing / Stripe | | `/api/scim/v2/*` | Separate enterprise SCIM product surface (if enabled) | | `/api/cron/*` | Scheduled jobs with shared secrets | | HTML scraping of `/dashboard/**` | Unstable, not an API | ## Not shipped as public v1 (yet) These product areas exist in the **dashboard** but have **no** public API key endpoints in v1: * Services / service categories * Staff / team * Availability * Gift cards * Webhooks outbound configuration via public API * Create / update / delete of any resource via public API If you need a resource that is not listed, contact product — do not invent paths. ## Data isolation * Every public request is scoped by the **organization on the API key**. * Soft-deleted / inactive rows are excluded (`isActive: true` filters on customers and appointments). * PHI and sensitive fields: treat customer PII and notes as confidential; do not log full payloads in multi-tenant systems. # Authentication Source: https://docs.cicini.com/guides/authentication Organization API keys for the Cicini Public API # Authentication Public API v1 uses **HTTP Bearer** authentication with an **organization API key**. ## Header ```http theme={null} Authorization: Bearer cic_test_… ``` or ```http theme={null} Authorization: Bearer cic_live_… ``` | Prefix | Intended use | | ----------- | --------------------- | | `cic_test_` | Development / sandbox | | `cic_live_` | Production | Keys are generated server-side (`cic_test_` or `cic_live_` + 64 hex characters). Only the **hash** is stored; the full secret is shown once at creation. ## Create a key (dashboard) 1. Sign in as an org **owner** or **admin**. 2. Open **Settings → Integrations → API keys** (`/dashboard/settings/integrations/api-keys`). 3. Create a test or live key. 4. Copy the secret immediately and store it in your secret manager. Creating or listing keys requires **API access** on the plan (`apiAccess` / `apiAccessEnabled`). Per plan-core: | Plan | API access | Max API keys (catalog) | | --------------------------- | ---------- | ---------------------- | | Free | No | 0 | | Starter (\$19/mo list) | No | 0 | | Professional (\$49/mo list) | Yes | 5 | | Enterprise | Yes | Unlimited (catalog) | Upgrade path in-product: `/dashboard/billing/upgrade`. ## Rules * Send the full key **only** in the `Authorization` header. * Never put live keys in frontend JavaScript, mobile apps you ship to end users, git, or support tickets. * Missing / malformed header → **401** `{ "error": "Unauthorized", "message": "API key required in Authorization header" }`. * Invalid, revoked, or expired key → **401** `{ "error": "Unauthorized", "message": "Invalid or expired API key" }`. * Each key is scoped to **one organization**. Responses only include that org’s data. You do not pass `orgId` on public routes. ## Example ```bash theme={null} export CICINI_API_KEY="cic_test_…" curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ "https://cicini.com/api/public/v1/appointments?limit=5" ``` ## OpenAPI security scheme The published OpenAPI document defines `bearerAuth` (HTTP bearer, API key). Importers of `/openapi.json` will prompt for a token automatically. ## Not supported on public v1 | Mechanism | Status | | ------------------------------ | -------------------------------------- | | OAuth 2.0 for third-party apps | Not public v1 | | NextAuth session cookies | Web app only | | SCIM bearer tokens | Separate SCIM surface — not this guide | | User password grants | Not applicable | # Errors Source: https://docs.cicini.com/guides/errors Status codes and error response shape for Public API v1 # Errors ## Status codes (public v1) | Status | Meaning | Typical cause | | ------ | ------------------------ | --------------------------------------------------- | | `200` | Success | Valid key and query | | `400` | Invalid query parameters | Zod validation failed (`limit`, `status`, dates, …) | | `401` | Unauthorized | Missing header, wrong format, invalid/expired key | | `500` | Internal server error | Unexpected server failure | Public v1 list routes do **not** currently return `403` for plan gates on the **read** path (auth is “valid key → org data”). Plan gating applies when **creating** API keys in the dashboard (`apiAccessEnabled`). ## Error body ```json theme={null} { "error": "Unauthorized", "message": "API key required in Authorization header" } ``` Validation failures may include structured Zod issues: ```json theme={null} { "error": "Invalid query parameters", "details": [ { "code": "too_big", "maximum": 100, "type": "number", "path": ["limit"], "message": "Number must be less than or equal to 100" } ] } ``` OpenAPI schema: `publicErrorSchema` (`error`, optional `message`, optional `details`). ## Client guidance | Situation | Action | | -------------- | --------------------------------------------------------------------- | | `401` | Fix key configuration; do not retry blindly | | `400` | Fix query params; do not retry the same payload | | `500` | Retry with exponential backoff; log `requestId` if you have edge logs | | Network errors | Retry with backoff; make sync jobs idempotent | Always check HTTP status **before** assuming a `data` array exists. # Getting started Source: https://docs.cicini.com/guides/getting-started Base URL, first request, and how to page through Public API v1 # Getting started The Cicini Public API is a **read-only**, **API key** authenticated HTTP API for organization data. ## Base URL | Environment | Base URL | | ----------------- | ----------------------- | | Production | `https://cicini.com` | | Local development | `http://localhost:3000` | All paths are relative to that base (for example `/api/public/v1/customers`). ## Prerequisites 1. A Cicini organization on a plan with **API access** (Professional or Enterprise — Free and Starter cannot create API keys). 2. An API key created in the dashboard under **Settings → Integrations → API keys** (`cic_test_…` or `cic_live_…`). 3. A backend or CLI that can send `Authorization: Bearer …` (never embed live keys in a browser SPA). ## First request ```bash theme={null} export CICINI_API_KEY="cic_test_YOUR_KEY" export CICINI_BASE_URL="https://cicini.com" # or http://localhost:3000 curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ -H "Accept: application/json" \ "$CICINI_BASE_URL/api/public/v1/customers?limit=10" ``` ### Success shape ```json theme={null} { "data": [ { "id": "clx…", "firstName": "Ada", "lastName": "Lovelace", "displayName": "Ada Lovelace", "email": "ada@example.com" } ], "pagination": { "limit": 10, "offset": 0, "total": 42, "hasMore": true } } ``` ### Failure example (missing key) ```json theme={null} { "error": "Unauthorized", "message": "API key required in Authorization header" } ``` Status: **401**. ## Conventions | Convention | Detail | | ----------- | ----------------------------------------------------------------- | | Protocol | HTTPS in production | | Format | JSON request/response; `Accept: application/json` | | Auth | `Authorization: Bearer ` only | | Time fields | ISO-8601 strings (for example `2026-07-19T15:00:00.000Z`) | | Money | Integer **cents** where present (`priceCents`, `amountPaidCents`) | | Org scope | Inferred from the API key — you never pass `orgId` on public v1 | ## Pagination (summary) | Query | Default | Limits | | -------- | ------- | ----------------- | | `limit` | `50` | Integer **1–100** | | `offset` | `0` | Integer ≥ 0 | Loop while `pagination.hasMore` is `true`, increasing `offset` by `limit`. Full guide: [Pagination](/guides/pagination). ## What to call next | Goal | Endpoint | | ------------------- | ----------------------------------------------------------------------- | | Sync CRM contacts | [`GET /api/public/v1/customers`](/resources/customers) | | Export schedule | [`GET /api/public/v1/appointments`](/resources/appointments) | | Understand failures | [Errors](/guides/errors) | | Plan / rate honesty | [Rate limits](/guides/rate-limits) · [API surface](/guides/api-surface) | Interactive parameter docs: **API Reference** tab (OpenAPI playground). # Pagination Source: https://docs.cicini.com/guides/pagination limit, offset, and hasMore on Public API v1 list endpoints # Pagination All public v1 **list** endpoints return a shared envelope: ```json theme={null} { "data": [ /* resources for this page */ ], "pagination": { "limit": 50, "offset": 0, "total": 123, "hasMore": true } } ``` ## Query parameters Validated by Zod (`publicPaginationQuerySchema` in `lib/openapi/public-v1-schemas.ts`): | Name | Type | Default | Constraints | | -------- | ----------------------------- | ------- | ----------- | | `limit` | integer (query string digits) | `50` | **1–100** | | `offset` | integer (query string digits) | `0` | ≥ **0** | Invalid values (non-numeric, out of range) → **400** with Zod `details`. ## Algorithm ```text theme={null} offset = 0 limit = 50 loop: GET …?limit=50&offset={offset} process data[] if not pagination.hasMore: stop offset = offset + limit ``` ### bash loop ```bash theme={null} OFFSET=0 LIMIT=50 BASE="https://cicini.com/api/public/v1/customers" while true; do RESP=$(curl -sS -H "Authorization: Bearer $CICINI_API_KEY" \ "$BASE?limit=$LIMIT&offset=$OFFSET") echo "$RESP" | jq '.data | length' HAS_MORE=$(echo "$RESP" | jq -r '.pagination.hasMore') [ "$HAS_MORE" = "true" ] || break OFFSET=$((OFFSET + LIMIT)) done ``` ## Ordering | Endpoint | Default order | | ------------ | ---------------------- | | Customers | `createdAt` descending | | Appointments | `startAt` descending | There is **no** public cursor/`page` param today — use **offset** only. ## Tips * Prefer smaller pages (25–50) for large orgs to keep payloads small. * `total` is the full filtered count, not just the page size. * Concurrent writes can change `total` between pages; design sync jobs to be idempotent on resource `id`. # Rate limits Source: https://docs.cicini.com/guides/rate-limits Honest guidance on Public API v1 throughput and plan catalog limits # Rate limits ## What is enforced today **Public v1 route handlers do not currently emit rate-limit headers** (`X-RateLimit-*`, `Retry-After`) and do not implement a dedicated per-key throttle in those handlers. That means: * You should still design clients for **reasonable volume** (batch with pagination, cache where appropriate). * Platform-level abuse protection (edge / infrastructure) may still reject pathological traffic. * Do **not** assume unlimited parallel polling is supported. ## Plan catalog (reference only) `lib/pricing/plan-core.ts` includes an `apiRateLimit` field that maps to **`apiRateLimitRpm`** (requests per minute) in the plan registry: | Plan | Catalog `apiRateLimit` (RPM) | | ------------ | ---------------------------- | | Free | `0` (API access off) | | Starter | `0` (API access off) | | Professional | `1000` | | Enterprise | `10000` | Treat these as **product catalog limits**, not a guarantee that public v1 already returns `429` when exceeded. When enforcement ships in the public handlers, this page and OpenAPI will be updated. ## Recommended client behavior 1. Prefer **incremental sync** (filter appointments by `startDate` / `endDate`) over full-table pulls every minute. 2. Cap concurrency (for example 1–2 parallel requests per key). 3. On unexpected `5xx` or connection errors, use exponential backoff with jitter. 4. Cache customer lists when your job only needs appointments. ## If you need higher volume Contact sales / support for Enterprise. Do not scrape dashboard HTML or call session-authenticated `/api/*` routes from an integration — those are **not** the public contract. # Overview Source: https://docs.cicini.com/index Build server-side integrations with the Cicini Public API v1 # Cicini Public API Welcome. The **Public API v1** is a **read-only**, **API-key** authenticated HTTP API for organization data. Use it to sync customers and appointments into your own tools (CRMs, analytics, back-office scripts). Base URL, first request, and response shape. Bearer keys (`cic_test_` / `cic_live_`) and plan requirements. Customers and appointments (bookings) that ship today. Interactive OpenAPI for every public v1 operation. ## What you can build today | Resource | Method | Path | Notes | | ------------ | ------ | ----------------------------- | -------------------------------------------------------------------- | | Customers | `GET` | `/api/public/v1/customers` | Active customers only; paginated | | Appointments | `GET` | `/api/public/v1/appointments` | Active appointments; filters for status, customer, staff, date range | Machine-readable contract: [OpenAPI](https://cicini.com/openapi.json) (regenerate with `pnpm docs:openapi` in the monorepo). ## Quick mental model ```mermaid theme={null} sequenceDiagram participant App as Your backend participant API as Cicini Public API participant DB as Org data App->>API: Authorization Bearer cic_… API->>API: Verify API key → orgId API->>DB: Query scoped to orgId DB-->>API: Rows API-->>App: data[] + pagination ``` 1. Create an API key in the Cicini dashboard (**Professional+** plan — see [Authentication](/guides/authentication)). 2. Call public endpoints from a **server** with `Authorization: Bearer …`. 3. Page through results with `limit` / `offset` and `pagination.hasMore`. ## Product truth (do not assume) | Topic | Truth | | ---------------------------------------- | ---------------------------------------------------------------------------------------------- | | API surface | **Only** paths under `/api/public/v1/*` are integration-friendly | | Write APIs | **Not available** on public v1 (no create/update/delete customers or appointments via API key) | | Services / staff / gift cards / webhooks | **Not** public v1 endpoints yet | | Dashboard session APIs | `/api/*` used by the web app require NextAuth — **not** for third-party integrations | | Super-admin APIs | `/api/admin/*` — platform only | | List prices (product) | Free $0 · Starter **$19\*\* · Professional **\$49** (`lib/pricing/plan-core.ts`) | | API access on Free / Starter | `apiAccess: false`, `apiKeys: 0` — keys are a **Professional+** feature | ## Where to read these docs | Surface | URL | | -------------- | ----------------------------------------------------------------------- | | **Production** | [https://docs.cicini.com](https://docs.cicini.com) (this Mintlify site) | | App landing | [https://cicini.com/docs](https://cicini.com/docs) — points here | | Local preview | `pnpm docs:dev` from the monorepo root (`developer-docs/`) | ## Next steps * [Getting started](/guides/getting-started) * [Public vs internal surface](/guides/api-surface) * [Examples (curl)](/examples/curl) # Appointments Source: https://docs.cicini.com/resources/appointments List organization appointments (bookings / reservations) via Public API v1 # Appointments Read-only access to organization **appointments** (also called reservations/bookings in the product UI). ## List appointments ```http theme={null} GET /api/public/v1/appointments ``` **Auth:** Bearer API key\ **OpenAPI operationId:** `listAppointments` ### Query parameters | Name | Type | Default | Description | | ------------ | -------------------------- | ------- | ------------------------------------------------------------------------------------- | | `limit` | integer | `50` | Page size (1–100) | | `offset` | integer | `0` | Rows to skip | | `status` | string enum | — | `scheduled` \| `confirmed` \| `checked_in` \| `no_show` \| `cancelled` \| `completed` | | `customerId` | string | — | Filter by customer id | | `staffId` | string | — | Filter by staff id | | `startDate` | string (ISO-8601 datetime) | — | Inclusive lower bound on appointment **start** | | `endDate` | string (ISO-8601 datetime) | — | Inclusive upper bound on appointment **start** | Validated by `publicAppointmentsQuerySchema`. ### Response ```json theme={null} { "data": [ { "id": "clxappt…", "orgId": "clxorg…", "customerId": "clxcust…", "staffId": "clxstaff…", "serviceId": "clxsvc…", "start": "2026-07-19T15:00:00.000Z", "end": "2026-07-19T15:30:00.000Z", "status": "confirmed", "locationType": "in_person", "priceCents": 5000, "currency": "cad", "note": null, "paymentStatus": "paid", "amountPaidCents": 5000, "requiresPayment": true, "createdAt": "2026-07-01T10:00:00.000Z", "updatedAt": "2026-07-01T10:05:00.000Z" } ], "pagination": { "limit": 50, "offset": 0, "total": 1, "hasMore": false } } ``` `start` and `end` are always ISO-8601 strings in the public response. Money fields are **integer cents** when present. Schema reference: `publicAppointmentSchema`. ### Filters and scope | Behavior | Detail | | ----------- | ---------------------------------- | | Org scope | Key’s organization only | | Soft-delete | Only `isActive: true` appointments | | Order | Newest `startAt` first | | Date filter | Applied on start time (`startAt`) | ### Example — week window ```bash theme={null} curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ "https://cicini.com/api/public/v1/appointments?status=confirmed&startDate=2026-07-14T00:00:00.000Z&endDate=2026-07-21T00:00:00.000Z&limit=100" ``` ### Example — one customer ```bash theme={null} curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ "https://cicini.com/api/public/v1/appointments?customerId=clxcust…&limit=20" ``` ### Errors | Status | When | | ------ | ------------------------------------- | | `400` | Invalid enum, datetime, or pagination | | `401` | Missing or invalid API key | | `500` | Server error | ## Not available on public v1 * Create / reschedule / cancel appointments * Collect payment or apply gift cards via API key * Calendar ICS export * Single-resource `GET /appointments/:id` # Customers Source: https://docs.cicini.com/resources/customers List organization customers via Public API v1 # Customers Read-only access to the organization **customer directory**. ## List customers ```http theme={null} GET /api/public/v1/customers ``` **Auth:** Bearer API key\ **OpenAPI operationId:** `listCustomers` ### Query parameters | Name | Type | Default | Description | | -------- | ------- | ------- | ----------------- | | `limit` | integer | `50` | Page size (1–100) | | `offset` | integer | `0` | Rows to skip | Validated by `publicCustomersQuerySchema`. ### Response ```json theme={null} { "data": [ { "id": "clxcustomer…", "supportId": "…", "firstName": "Ada", "lastName": "Lovelace", "displayName": "Ada Lovelace", "email": "ada@example.com", "phone": "+1…", "primaryPhone": "+1…", "secondaryPhone": null, "preferredContactMethod": "email", "dateOfBirth": "…", "gender": "…", "notes": "…", "marketingConsent": true, "addressLine1": "…", "city": "…", "region": "…", "postalCode": "…", "country": "CA", "preferredStaffId": "…", "isActive": true, "createdAt": "2026-01-15T12:00:00.000Z", "updatedAt": "2026-01-15T12:00:00.000Z" } ], "pagination": { "limit": 50, "offset": 0, "total": 1, "hasMore": false } } ``` Field set mirrors the public projection of the customer mapper (optional fields may be omitted when empty). Schema reference: `publicCustomerSchema`. ### Filters and scope | Behavior | Detail | | ----------- | ------------------------------------------------------ | | Org scope | Key’s organization only | | Soft-delete | Only `isActive: true` customers | | Order | Newest `createdAt` first | | Search | **No** public search query today — page through `data` | ### Example ```bash theme={null} curl -sS \ -H "Authorization: Bearer $CICINI_API_KEY" \ "https://cicini.com/api/public/v1/customers?limit=25&offset=0" | jq . ``` ### Errors | Status | When | | ------ | -------------------------- | | `400` | Bad `limit` / `offset` | | `401` | Missing or invalid API key | | `500` | Server error | See [Errors](/guides/errors). ## Not available on public v1 * `GET /customers/:id` * `POST` / `PATCH` / `DELETE` customers * Import/export CSV via API key