Home / Integrations / API
The Ringmere API.
Read your calls, bookings, customers and messages for the team, make bookings from your own system, and hear about each booking and message the moment it happens. Version 1, on every plan.
The Ringmere API is a JSON REST API at https://app.ringmere.com/api/v1. Authenticate with a key you make in Settings, sent as a Bearer token. Keys have scopes (read, write and call transcripts), each key allows 120 requests a minute and each business 300 across all its keys, and a booking made through the API goes through the same checks as one made on a call. REST hooks deliver signed events to your own HTTPS URL.
Getting a key
The account holder makes keys in the dashboard: Settings, Calendar, in the API keys and webhooks card. Give the key a name, choose its scopes, and copy it when it is shown: it appears once, and Ringmere keeps only a keyed digest of it, never the key itself. Making a key asks you to confirm it is you, with your authenticator code if two-factor is on, or your password. Revoking a key stops it at once and deletes the hooks it registered.
A key belongs to one business. An agency makes one for each clinic.
Authentication
Send the key in the Authorization header on every request. It is read from that header only, never from the query string or a cookie, because URLs end up in logs. A dashboard sign-in is not an API key and is refused.
GET /api/v1/me
Authorization: Bearer rmk_live_…
The API is for servers, not browsers. It sends no CORS headers, a browser POST or DELETE that carries another site’s Origin is refused with 403, and a key must never be put in front-end code.
Scopes
| Scope | Allows |
|---|---|
read | Every GET, and subscribing to and removing hooks |
write | POST /appointments and POST /handoffs/:id/resolve |
read:transcripts | Call transcripts, which are personal data in callers’ own words. Needs read as well. |
A request without the scope it needs gets 403 insufficient_scope. Write access never goes beyond what the dashboard gives the owner.
Limits
- 120 requests a minute per key, and 300 a minute per business across all its keys, both counted across all of Ringmere’s servers. Past either you get
429withrate_limitedand the standard RateLimit headers. - Refused keys are slowed, not valid ones. Requests with a missing, unknown or revoked key are counted per address, and past 100 a minute they get
429instead of401. A valid key is never held back by other keys failing from the same address. - 30 bookings an hour and 200 a day per business through the API, because each booking texts the customer. Past either you get
429 api_booking_limitwithwindowset tohourorday. A retry of a booking already made does not count, and bookings the receptionist makes on calls are never held to it. - Up to 25 hooks per business.
Conventions
- JSON in and out. Times are ISO 8601 in UTC. Ids are UUIDs and never change. Money is a number in pounds.
- One record comes back as
{ "data": { … } }; a list as{ "data": [ … ], "next_cursor": "…" }. - Lists are newest first.
limitis 1 to 100 (25 by default). Passnext_cursorback ascursorfor the next page; it is null on the last page, and a cursor never skips or repeats a row.
Endpoints
Base URL: https://app.ringmere.com/api/v1
| Endpoint | Scope | What it does |
|---|---|---|
GET /me | any | The business the key belongs to, and the key’s name, prefix and scopes |
GET /calls, GET /calls/:id | read | Calls: start and end, duration, outcome, caller name and number, summary. With read:transcripts, the transcript too |
GET /appointments, GET /appointments/:id | read | Bookings, filterable with ?status=confirmed|rescheduled|cancelled|completed |
POST /appointments | write | Make a booking, with the receptionist’s own checks |
GET /customers, GET /customers/:id | read | Name, phone and email only. ?phone= matches on the last ten digits; ?email= ignores case |
GET /handoffs, GET /handoffs/:id | read | Messages for the team: caller, reason, summary, urgency and status |
POST /handoffs/:id/resolve | write | Mark a message done, as the dashboard’s Done button does |
POST /hooks, GET /hooks, DELETE /hooks/:id | read | Subscribe a URL to an event, list your hooks, remove one |
Another business’s id answers 404 not_found, exactly as an id that does not exist: the API never says whether it does.
A call
{
"id": "8f0c…", "started_at": "2026-10-12T09:00:00.000Z", "ended_at": "2026-10-12T09:01:35.000Z",
"duration_seconds": 95, "outcome": "booked", "caller_name": "Priya Shah",
"caller_number": "+447700900123", "summary": "Priya booked a cut for Tuesday at 2pm.",
"handoff_status": "none", "created_at": "2026-10-12T09:01:36.000Z"
}
With read:transcripts, GET /calls/:id adds a transcript list of speaker and text, and GET /calls?include=transcript adds it to each item. Without the scope it is never included, whatever is asked.
An appointment
{
"id": "3c6a…", "status": "confirmed", "customer_name": "Priya Shah", "customer_phone": "+447700900123",
"service_id": "9a1b…", "service_name": "Cut and finish", "staff_name": "",
"start_time": "2026-10-14T13:00:00.000Z", "end_time": "2026-10-14T13:45:00.000Z",
"duration_minutes": 45, "price": 38, "party_size": null, "notes": "",
"source": "ai_call", "created_at": "…", "updated_at": "…"
}
source is ai_call (booked by the receptionist), manual (by hand) or api.
Making a booking
POST /api/v1/appointments
Authorization: Bearer rmk_live_…
Idempotency-Key: 7b1c8e2a-website-form-123
Content-Type: application/json
{
"customer_name": "Priya Shah",
"customer_phone": "07700 900123",
"customer_email": "priya@example.com",
"service_name": "Cut and finish",
"start_time": "2026-10-14T14:00",
"notes": "From the website"
}
- Required:
customer_name,customer_phone,start_time, andservice_idorservice_name. Optional:customer_email,staff_name,notes,party_size(group classes only) andintake_notes, for the details some trades must ask first, such as a garage’s vehicle registration. - A
start_timewith an offset orZis that instant; without one it is the business’s own local time. - It books exactly as the receptionist does: opening hours (or a class’s own times), staff hours, the minimum notice, the diary and its capacity, and a connected Google or Microsoft calendar, which is checked live. The customer gets their confirmation, the owner gets an alert saying the booking came through the API, and your webhooks and hooks fire.
- It answers
201with the appointment, or200with the existing one when this customer already holds a booking at that time. The account needs a live subscription (402otherwise), and a business that does not take appointments answers409 bookings_not_offered. - Customer texts (the confirmation, changes and reminders) go only to a UK number for a booking made through the API. A booking for a number abroad is still made, an email confirmation still goes if you sent
customer_email, and the reply carries acustomer_text_not_sentwarning so you know.
Idempotency
Send an Idempotency-Key header (1 to 255 visible ASCII characters) with a booking and a retry is safe. The same key with the same body within 24 hours gets the first answer back, with Idempotent-Replayed: true, and books nothing. The same key with a different body is 422 idempotency_key_reused, and while the first request is still running it is 409 request_in_progress. A refused booking releases its key. Use an opaque value, such as a UUID or a hash of your own record’s id: the key is stored as sent, so it should not carry a name or a number.
Errors
{ "error": { "code": "slot_taken", "message": "That time is not available." } }
| Status | Codes |
|---|---|
| 400 | invalid_request, invalid_cursor, invalid_target_url |
| 401 | unauthorized: a missing, unknown or revoked key, or a closed account |
| 402 | subscription_inactive: bookings need a live subscription |
| 403 | insufficient_scope |
| 404 | not_found |
| 409 | slot_taken, not_available, bookings_not_offered, booking_not_set_up, request_in_progress, too_many_hooks |
| 422 | name_required, intake_required, contact_number_required, too_soon, not_a_class_time, staff_ineligible, staff_not_bookable, service_not_found (with the services you can book), idempotency_key_reused |
| 429 | rate_limited, api_booking_limit |
| 500, 503 | internal_error, unavailable, calendar_unavailable |
Hooks and events
Subscribe a URL to an event and Ringmere posts it there whenever it happens.
POST /api/v1/hooks
{ "target_url": "https://example.com/hooks/ringmere", "event": "call.completed" }
The reply includes a secret, shown once, which signs each delivery to that hook. The URL must be https:// on the standard port and reach a public address; it is checked when you save it and again at each delivery. DELETE /hooks/:id answers 204.
| Event | When | Carries |
|---|---|---|
appointment.booked | A booking is made, or a cancelled one restored | The appointment |
appointment.rescheduled | A booking moves | The appointment |
appointment.cancelled | A booking is cancelled | The appointment |
call.completed | A call’s record is written, within about a minute | The call, never the transcript |
handoff.created | A caller leaves a message for the team | The message |
order.placed, order.updated, order.cancelled | Food orders | The order |
booking_request.created, room.booked | Hotel stays | The stay |
Bookings made by hand in the dashboard do not fire appointment.booked; moves and cancellations made there do, when the customer is notified.
A delivery
POST <target_url>
Content-Type: application/json
X-Ringmere-Event: appointment.booked
X-Ringmere-Event-Id: 5d7c…
X-Ringmere-Signature: 3f9a…
{ "id": "5d7c…", "event": "appointment.booked", "business_id": "1f0e…",
"occurred_at": "2026-10-12T09:01:20.000Z", "data": { …the appointment… } }
- The
id, also sent asX-Ringmere-Event-Id, is the same on each retry and for each subscriber, so store the ids you have processed and skip repeats.occurred_atis when it happened, not when it was delivered, and order is not guaranteed. - Answer with a
2xxto acknowledge. A5xx,408,429, a timeout (5 seconds) or no answer is retried with backoff; any other4xxstops it. - Answer
410 Goneand the subscription is deleted, so nothing more is sent to it.
Verifying the signature
X-Ringmere-Signature is the hex HMAC-SHA256 of the exact request body, keyed with the hook’s secret. Check the raw bytes before you parse them. The webhook you set in Settings uses the same scheme with its own secret.
Node
import crypto from 'node:crypto';
export function isFromRingmere(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
const given = Buffer.from(String(signature || ''), 'hex');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
// Express:
// app.post('/hooks/ringmere', express.raw({ type: 'application/json' }), (req, res) => {
// if (!isFromRingmere(req.body, req.get('x-ringmere-signature'), process.env.RINGMERE_HOOK_SECRET)) return res.sendStatus(401);
// const event = JSON.parse(req.body);
// res.sendStatus(200);
// });
Python
import hashlib, hmac
def is_from_ringmere(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
The signature covers the body, not a time, so it proves a delivery came from Ringmere but not that it is the first time you have seen it. Keeping the id of each event you have processed refuses a replay, and absorbs our own retries too.
Webhooks and hooks
The single webhook URL in Settings (how webhooks work) sends booking, order and message events with the same signature scheme, and needs no code to set up. Hooks made through the API are delivered beside it, each with its own secret, and add call.completed. A fault in one cannot stop the other.
Personal data
What the API returns and the hooks deliver is your own customers’ data, and you are the controller of what you send on to your own systems; our Data Processing Agreement covers what we do with it. Calls carry the caller’s name and number and the summary, and the transcript only with read:transcripts, never in a hook. Customers carry name, phone and email and nothing else. When a caller asks to be erased, their records are redacted or deleted, later reads return the redacted values, and a queued delivery that names them is dropped; what a receiver has already been sent is its own to erase. Closing the account deletes its keys and hooks.
Questions
The API, plainly answered.
Does Ringmere have an API or work with Zapier?
Which plans include the API?
Can I call the API from a web page?
How do I stop receiving a hook?
Keep reading
Related.
Connect an AI receptionist to your CRM: API and webhooks
An AI receptionist that books the job is good. One whose bookings and messages land in the system you already run your business from is better.
Read moreRingmere pricing
Starter £49, Growth £99 and Pro £249 a month, each with a UK number and a 7-day free trial.
Read moreStop missing calls. Start booking them.
Put Ringmere on your line today and never lose another booking to a missed call.