← Developers

API reference v1

Every endpoint version 1 serves. Additive only: fields and endpoints may be added, and nothing already here is renamed, removed or retyped.

See also webhooks and the MCP server.

Authentication

Send the key as a bearer token on every request. The session cookie is not accepted here, and the key names the business, so no path carries a business identifier.

curl -H "Authorization: Bearer torim_…" \
  https://torim.ai/api/v1/business

The machine-readable schema for everything below is at /api/v1/openapi.json.

Scopes

Chosen when the key is created and fixed for its life. A call needing a scope the key does not hold answers 403 insufficient_scope.

catalogue:readThe business, its services, its team and its free times. Every key holds this.
appointments:readThe diary: who is booked, when, and how to reach them.
appointments:writeBook, move and cancel appointments.
catalogue:writeChange services, opening hours and closures.

business

GET/api/v1/business

The business this key belongs to

No identifier is passed: the key names the business.

200 401 invalid_token, token_revoked, token_expired403 insufficient_scope — needs catalogue:read429 rate_limited

catalogue

GET/api/v1/closures

Days the business is shut, from today on

200 403 insufficient_scope — needs catalogue:read

POST/api/v1/closures

Shut the business for a day or a run of days

Body

  • from date, required — First day closed, `YYYY-MM-DD`.
  • reason string,null
  • to date — Last day closed, inclusive. Defaults to `from`, since a one-day closure is the common case.

201 400 invalid_range403 insufficient_scope — needs catalogue:write

DELETE/api/v1/closures/{id}

Reopen a day that was closed

Parameters

  • id path, required — The closure's id

204 Reopened403 insufficient_scope — needs catalogue:write404 no such closure here

GET/api/v1/hours

The week the business is open

200 403 insufficient_scope — needs catalogue:read

PUT/api/v1/hours

Replace that week

The whole week, not a patch: "Tuesday now closes at six" and "Tuesday is gone" are the same edit to a caller holding the week. Sending an empty list closes the business, which is a real thing to want and not an error.

Body

  • rules OpeningRuleBody[], required

200 400 invalid_window, overlapping_hours, invalid_weekday403 insufficient_scope — needs catalogue:write

GET/api/v1/services

Every service, including retired ones

200 403 insufficient_scope — needs catalogue:read

POST/api/v1/services

Add a service

The slug is chosen by the server and returned — it has to be unique within the business, and a caller-proposed one is either a collision it cannot act on or a uniqueness check that races.

Body

  • arrival_window_min int32 — For a home visit, the arrival window it is booked as, in minutes — "between eight and eleven" is 180 — or `null` for an exact time. A multiple of half an hour, up to eight hours. Absent leaves it as it is, for the reason the instructions give; anything but a home visit has none.
  • description_en string,null
  • description_he string,null
  • duration_min int32, required
  • instructions_en string,null
  • instructions_he string,null — What a customer should read before the visit, in paragraphs — shown on the page, the confirmation and the visit card. Empty or `null` clears them; **absent leaves them as they are**, unlike every field above. The exception is on purpose: `/api/v1` takes this same body, and an integration written before these fields existed sends a whole service without them. Read as "none", its next update would silently wipe what the business wrote for its customers.
  • location string — `on_site` (the default), `online` or `at_home`. An online service carries a room in `meeting_url`; the other two refuse one.
  • meeting_url string,null
  • name_en string,null
  • name_he string, required
  • occupy_min int32 — How long a booking holds the person performing it, when that differs from how long the customer is booked for. Absent means it does not.
  • price_agorot int32, required
  • rebook_after_days int32 — How many days after a visit to nudge the customer to book the next one — between [`REBOOK_AFTER_DAYS`]'s ends, or absent for never. Only people who agreed to hear from the business are nudged.
  • show_duration boolean,null — Whether customers are told how long it takes. The length still sets the times offered either way. Absent leaves it as it is — on, for a new service — for the reason the instructions give below.
  • show_price boolean
  • sort_order int32
  • visits_per_window int32 — For a home visit booked as a window, how many visits one person takes in it — `null` for as many as the visit's length fits, never more. Absent leaves it as it is.

201 400 invalid_duration, invalid_occupancy, invalid_price and the other field checks403 insufficient_scope — needs catalogue:write

PATCH/api/v1/services/{slug}

Change a service

Every field is replaced, so send the whole service. Changing a price leaves appointments already booked alone — each one keeps what it was booked with.

Parameters

  • slug path, required — The service's slug

Body

  • arrival_window_min int32 — For a home visit, the arrival window it is booked as, in minutes — "between eight and eleven" is 180 — or `null` for an exact time. A multiple of half an hour, up to eight hours. Absent leaves it as it is, for the reason the instructions give; anything but a home visit has none.
  • description_en string,null
  • description_he string,null
  • duration_min int32, required
  • instructions_en string,null
  • instructions_he string,null — What a customer should read before the visit, in paragraphs — shown on the page, the confirmation and the visit card. Empty or `null` clears them; **absent leaves them as they are**, unlike every field above. The exception is on purpose: `/api/v1` takes this same body, and an integration written before these fields existed sends a whole service without them. Read as "none", its next update would silently wipe what the business wrote for its customers.
  • location string — `on_site` (the default), `online` or `at_home`. An online service carries a room in `meeting_url`; the other two refuse one.
  • meeting_url string,null
  • name_en string,null
  • name_he string, required
  • occupy_min int32 — How long a booking holds the person performing it, when that differs from how long the customer is booked for. Absent means it does not.
  • price_agorot int32, required
  • rebook_after_days int32 — How many days after a visit to nudge the customer to book the next one — between [`REBOOK_AFTER_DAYS`]'s ends, or absent for never. Only people who agreed to hear from the business are nudged.
  • show_duration boolean,null — Whether customers are told how long it takes. The length still sets the times offered either way. Absent leaves it as it is — on, for a new service — for the reason the instructions give below.
  • show_price boolean
  • sort_order int32
  • visits_per_window int32 — For a home visit booked as a window, how many visits one person takes in it — `null` for as many as the visit's length fits, never more. Absent leaves it as it is.

200 400 a field failed its check404 no such service here

POST/api/v1/services/{slug}/active

Retire a service, or bring it back

There is no delete. A slug appears in booking links already handed out and in appointments already made; retiring stops new bookings and leaves both intact.

Parameters

  • slug path, required — The service's slug

Body

  • active boolean, required

200 404 no such service here

GET/api/v1/team

Who works here

200 403 insufficient_scope — needs catalogue:read

availability

GET/api/v1/availability

When one service can be booked

Answered by the same code the public booking page uses, so a time offered here is a time `POST /appointments` will accept.

Parameters

  • service query, required — Service **slug**, as `/api/v1/services` returns it.
  • from query — First day to look at, `YYYY-MM-DD`. Defaults to today where the business is.
  • days query — How many days to look at. Capped by the business's own booking horizon.

200 400 invalid_range404 no such service

appointments

GET/api/v1/appointments

The diary, a window at a time

`to` is inclusive and defaults to six days after `from`. The window may not exceed 62 days.

Parameters

  • from query — First day of the window, `YYYY-MM-DD`. Defaults to today.
  • to query — Last day of the window, inclusive. Defaults to six days after `from`.

200 400 invalid_range403 insufficient_scope — needs appointments:read

POST/api/v1/appointments

Book somebody in

The slot is re-derived server-side and the price is read from the service, so neither is taken from this request. The booking is the business's own, so it is `confirmed` at once: it never waits for approval, and neither an unsigned consent form nor a rule about a customer's no-shows or cancellations refuses it — those govern customers booking for themselves.

Body

  • customer_email string,null — The only way this booking can ever be written to — a walk-in with no address can be told nothing when it moves.
  • customer_name string, required
  • customer_phone string,null
  • note string,null — Free text for the diary. A service at the customer's home takes no address here: only a customer's own account sets one, so a walk-in's address is written in the note, as the desk does.
  • off_grid boolean — Take `start` as given rather than checking it against the slot grid. Defaults to false, which is the safe answer for a program: ask `/availability` and book one of the times it gave you. Set it when you are recording something that happened away from the grid — a walk-in at 10:07 — and accept that nothing then checks the time is sensible except the double-booking constraint.
  • service string, required — Service **slug**.
  • staff_id uuid, required — Which member of the team performs it. From `/api/v1/team`.
  • start date-time, required — Absolute start instant, RFC 3339.

201 400 invalid_name, invalid_email, invalid_service403 insufficient_scope — needs appointments:write404 no such service, or that person does not work here409 slot_unavailable, slot_taken, session_full

GET/api/v1/appointments/{id}

One appointment

Parameters

  • id path, required — The appointment's id

200 404 no such appointment here

POST/api/v1/appointments/{id}/cancel

Call one off

Frees the slot and tells the waiting list. Where the business keeps a freed time for whoever has waited longest, `/availability` does not offer it while it is kept. A `pending` request may be cancelled here too; this is a cancellation, not the diary's decline, and the customer is not sent the decline message.

Parameters

  • id path, required — The appointment's id

200 400 illegal_status404 no such appointment here

POST/api/v1/appointments/{id}/move

Reschedule

One update under the same lock a booking takes, so a lost race leaves the appointment where it was rather than nowhere.

Parameters

  • id path, required — The appointment's id

Body

  • off_grid boolean — As on `CreateAppointment`.
  • start date-time, required — The new start instant, RFC 3339.

200 404 no such appointment here409 already_cancelled, slot_unavailable, slot_taken

Appointment statuses

Every appointment carries a status. Statuses may be added within v1 — pending was — so do not treat the list as closed.

pendingA customer's booking request, waiting for the business to approve it — only where the business asks for approval. It holds its time like a confirmed booking. Approved, it becomes confirmed; declined, or unanswered about an hour before it starts, cancelled. A booking made with a key is never pending.
confirmedBooked, and still ahead or not yet marked.
completedMarked done in the diary.
no_showMarked in the diary as somebody who did not come.
cancelledCalled off, by either side. Cancelled appointments stay in the list.

A customer may book several services back to back as one visit. Each part is an appointment of its own here, and every part carries the same visit_id; a booking of one service has none. Cancelling or moving a part through this API leaves the others where they are.

A booking made with a key is the business's own: it is confirmed at once, and neither an unsigned consent form nor a rule about a customer's no-shows or cancellations refuses it. Those apply to customers booking for themselves.

Errors

Every failure is JSON carrying a stable code and an English error. Switch on the code; the sentence is for you reading logs, and may be reworded.

{ "code": "insufficient_scope", "error": "this token does not hold the permission for that" }
401invalid_tokenNo bearer token, or no such token.
401token_revokedThe key was revoked.
401token_expiredThe key is past its expiry.
403insufficient_scopeThe key does not hold the scope this call needs.
429rate_limitedToo many requests on this key. Back off.
404not_foundNo such thing, or it belongs to somebody else.
409slot_unavailableThat time is not one the grid offers.
409slot_takenSomebody reached that slot first.
500server_errorSomething broke on our side.

Rate limit and versioning

600 requests an hour per key, counted per key rather than per account — two integrations do not share a budget. Over it answers 429 rate_limited.

Version 1 is additive only: fields and endpoints may be added, and nothing already here is renamed, removed or retyped. Requests reject unknown fields, so a misspelled one is a 400 rather than a value silently ignored.

Something not answered here? Get in touch.