# Build a booking app with Claude Code, Cursor or Lovable – Tiempo

> A rules file and a starter prompt that make your coding agent use Tiempo for availability, holds and bookings instead of writing a fragile scheduler of its own.

Source: https://tiempo.one/docs/build-with-ai

Ask a coding agent for a salon or court-booking app and it will write its own scheduler: a table of appointments and a check for overlaps before each insert. That works in the demo and double-books in production, when two customers check at the same moment, when a payment takes longer than expected, or when the clocks change. [Here’s why](https://tiempo.one/docs/double-booking).

Give the agent Tiempo instead. Two things help it get it right first time: a rules file in your project, and a prompt that says what Tiempo is for.

## The rules file

It tells the agent where Tiempo’s docs are, how your app’s ideas map onto it (shops are accounts, staff are resources, a service is a duration), the booking flow, and the mistakes to avoid. It’s plain HTTP, so it works in any language and framework.

Download it into your project, or copy it from here:

```bash
curl -o AGENTS.md https://tiempo.one/tiempo-rules.md
```

AGENTS.md

```
# Scheduling with Tiempo

This project uses Tiempo for all scheduling: availability, holds and bookings. Tiempo is an
HTTP API. Call it with fetch (or any HTTP client) from server code; there's no SDK to install.

- Agent docs: https://tiempo.one/llms.txt (index) and https://tiempo.one/llms-full.txt (everything)
- Exact contract: https://api.tiempo.one/openapi.json
- Base URL: https://api.tiempo.one
- Auth: `Authorization: Bearer $TIEMPO_KEY` from an environment variable. Test keys start
  `tpt_`, live keys `tpk_`. Server-side only: never send the key to a browser or app.
- Each business on our platform is a Tiempo account. Create it once (POST /v1/accounts with
  name and IANA timezone), store its id (`acct_…`) on our business record, and send
  `Tiempo-Account: acct_…` on every request about that business.

## How our app maps onto Tiempo

| In our app | In Tiempo |
|---|---|
| Business, shop, club | Account |
| Staff member, chair, room, court, equipment | Resource (`type` is free text) |
| Opening hours, working hours, rotas | Schedule: weekly local times from a date; several weeks = a rota. A resource with no schedule is closed |
| Holiday, sick day, closure, maintenance | Block |
| A one-off change to one day's hours | Schedule override |
| A service (a 45-minute cut) | Segments on a search or booking: how long, which resources |
| "Any available" staff or court | `resources: [...]` candidates, in our order of preference |
| A class with N places | A booking with `seats: N`; each customer is an attendee |
| A repeating appointment | A series (an RRULE) |

Keep in our own database: services, prices, customers, payments and messages. Store Tiempo's
ids (`acct_`, `res_`, `bkg_`) next to our records. Don't copy schedules or availability.

## The booking flow

1. Search: `POST /v1/availability` with `from`, `to` (UTC instants, up to 31 days), the
   business `timezone`, and `segments: [{ "resources": ["res_…"], "duration_minutes": 45 }]`.
   Show the returned `slots`. Never work out availability ourselves. The slot spacing is the
   business's setting (`slot_interval_minutes`, `slot_alignment` on the account); set it
   there rather than on every search.
   If staff take different times for a service, send one `duration_minutes` on the search
   (not on segments) plus `duration_overrides: { "res_bob": 30 }`; each slot is then one
   possible booking (a start can appear once per length), and you book the person picked.
2. Hold: `POST /v1/bookings` with `segments: [{ "resources" or "resource", "start", "end" }]`
   and our ids in `metadata`. It returns `status: "pending"` and `expires_at` (15 minutes by
   default). The hold occupies the time while the customer pays.
3. Take payment in our app. Then `POST /v1/bookings/{id}/confirm`. If the customer leaves, do
   nothing: the hold expires and the time frees up.
4. `409 booking_conflict` means the time was taken: show other slots, don't retry the request.
5. Changes: `POST /v1/bookings/{id}/cancel`, `POST /v1/bookings/{id}/reschedule` with new
   segments. To book without a hold, create with `"status": "confirmed"`.
6. Staff screens: when the business books a client in itself (after hours, on a day off), send
   `"conflict_policy": "capacity_only"`: working hours are ignored but it still never overlaps
   another booking. Only a deliberate "book anyway" uses `"allow"`. Online customers use the
   default.
7. Mark bookings `complete` when they're paid or done (allowed before the start); the time
   stays taken. Only `cancel` frees it.

## Rules

- Times are UTC instants (`2026-11-16T09:00:00Z`). Convert to the business's timezone only for
  display. To list a local day: `GET /v1/bookings?from_date=2026-11-16&to_date=2026-11-16`.
- Send an `Idempotency-Key` on every POST, made from our own record and action
  (`apt_123-hold`), so retries never create a second booking.
- Webhooks: one endpoint for the whole platform (`POST /v1/webhook_endpoints` with `url` and
  `enabled_events: ["*"]` or exact types like `booking.confirmed`; no wildcards like
  `booking.*`). Verify `Tiempo-Signature` (`t=…,v1=…`: HMAC-SHA256 of `${t}.${rawBody}` with
  the endpoint's `tpw_` secret; reject if more than 5 minutes old), dedupe on `event.id`, and
  reply 2xx within 10 seconds.
- Update our records from webhooks (`booking.confirmed`, `booking.cancelled`, `booking.expired`,
  `booking.rescheduled`, `booking.conflicted`), not by polling.
- Errors are `{ "error": { "type", "code", "message", "param" } }`. Unknown fields are rejected
  (`400 parameter_unknown`), so send only documented fields.
- Limits: 100 requests a second per platform live, 25 in test. On `429 rate_limited` wait
  `Retry-After` seconds and retry with the same `Idempotency-Key`; don't loop without waiting.
- Build and test with a test key first; switch to the live key only in production config.
- Don't build these, Tiempo does them: availability calculation, slot locking, hold expiry
  timers, overlap checks, recurring-booking expansion, timezone and DST handling.

```

## Where it goes

-   **Claude Code:** save it as `CLAUDE.md` in the project root, or keep it as `AGENTS.md` and add the line `@AGENTS.md` to your `CLAUDE.md`.
-   **Cursor:** add it as a project rule, `.cursor/rules/tiempo.mdc`, with `alwaysApply: true`.
-   **Codex, Zed and other agents** that read `AGENTS.md`: keep it in the project root.
-   **Lovable, Bolt and v0:** paste it into the project’s knowledge or instructions. Keep the Tiempo key in a server-side secret (an edge function or API route), never in the browser.

.cursor/rules/tiempo.mdc

```
---
description: Scheduling with Tiempo (availability, holds, bookings)
alwaysApply: true
---
(paste the rules file here)
```

## A starter prompt

Name Tiempo, point at `llms.txt`, and describe the flow in your own words: who the businesses are, what gets booked, and when payment happens. Get a test key from [sign-up](https://tiempo.one/signup) and put it in `TIEMPO_KEY` before the agent starts.

For classes, courts or a chain of shops, swap in the matching guide: [classes](https://tiempo.one/docs/classes), [courts and rooms](https://tiempo.one/docs/courts-and-rooms), [multi-location](https://tiempo.one/docs/multi-location).

Prompt

```
Build a booking app for barbershops. Use Tiempo for all scheduling
(availability, holds, bookings): read https://tiempo.one/llms.txt first and follow
the rules in AGENTS.md. Each shop is a Tiempo account, each barber a resource.
Customers pick a service and a time, we hold the slot, take payment with
Stripe, then confirm the booking. Use my test key from TIEMPO_KEY.
```

## Check what it built

Agents drift. Before you ship, check that the code:

-   Gets open times from `POST /v1/availability`, and has no overlap query or slot table of its own.
-   Holds before taking payment and confirms after, so two customers paying at once can’t both win the slot.
-   Sends an `Idempotency-Key` on every POST and handles `409 booking_conflict` by offering other times.
-   Keeps the key on the server, and verifies `Tiempo-Signature` on webhooks before trusting them.
-   Stores times as UTC instants and converts with the business’s timezone only for display.

Then run the race yourself: the [test harness](https://tiempo.one/app/harness) in the dashboard books the same slot twice and shows the refusal. The [testing guide](https://tiempo.one/docs/testing) has more.

The machine-readable description is at [/openapi.json](https://tiempo.one/openapi.json). Try calls in [Swagger UI](https://tiempo.one/swagger-ui.html).[Tiempo home](https://tiempo.one/)[Dashboard](https://tiempo.one/app)
