Skip to content

New The AI chatbot on its own, for your website, from the Chat plan. See the chatbot

oneanswer

Build on Oneanswer

A REST API for calls, appointments, contacts and leads, and signed webhooks for everything that happens. JSON in, JSON out, one business per token.

Getting started

  1. In the dashboard, open API tokens (owners, on plans that include the API) and create a token with only the permissions you need.
  2. Copy it straight away; it’s shown once. Keep it on your server, never in a browser or app.
  3. Send it as a bearer token:
curl https://beta.ittypingtest.io/api/v1/calls?per_page=5 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: application/json"

The base URL is https://beta.ittypingtest.io/api/v1. Every token belongs to one business, so there’s never a business ID in the URL.

Tokens and permissions

Each token has a list of permissions. A request without the right one gets 403. Tokens expire after a year and stop working straight away when revoked, when their owner leaves the business, or if the business is paused.

PermissionAllows
tenant:readRead business details
team:readRead team members
calls:readRead calls, summaries and messages
appointments:readRead appointments and free times
appointments:writeBook and cancel appointments
crm:readRead contacts and leads

Requests and errors

  • JSON: send Accept: application/json; POST bodies are JSON. Times are ISO 8601; phone numbers are E.164 (+447700900123).
  • Pages: lists return links.next; request it as-is until it’s null. Up to 100 per page.
  • Rate limit: 120 requests a minute per token. Over that you get 429 with a Retry-After header.
  • Retries: send an Idempotency-Key header when booking, so repeating a request after a timeout never books twice.
  • Your request log: the API tokens page lists your last requests with status and timing (kept 30 days, no bodies).
StatusMeans
200 / 201Done.
401Missing, wrong, expired or revoked token.
403The token lacks the permission, or the business is paused.
404No such record in this business.
409That time was just taken (booking).
422Invalid input; "errors" says which field and why.
429Too many requests; wait and retry.
5xxOur problem; retry with back-off.

Business

GET /api/v1/tenant

The business tenant:read

The business this token belongs to: name, time zone, plan.

Example response
{
    "data": {
        "id": "01JC8Z…",
        "type": "tenant",
        "attributes": {
            "name": "Northside Hair Studio",
            "status": "active",
            "timezone": "Europe/London",
            "industry": "salon",
            "phone": "+441614960000",
            "website": "https://example.com",
            "trial_ends_at": null
        }
    }
}

GET /api/v1/team

Team members team:read

Everyone on the business’s team, with their role.

Example response
{
    "data": [
        {
            "type": "member",
            "attributes": {
                "name": "Priya Shah",
                "email": "[email protected]",
                "role": "manager",
                "joined_at": "2026-09-02T08:15:00Z"
            }
        }
    ],
    "meta": {
        "count": 1
    }
}

Calls

GET /api/v1/calls

List calls calls:read

Newest first, with the summary.

NameInDescription
outcomequeryOnly calls with this outcome.One of: resolved, booked, rescheduled, booking_cancelled, lead_captured, transferred, message_taken, abandoned, forwarded, limit_reached, error, voicemail, declined, opted_out
sincequeryOnly calls that started at or after this time.
per_pagequeryResults per page, 1–100 (default 25).
cursorqueryFrom links.next of the previous page.
Example response
{
    "data": [
        {
            "id": "01JD2K…",
            "type": "call",
            "attributes": {
                "caller": "+447700900123",
                "called": "+441614960000",
                "outcome": "booked",
                "status": "completed",
                "started_at": "2026-10-21T09:41:12Z",
                "ended_at": "2026-10-21T09:43:40Z",
                "duration_seconds": 148,
                "transferred_to": null,
                "summary": "Sam booked a men’s cut for Thursday at 2pm."
            }
        }
    ],
    "links": {
        "next": "https://app.oneanswer.io/api/v1/calls?cursor=eyJ…",
        "prev": null
    }
}

GET /api/v1/calls/{call}

One call calls:read

Add include=transcript for everything that was said.

NameInDescription
call*pathThe call ID.
includequeryExtra data to include.One of: transcript
Example response
{
    "data": {
        "id": "01JD2K…",
        "type": "call",
        "attributes": {
            "caller": "+447700900123",
            "called": "+441614960000",
            "outcome": "booked",
            "status": "completed",
            "started_at": "2026-10-21T09:41:12Z",
            "ended_at": "2026-10-21T09:43:40Z",
            "duration_seconds": 148,
            "transferred_to": null,
            "summary": "Sam booked a men’s cut for Thursday at 2pm.",
            "transcript": [
                {
                    "role": "assistant",
                    "text": "Hello, Northside Hair Studio, Ava speaking.",
                    "at": "2026-10-21T09:41:13Z"
                },
                {
                    "role": "caller",
                    "text": "Hi, can I book a cut for Thursday?",
                    "at": "2026-10-21T09:41:18Z"
                }
            ]
        }
    }
}

GET /api/v1/search

Search what customers said calls:read

Searches call transcripts and summaries, website chats and texts. Every word must appear; use "quotes" for a phrase and -word to leave one out. Results follow the token owner’s permissions.

NameInDescription
q*queryWhat to search for.
channelsqueryComma-separated: calls, chats, texts (default all).
speakerquerycustomer = only what the customer said.One of: customer, anyone
fromqueryFrom this day (YYYY-MM-DD).
toqueryUp to this day.
limitquery1–100 (default 25).
Example response
{
    "data": [
        {
            "type": "call",
            "id": "01JD2K…",
            "at": "2026-10-21T10:42:00+01:00",
            "title": "Call from Sam Taylor",
            "snippets": [
                "Caller: Do you take NHS patients?"
            ]
        }
    ],
    "query": {
        "words": [
            "nhs"
        ],
        "excluded": []
    }
}

Appointments

GET /api/v1/appointments

List appointments appointments:read

In start time order.

NameInDescription
fromqueryStarting at or after.
toqueryStarting before.
statusqueryOnly this status.One of: booked, completed, no_show, cancelled
per_pagequeryResults per page, 1–100 (default 25).
cursorqueryFrom links.next of the previous page.
Example response
{
    "data": [
        {
            "id": "01JD3M…",
            "type": "appointment",
            "attributes": {
                "reference": "K7P4M",
                "status": "booked",
                "service": "Men’s haircut",
                "calendar": "Sam",
                "starts_at": "2026-10-22T14:00:00+01:00",
                "ends_at": "2026-10-22T14:30:00+01:00",
                "source": "call",
                "customer": {
                    "name": "Sam Taylor",
                    "phone": "+447700900123",
                    "email": null
                }
            }
        }
    ],
    "links": {
        "next": null,
        "prev": null
    }
}

GET /api/v1/availability

Free times appointments:read

Bookable start times for a service on one day, using the same rules as the receptionist.

NameInDescription
service*queryService ID.
date*queryYYYY-MM-DD in the business’s time zone.
calendarqueryOnly this calendar (ID).
Example response
{
    "data": [
        {
            "start": "2026-10-22T10:00:00+01:00",
            "end": "2026-10-22T10:30:00+01:00",
            "calendar": "01JB…"
        }
    ]
}

GET /api/v1/appointments/{appointment}

One appointment appointments:read

NameInDescription
appointment*pathThe appointment ID.
Example response
{
    "data": {
        "id": "01JD3M…",
        "type": "appointment",
        "attributes": {
            "reference": "K7P4M",
            "status": "booked",
            "service": "Men’s haircut",
            "calendar": "Sam",
            "starts_at": "2026-10-22T14:00:00+01:00",
            "ends_at": "2026-10-22T14:30:00+01:00",
            "source": "call",
            "customer": {
                "name": "Sam Taylor",
                "phone": "+447700900123",
                "email": null
            }
        }
    }
}

POST /api/v1/appointments

Book an appointment appointments:write

Checked against opening hours, other bookings and time off, exactly like a phone booking. Send an Idempotency-Key header so a retry never books twice.

NameInDescription
Idempotency-Keyheader8–80 letters, digits or - _ : . (recommended).
service*bodyService ID.
start*bodyA free start time from /availability.
calendarbodyCalendar ID (optional).
customer*body{ "name": required, "phone", "email" }
notesbodyUp to 2,000 characters.
Example response
{
    "data": {
        "id": "01JD3M…",
        "type": "appointment",
        "attributes": {
            "reference": "K7P4M",
            "status": "booked",
            "service": "Men’s haircut",
            "calendar": "Sam",
            "starts_at": "2026-10-22T14:00:00+01:00",
            "ends_at": "2026-10-22T14:30:00+01:00",
            "source": "call",
            "customer": {
                "name": "Sam Taylor",
                "phone": "+447700900123",
                "email": null
            }
        }
    }
}

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

Cancel an appointment appointments:write

The customer is told, as with any cancellation.

NameInDescription
appointment*pathThe appointment ID.
reasonbodyUp to 200 characters.
Example response
{
    "data": {
        "id": "01JD3M…",
        "type": "appointment",
        "attributes": {
            "reference": "K7P4M",
            "status": "cancelled",
            "service": "Men’s haircut",
            "calendar": "Sam",
            "starts_at": "2026-10-22T14:00:00+01:00",
            "ends_at": "2026-10-22T14:30:00+01:00",
            "source": "call",
            "customer": {
                "name": "Sam Taylor",
                "phone": "+447700900123",
                "email": null
            }
        }
    }
}

Contacts and leads

GET /api/v1/contacts

List contacts crm:read

Newest first. Filter by an exact phone number to look someone up.

NameInDescription
phonequeryE.164, e.g. +447700900123.
per_pagequeryResults per page, 1–100 (default 25).
cursorqueryFrom links.next of the previous page.
Example response
{
    "data": [
        {
            "id": "01JC…",
            "type": "contact",
            "attributes": {
                "name": "Sam Taylor",
                "phone": "+447700900123",
                "email": "[email protected]",
                "last_contacted_at": "2026-10-21T09:42:00Z"
            }
        }
    ],
    "links": {
        "next": null,
        "prev": null
    }
}

GET /api/v1/leads

List leads crm:read

Enquiries the receptionist qualified, newest first.

NameInDescription
statusqueryOnly this status.One of: new, qualified, unqualified, contacted, won, lost
per_pagequeryResults per page, 1–100 (default 25).
cursorqueryFrom links.next of the previous page.
Example response
{
    "data": [
        {
            "id": "01JC…",
            "type": "lead",
            "attributes": {
                "status": "qualified",
                "score": 80,
                "interest": "Kitchen refit quote",
                "contact": {
                    "name": "Sam Taylor",
                    "phone": "+447700900123"
                }
            }
        }
    ],
    "links": {
        "next": null,
        "prev": null
    }
}

Webhooks

Add an endpoint in the dashboard under Integrations and choose events. Each event is a JSON POST, signed with your endpoint’s secret. Reply with any 2xx within 10 seconds; failures are retried with back-off for about eight hours, and you can resend any delivery from the dashboard.

POST /your/endpoint
Content-Type: application/json
Oneanswer-Event: appointment.booked
Oneanswer-Event-Id: 01JE…            (same on every retry: use it to ignore duplicates)
Oneanswer-Signature: t=1760000000,v1=5257a8…

{
    "id": "01JE…",
    "type": "appointment.booked",
    "created_at": "2026-10-21T09:43:41+01:00",
    "data": {
        "…": "…"
    }
}

Check the signature

v1 is the HMAC-SHA256 of {t}.{raw body} with your secret, in hex. Use the raw body (before parsing), compare in constant time, and refuse anything more than five minutes old.

PHP

$body = file_get_contents('php://input');
$sig = [];
foreach (explode(',', $_SERVER['HTTP_ONEANSWER_SIGNATURE'] ?? '') as $part) {
    [$k, $v] = array_pad(explode('=', $part, 2), 2, '');
    $sig[$k] = $v;
}
$expected = hash_hmac('sha256', ($sig['t'] ?? '').'.'.$body, getenv('ONEANSWER_WEBHOOK_SECRET'));
if (! hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
    http_response_code(400);
    exit;
}
$event = json_decode($body, true);

Node.js (Express)

const crypto = require('crypto');
app.post('/oneanswer', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = Object.fromEntries((req.get('Oneanswer-Signature') || '').split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', process.env.ONEANSWER_WEBHOOK_SECRET)
    .update(`${sig.t}.${req.body}`).digest('hex');
  const given = Buffer.from(sig.v1 || '');
  const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
  if (!fresh || given.length !== expected.length || !crypto.timingSafeEqual(given, Buffer.from(expected))) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  res.sendStatus(200);
});

Python

import hashlib, hmac, os, time

def verify(raw_body: bytes, header: str) -> bool:
    sig = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    expected = hmac.new(os.environ["ONEANSWER_WEBHOOK_SECRET"].encode(),
                        f"{sig.get('t', '')}.".encode() + raw_body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(sig.get("t", 0))) < 300
    return fresh and hmac.compare_digest(expected, sig.get("v1", ""))

Events

appointment.booked · Appointment booked
{
    "data": {
        "reference": "K7P4M",
        "status": "booked",
        "service": "Men’s haircut",
        "calendar": "Sam",
        "starts_at": "2026-10-22T14:00:00+01:00",
        "ends_at": "2026-10-22T14:30:00+01:00",
        "source": "call",
        "customer": {
            "name": "Sam Taylor",
            "phone": "+447700900123",
            "email": null
        }
    }
}
appointment.rescheduled · Appointment moved
{
    "data": {
        "reference": "K7P4M",
        "status": "booked",
        "service": "Men’s haircut",
        "calendar": "Sam",
        "starts_at": "2026-10-22T14:00:00+01:00",
        "ends_at": "2026-10-22T14:30:00+01:00",
        "source": "call",
        "customer": {
            "name": "Sam Taylor",
            "phone": "+447700900123",
            "email": null
        }
    }
}
appointment.cancelled · Appointment cancelled
{
    "data": {
        "reference": "K7P4M",
        "status": "booked",
        "service": "Men’s haircut",
        "calendar": "Sam",
        "starts_at": "2026-10-22T14:00:00+01:00",
        "ends_at": "2026-10-22T14:30:00+01:00",
        "source": "call",
        "customer": {
            "name": "Sam Taylor",
            "phone": "+447700900123",
            "email": null
        }
    }
}
appointment.completed · Appointment completed
{
    "data": {
        "reference": "K7P4M",
        "status": "booked",
        "service": "Men’s haircut",
        "calendar": "Sam",
        "starts_at": "2026-10-22T14:00:00+01:00",
        "ends_at": "2026-10-22T14:30:00+01:00",
        "source": "call",
        "customer": {
            "name": "Sam Taylor",
            "phone": "+447700900123",
            "email": null
        }
    }
}
lead.created · New lead
{
    "data": {
        "id": "01JC…",
        "status": "qualified",
        "score": 80,
        "interest": "Kitchen refit quote",
        "contact": {
            "name": "Sam Taylor",
            "phone": "+447700900123"
        }
    }
}
lead.updated · Lead status changed
{
    "data": {
        "id": "01JC…"
    }
}
call.completed · Call finished (with summary)
{
    "data": {
        "caller": "+447700900123",
        "called": "+441614960000",
        "outcome": "booked",
        "status": "completed",
        "started_at": "2026-10-21T09:41:12Z",
        "ended_at": "2026-10-21T09:43:40Z",
        "duration_seconds": 148,
        "transferred_to": null,
        "summary": "Sam booked a men’s cut for Thursday at 2pm."
    }
}
message.taken · Message taken for the team
{
    "data": {
        "id": "01JC…",
        "caller_name": "Sam Taylor",
        "caller_phone": "+447700900123",
        "message": "Please call me back about the quote.",
        "reason": "callback",
        "call_id": "01JD…"
    }
}
chat.completed · Website chat finished
{
    "data": {
        "id": "01JC…",
        "status": "closed",
        "outcome": "answered",
        "visitor_name": "Jo",
        "visitor_email": "[email protected]",
        "visitor_phone": null,
        "page_url": "https://example.com/prices",
        "messages": 4,
        "summary": "Jo asked about parking.",
        "started_at": "2026-10-21T09:40:00+01:00"
    }
}
outbound.completed · Outbound call finished
{
    "data": {
        "id": "01JC…",
        "purpose": "reminder",
        "to": "+447700900123",
        "status": "completed",
        "result": "resolved",
        "attempts": 1,
        "call_id": "01JD…",
        "contact": {
            "id": "01JC…",
            "name": "Sam Taylor",
            "phone": "+447700900123",
            "email": null
        }
    }
}
keyword.spotted · Keyword alert matched
{
    "data": {
        "id": "01JC…",
        "alert": "Complaints",
        "phrase": "not happy",
        "channel": "calls",
        "snippet": "I’m really not happy with my crown",
        "call_id": "01JD…",
        "conversation_id": null
    }
}

Try it

Runs a read-only request from your browser with your token. The token stays in this page and is only sent to https://beta.ittypingtest.io.

Versions

This is version 1 (reference 1.4.0). We add endpoints, fields and events without notice, so ignore fields you don’t know. Anything that would break an integration goes into /api/v2, with v1 kept running for at least 12 months.