Two paths, one backend. Take our widget, or build your own UI on the same API. Pick whichever matches your stack.
Every account gets a publishable key (pk_live_...)
from your dashboard. It's safe to put in client-side code. The real security boundary is the list of origins you allow it to be called from, which you also set in the dashboard. Register every domain you'll embed on (including localhost while you're testing).
We render the booking UI, you just paste it in.
If your site is built with a React-based framework, use the component directly.
View on npm ↗npm install @creno/react import { BookingWidget } from "@creno/react";
import "@creno/react/style.css";
export default function BookingPage() {
return (
<BookingWidget
apiUrl="https://api.crenoapp.com"
apiKey="pk_live_..."
/>
);
} | Prop | Type | Description |
|---|---|---|
| apiUrl * | string | Your Créno API base URL. |
| apiKey * | string | Your publishable key (safe to expose in browser JS). |
| resourceId | string | Target a specific calendar. Omit to use your default. |
| labels | Partial<Labels> | Override any UI string. French and English ship built-in; use this for any other language. |
| formatLocale | string | Date/time formatting locale, e.g. "fr-CA". Defaults to the browser locale. |
| onBooked | (booking) => void | Called after a booking is successfully created. |
Vue 3 wrapper around the same component. Auto-loads the embed script, no <script> tag to add yourself.
View on npm ↗npm install @creno/vue <script setup>
import { BookingWidget } from "@creno/vue";
</script>
<template>
<BookingWidget
api-url="https://api.crenoapp.com"
api-key="pk_live_..."
@booked="(booking) => console.log(booking.id)"
/>
</template> | Prop | Description |
|---|---|
| apiUrl * | Your Créno API base URL. |
| apiKey * | Your publishable key. |
| resourceId | Target a specific calendar. |
| labels | Override any UI string. |
| @booked | Emitted with the created booking. |
Plain HTML, Shopify, Webflow, Squarespace, or anywhere else you can paste a script tag. No build step, no framework required.
<script src="https://cdn.crenoapp.com/embed.js" defer></script>
<creno-widget
api-url="https://api.crenoapp.com"
api-key="pk_live_..."
></creno-widget> Renders inside a Shadow DOM, so it can never clash with your site's own CSS in either direction: your styles won't leak in, and the widget's styles won't leak out.
| Attribute | Description |
|---|---|
| api-url * | Your Créno API base URL. |
| api-key * | Your publishable key. |
| resource-id | Target a specific calendar. |
| locale | e.g. "fr-CA". |
Every color, radius, and font is a CSS custom property. Override them from your own stylesheet, with no build config and no component props needed, and it works identically for both the React component and the script embed.
creno-widget, .my-booking-page {
--pbw-color-accent: #b8452f;
--pbw-color-accent-hover: #963a27;
--pbw-radius-lg: 8px;
} | Variable | Default | Controls |
|---|---|---|
| --pbw-color-accent | #2563eb | Primary brand color: buttons, selected date, links |
| --pbw-color-accent-hover | #1d4ed8 | Hover state for the primary button |
| --pbw-color-accent-soft | #eff6ff | Light tint used for hover backgrounds |
| --pbw-color-surface | #ffffff | Card background |
| --pbw-color-border | #e5e7eb | Default border color |
| --pbw-color-text | #111827 | Primary text color |
| --pbw-radius-lg | 16px | Card corner radius |
| --pbw-font | system font stack | Font family used throughout |
You do the rendering. We stay the backend and the infrastructure.
TypeScript client, zero runtime dependencies (Node 18+'s native fetch). A distinct error class per error type, same taxonomy as the Python kit.
View on npm ↗npm install @creno/node import { CrenoClient, CrenoConflictError } from "@creno/node";
const client = new CrenoClient("pk_live_...");
const availability = await client.getAvailability({ from: "2026-08-01", to: "2026-08-07" });
try {
const booking = await client.createBooking({
startAt: availability.slots[0].startAt,
customerName: "Jane Doe",
customerEmail: "jane@example.com",
});
} catch (err) {
if (err instanceof CrenoConflictError) {
// that slot was just taken, re-check availability and retry
}
} A typed client for any Python backend: normal methods, plus a distinct exception per error type (slot taken, plan limit, too many requests...) instead of parsing status codes by hand.
View on PyPI ↗pip install creno from creno import CrenoClient
client = CrenoClient(api_key="pk_live_...")
availability = client.get_availability(from_="2026-08-01", to="2026-08-07")
booking = client.create_booking(
start_at=availability.slots[0].start_at,
customer_name="Jane Doe",
customer_email="jane@example.com",
) Both the widget and the script embed call this same REST API underneath. If you're building your own UI, or integrating from a backend (Python, Ruby, PHP, Go, whatever), you can call it directly, no client library required, just an HTTP client.
Every request needs your publishable key in the X-API-Key header. That key identifies your account; the real protection is the origin allowlist you configure in the dashboard, which applies to requests coming from a browser (CORS). Server-side calls (Python, etc.) never go through a browser, so origin checking doesn't apply to them, still keep your key server-side and out of source control.
X-API-Key: pk_live_... https://api.crenoapp.com Lists the active service types for your calendar. If it returns 0 or 1 results, your integration can skip this step entirely and omit serviceTypeId everywhere else.
| Params | Description |
|---|---|
| resourceId | Optional. Omit for your default calendar. |
[
{ "id": "...", "resourceId": "...", "name": "Haircut", "active": true, "sortOrder": 0 }
] Returns actually-open slots for a date range, after applying recurring hours, exceptions, and already-taken bookings.
| Params | Description |
|---|---|
| from* | Start date, YYYY-MM-DD. |
| to* | End date, YYYY-MM-DD. |
| resourceId | Optional. Omit for your default calendar. |
| serviceTypeId | Optional, if you use a service-type menu. |
{
"resourceId": "...",
"timezone": "America/Toronto",
"slots": [
{ "startAt": "2026-08-03T13:00:00.000Z", "endAt": "2026-08-03T13:30:00.000Z" }
]
} Creates a booking. Re-validated server-side at write time (never trusts an earlier availability call alone), and protected against double-booking at the database level.
{
"startAt": "2026-08-03T13:00:00.000Z",
"customerName": "Jane Doe",
"customerEmail": "jane@example.com",
"customerPhone": "+1 514 555 0100",
"notes": "First visit",
"resourceId": null,
"serviceTypeId": null,
"lang": "en"
} startAt, customerName, and customerEmail are required, everything else is optional. The response (201) is the created booking.
| Endpoint | Rate limits |
|---|---|
| POST /v1/public/bookings | 5 / minute |
| GET /v1/public/service-types, /v1/public/availability | 60 / minute |
Every error returns a JSON body with an error field. The ones that matter for integration:
| Code | Description |
|---|---|
| 401 | Missing or invalid X-API-Key header. |
| 403 | Origin not allowed for this account (see the origin allowlist in your dashboard). |
| 404 | No calendar configured for this account, or resource not found. |
| 409 | That slot was just taken by someone else, re-check availability and retry. |
| 402 | The account's plan hit a limit (bookings, calendars, or websites); the body includes limitType and plan. |
| 429 | Too many requests; the limit is enforced per API key, not per IP. |
No official kit for Ruby, Go, PHP, etc., the REST API itself is enough, with any HTTP client:
curl "https://api.crenoapp.com/v1/public/availability?from=2026-08-01&to=2026-08-07" \
-H "X-API-Key: pk_live_..."
curl -X POST "https://api.crenoapp.com/v1/public/bookings" \
-H "X-API-Key: pk_live_..." \
-H "Content-Type: application/json" \
-d '{"startAt":"2026-08-03T13:00:00.000Z","customerName":"Jane Doe","customerEmail":"jane@example.com"}'