DevRoam API
Connect Roa to your CRM, website or automations: check free times, create and change appointments, and get a webhook every time Roa books, answers a call or takes a message.
The API and webhooks are included in the Professional and Advanced plans. No code? See the Zapier and Make guide.
Quickstart
- In the app go to Settings → API and tap Create key. Copy it: it starts with
dr_live_and won’t be shown again. - Store it as the environment variable
DEVROAM_API_KEY(never in browser code). - Ask for the free times on a day:
curl "https://app.devroam.com/api/v1/availability?date=2026-11-03" \
-H "Authorization: Bearer $DEVROAM_API_KEY"const res = await fetch("https://app.devroam.com/api/v1/availability?date=2026-11-03", {
headers: { Authorization: `Bearer ${process.env.DEVROAM_API_KEY}` },
});
console.log(await res.json());import os, requests
r = requests.get("https://app.devroam.com/api/v1/availability?date=2026-11-03",
headers={"Authorization": f"Bearer {os.environ['DEVROAM_API_KEY']}"})
print(r.json())Response: that day’s free times in the business’s local time, ready to show or to book with POST /api/v1/appointments.
Authentication
Send the key on every request in the Authorization: Bearer dr_live_… header. Each key belongs to one business; create several (one per integration) and revoke them any time in Settings. A revoked key stops working immediately (401).
Dates and times
All times are the business’s local time in YYYY-MM-DD HH:MM format (for example 2026-11-03 17:30). Don’t send a time zone or convert to UTC: DevRoam applies the business time zone and daylight-saving changes.
Endpoints
Base URL: https://app.devroam.com
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/business | Business details (name, sector, time zone…). |
| GET | /api/v1/services | Services with name, price, duration and buffer. |
| GET | /api/v1/staff | Team members (id and name). Use the id as staff_id. |
| GET | /api/v1/availability | Free times for one day. Same engine as Roa and the booking page: opening hours and split shifts, public holidays, blocks, notice, capacity and existing appointments.date (YYYY-MM-DD, required) · staff_id · service · party_size · area |
| GET | /api/v1/appointments | List appointments or reservations.from · to · status · limit |
| POST | /api/v1/appointments | Create an appointment. Returns 201, or 409 if the time is no longer free.Body: customer_name*, start_local*, customer_phone, service, staff_id, notes, party_size, area, notify_customer (default true) |
| GET | /api/v1/appointments/{id} | One appointment. |
| PATCH | /api/v1/appointments/{id} | Change the time, details or notes. Same fields as create. |
| DELETE | /api/v1/appointments/{id} | Cancel the appointment. |
| GET | /api/v1/calls | Latest calls answered by Roa.limit |
| GET | /api/v1/calls/{id} | One call with its transcript. |
| GET | /api/v1/customers | Known customers (callers).q · limit |
| GET | /api/v1/messages | Messages taken by Roa.limit |
Example: list services
curl "https://app.devroam.com/api/v1/services" \
-H "Authorization: Bearer $DEVROAM_API_KEY"const res = await fetch("https://app.devroam.com/api/v1/services", {
headers: { Authorization: `Bearer ${process.env.DEVROAM_API_KEY}` },
});
console.log(await res.json());import os, requests
r = requests.get("https://app.devroam.com/api/v1/services",
headers={"Authorization": f"Bearer {os.environ['DEVROAM_API_KEY']}"})
print(r.json())Create an appointment
Check availability first or handle the 409: if the time was taken in the meantime, DevRoam does not create the appointment. With notify_customer (default true) the customer gets the same confirmation as when booking with Roa.
curl -X POST https://app.devroam.com/api/v1/appointments \
-H "Authorization: Bearer $DEVROAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customer_name":"Ana López","customer_phone":"612345678","service":"Haircut","start_local":"2026-11-03 17:30"}' const res = await fetch("https://app.devroam.com/api/v1/appointments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DEVROAM_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_name: "Ana López",
customer_phone: "612345678",
service: "Haircut",
start_local: "2026-11-03 17:30",
}),
});
if (res.status === 409) console.log("That time is no longer free");
else console.log(await res.json());import os, requests
r = requests.post("https://app.devroam.com/api/v1/appointments",
headers={"Authorization": f"Bearer {os.environ['DEVROAM_API_KEY']}"},
json={"customer_name": "Ana López", "customer_phone": "612345678",
"service": "Haircut", "start_local": "2026-11-03 17:30"})
print(r.status_code, r.json())Webhooks
In Settings → Webhooks add an HTTPS URL and choose the events. DevRoam sends a JSON POST with id, type, created, business_id and data.
booking.created | Appointment created (by Roa, the booking page, the dashboard or the API) |
booking.changed | Appointment changed |
booking.cancelled | Appointment cancelled |
call.completed | Call finished |
message.received | New message |
test.ping | "Send test" button |
Verify the signature
Every delivery carries DevRoam-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 with your webhook secret over t + "." + body. Verify it against the raw body:
# Header received:
# DevRoam-Signature: t=1761231000,v1=5f2c…
# v1 = hex HMAC-SHA256(secret, "<t>.<raw body>")import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return fresh && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body,
hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(parts["v1"], expected)Retries
Reply with a 2xx within 8 seconds. Otherwise DevRoam retries after 1 min, 5 min, 30 min, 2 h and 6 h. Use the event id to ignore duplicates.
Errors and limits
400 | Invalid request (missing field or wrong date format) |
401 | Missing, invalid or revoked key |
403 | Your plan does not include the API, or the subscription is not active |
404 | Not found (or not in your business) |
409 | The time is not available |
429 | More than 120 requests per minute with the same key. Wait the seconds in the Retry-After header |
Errors return JSON: {{"error":{{"status":401,"code":"invalid_api_key","message":"…","docs":"…"}}}}. Branch on code; message is human-readable and in English when you send Accept-Language: en.
OpenAPI specification
Download the OpenAPI 3.1 spec to generate a client or import it into Postman or Insomnia: openapi.json.