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

ScopeAllows
readEvery GET, and subscribing to and removing hooks
writePOST /appointments and POST /handoffs/:id/resolve
read:transcriptsCall 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 429 with rate_limited and 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 429 instead of 401. 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_limit with window set to hour or day. 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. limit is 1 to 100 (25 by default). Pass next_cursor back as cursor for 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

EndpointScopeWhat it does
GET /meanyThe business the key belongs to, and the key’s name, prefix and scopes
GET /calls, GET /calls/:idreadCalls: start and end, duration, outcome, caller name and number, summary. With read:transcripts, the transcript too
GET /appointments, GET /appointments/:idreadBookings, filterable with ?status=confirmed|rescheduled|cancelled|completed
POST /appointmentswriteMake a booking, with the receptionist’s own checks
GET /customers, GET /customers/:idreadName, phone and email only. ?phone= matches on the last ten digits; ?email= ignores case
GET /handoffs, GET /handoffs/:idreadMessages for the team: caller, reason, summary, urgency and status
POST /handoffs/:id/resolvewriteMark a message done, as the dashboard’s Done button does
POST /hooks, GET /hooks, DELETE /hooks/:idreadSubscribe 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, and service_id or service_name. Optional: customer_email, staff_name, notes, party_size (group classes only) and intake_notes, for the details some trades must ask first, such as a garage’s vehicle registration.
  • A start_time with an offset or Z is 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 201 with the appointment, or 200 with the existing one when this customer already holds a booking at that time. The account needs a live subscription (402 otherwise), and a business that does not take appointments answers 409 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 a customer_text_not_sent warning 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." } }
StatusCodes
400invalid_request, invalid_cursor, invalid_target_url
401unauthorized: a missing, unknown or revoked key, or a closed account
402subscription_inactive: bookings need a live subscription
403insufficient_scope
404not_found
409slot_taken, not_available, bookings_not_offered, booking_not_set_up, request_in_progress, too_many_hooks
422name_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
429rate_limited, api_booking_limit
500, 503internal_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.

EventWhenCarries
appointment.bookedA booking is made, or a cancelled one restoredThe appointment
appointment.rescheduledA booking movesThe appointment
appointment.cancelledA booking is cancelledThe appointment
call.completedA call’s record is written, within about a minuteThe call, never the transcript
handoff.createdA caller leaves a message for the teamThe message
order.placed, order.updated, order.cancelledFood ordersThe order
booking_request.created, room.bookedHotel staysThe 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 as X-Ringmere-Event-Id, is the same on each retry and for each subscriber, so store the ids you have processed and skip repeats. occurred_at is when it happened, not when it was delivered, and order is not guaranteed.
  • Answer with a 2xx to acknowledge. A 5xx, 408, 429, a timeout (5 seconds) or no answer is retried with backoff; any other 4xx stops it.
  • Answer 410 Gone and 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?
Yes to an API. Every plan includes a REST API that reads your calls, bookings, customers and messages for the team, makes bookings and marks messages done, plus signed webhooks and REST hooks that tell your own system when something happens. For Zapier, the Webhooks by Zapier “Catch Hook” trigger works today: point a Ringmere webhook at it and bookings, changes and messages flow into Zapier and on to thousands of apps. An official Ringmere app in the Zapier directory is planned, and is not published yet.
Which plans include the API?
Every plan, Starter included. Making a booking through the API needs a live subscription, as answering the phone does.
Can I call the API from a web page?
No. The API is for servers. It sends no CORS headers, a browser request carrying another site’s Origin is refused, and a key in front-end code would be readable by anyone. Call it from your own server, or from a tool such as Zapier.
How do I stop receiving a hook?
Delete it with the API, revoke the key that made it in Settings, or answer a delivery with 410 Gone: Ringmere then removes the subscription and sends nothing more to it.

Stop missing calls. Start booking them.

Put Ringmere on your line today and never lose another booking to a missed call.

Call answered2:47am · first ring
Booked in your calendarCut & blow dry · 2:30pm
£42 recoveredWhile you slept
Start your 7-day free trialTalk to us