Integrations

Cradle integration API guide

What a software vendor's API needs to provide for Cradle to integrate with it: suggested endpoints and an example of each data shape.

Audience: engineering teams at software vendors serving accounting firms (practice management, workflow, advisory, receivables and similar tools) building an API for Cradle to integrate with.

Purpose: what the partner API needs to provide for Cradle to integrate with it. Each section gives a suggested endpoint and an example of the data shape. Different names are fine as long as the same data is there.

1. What Cradle does

Cradle is a cloud phone system for accounting firms in Australia, New Zealand and the UK. A firm connects the partner platform to Cradle once, at the organisation level. Cradle then does the following:

  1. Identifies callers. Before a staff member answers, Cradle shows the client or contact name from the partner platform and their client group (if applicable).
  2. Logs activity back. When a call, SMS, WhatsApp message or voicemail completes, Cradle writes a note to the matched client, contact or group: who called, direction, duration, outcome, a link to the recording, an AI summary, and optionally the transcript. Cradle can also file attachments (a photo a client sent, for example).
  3. Books time. Each call can become a time entry against the client, booked to the staff member who handled the call.
  4. Creates follow-up tasks. A missed call, a transfer that didn't connect, or an action item the AI picked out of the transcript can become a task assigned to a staff member.
  5. Updates records mid-call. A staff member on a call with a client whose mobile number isn't on file can add it without leaving the phone app.
  6. Click-to-call. Phone numbers shown in the partner UI can start a Cradle call.

2. Summary of what we are looking for

Capability What to provide Section
Authentication OAuth 2.0 with a refresh token, or a per-firm static key pair sent as headers 3
Clients and organisations A paginated list with an id, name, every phone number, email, owner or manager email, group id, web URL and last-modified timestamp 5.1
Contacts (people) The same, paginated, plus the id of the client they belong to 5.2
Client groups (optional) A paginated list with group id, name, members, and an entity the group's activity files against 5.3
Staff A paginated list with an id and email 5.4
Call and message notes Create a note with a subject, HTML body, and one or more records to file against, with an option to update it at a later stage 6.1, 6.2
Files (optional) Upload a file and link it to a note or record 6.3
Tasks (optional) Create a task with a title, assignee, due date and the records it relates to 6.4
Record updates Add a phone number to an existing client or contact; create a contact 6.5
Webhooks Client, contact and group changes pushed to Cradle 7
Time entries (optional) Book minutes against a client and staff member with a plain-text note 6.6

3. Authentication

Provide one of the two models below. In both, revoking access must produce a 401 or 403 on the next request; Cradle treats that as "the firm disconnected" and stops syncing.

3.1 OAuth 2.0

Authorisation-code flow with a refresh token. The scope should cover reads and writes from the start, so firms don't have to re-consent when write features are added. The flow must identify the firm's tenant so Cradle can store the connection against it.

GET  /oauth/authorize
POST /oauth/token
POST /oauth/revoke
  1. Cradle sends the firm's admin to /oauth/authorize with client_id, redirect_uri, scope and state.
  2. The admin approves and is redirected back to Cradle with a code.
  3. Cradle exchanges the code at /oauth/token and receives:
{ "access_token": "…", "refresh_token": "…", "expires_in": 1800, "token_type": "Bearer", "tenant_id": "…" }
  1. Cradle sends Authorization: Bearer <access_token> on every request, refreshes with the refresh token before expiry, and stores the new tokens.
  2. Disconnect calls /oauth/revoke.

The tenant id can come back on the token response, as above, or from a separate call that lists the tenants the token can access.

3.2 Static key pair (alternative)

The firm's admin generates credentials in the partner's settings and pastes them into the Cradle admin portal. Two values, sent as request headers, scoped to one firm.

GET /clients
X-Api-Key-Id: <key id>
X-Api-Key-Secret: <secret>

Revoking the key in the partner's settings makes the next request return 401.

4. Conventions

4.1 Format

JSON over HTTPS. Ids can be numbers, UUIDs or strings up to 40 characters; Cradle stores and returns them unchanged.

4.2 Environments

Provide a test base URL and a production base URL, with a test account Cradle can read from and write to.

4.3 Phone numbers

Return every phone number on the record, each with a type, on the list response. Keep the leading + and country code as entered. Cradle parses each number against the firm's country and matches on all of them.

"phone_numbers": [
  { "number": "+64 9 555 0100", "type": "phone" },
  { "number": "021 555 0101", "type": "mobile" }
]

4.4 Pagination and incremental sync

Paginated lists (pages of 100 or more) that can be filtered or ordered by last-modified, so a daily sync reads only what changed.

GET /clients?modified_since=2026-09-10T00:00:00Z&page=2&per_page=100
Authorization: Bearer <access_token>
{ "results": [ … ], "page": 2, "per_page": 100, "total": 1840 }

A cursor (next link) in place of page numbers is equally fine.

4.5 Deletes

Mark deleted or archived records with a flag so they can be removed from caller ID. Without one, Cradle has to pull everything and diff.

GET /clients?modified_since=2026-09-10T00:00:00Z
Authorization: Bearer <access_token>
{ "results": [ { "id": "cli_3021", "name": "Acme Roofing Ltd", "deleted": true, "modified_at": "2026-09-10T08:00:00Z" } ], "page": 1, "per_page": 100, "total": 1 }

4.6 Timezone

Dates in notes and on time entries are in the practice's local time. The firm's admin sets the timezone in Cradle.

4.7 Errors

Standard HTTP status codes, with a short error message in the body. Cradle retries a request that fails with a 5xx error.

HTTP/1.1 422 Unprocessable Entity
{ "error": "phone_numbers[0].number is required" }

5. Reading practice data

5.1 Clients and organisations

GET /clients?page=1&per_page=100
Authorization: Bearer <access_token>

Provide a list of the firm's clients (companies, trusts, individuals) with, per record: an id, a display name, every phone number, an email, the manager or owner's email, a group id, a web URL that opens the record, and a last-modified timestamp.

{
  "results": [
    {
      "id": "cli_3021",
      "name": "Acme Roofing Ltd",
      "type": "company",
      "email": "accounts@acmeroofing.co.nz",
      "phone_numbers": [
        { "number": "+64 9 555 0100", "type": "phone" },
        { "number": "021 555 0101", "type": "mobile" }
      ],
      "manager_email": "priya@firm.co.nz",
      "group_id": "grp_77",
      "web_url": "https://app.example.com/clients/cli_3021",
      "modified_at": "2026-08-30T03:12:45Z",
      "deleted": false
    }
  ],
  "page": 1,
  "per_page": 100,
  "total": 1840
}

type tells Cradle whether to show the record as a person or an organisation (company, trust, individual, and so on). group_id is null or absent when the client isn't in a group.

5.2 Contacts (people)

GET /contacts?page=1&per_page=100
Authorization: Bearer <access_token>

Provide the people linked to each client with, per record: an id, a display name, every phone number, an email, the id of the client they belong to, and a last-modified timestamp. Cradle syncs the list once a day.

{
  "results": [
    {
      "id": "con_9001",
      "name": "Sam Rangi",
      "email": "sam@acmeroofing.co.nz",
      "phone_numbers": [{ "number": "+64 21 555 0199", "type": "mobile" }],
      "client_id": "cli_3021",
      "web_url": "https://app.example.com/contacts/con_9001",
      "modified_at": "2026-08-30T03:12:45Z",
      "deleted": false
    }
  ],
  "page": 1,
  "per_page": 100,
  "total": 2915
}

5.3 Client groups (optional)

GET /groups?page=1&per_page=100
Authorization: Bearer <access_token>

If the platform has client groups, provide: a group id, a name, the members (or the group id on each member record), and an entity the group's activity can be filed against, for platforms where a note can't be filed on the group itself.

{
  "results": [
    {
      "id": "grp_77",
      "name": "Acme Roofing Group",
      "primary_client_id": "cli_3021",
      "member_client_ids": ["cli_3021", "cli_3022", "cli_3023"],
      "modified_at": "2026-08-30T03:12:45Z"
    }
  ],
  "page": 1,
  "per_page": 100,
  "total": 212
}

5.4 Staff

GET /staff?page=1&per_page=100
Authorization: Bearer <access_token>

Provide a list of staff with an id and an email. Cradle matches its own users to partner staff by email, case-insensitively, to attribute notes and time entries. The note will be rejected if the staff member isn't matched.

{
  "results": [
    { "id": "usr_11", "name": "Priya Patel", "email": "priya@firm.co.nz", "active": true }
  ],
  "page": 1,
  "per_page": 100,
  "total": 14
}

6. Writing back

6.1 Call notes

Provide a way to create a note with: a subject, an HTML body (or plain text, stated), one or more records to file it against (person, client, group), and an identifier of Cradle's own so a retried post doesn't create a duplicate. Provide an option to update the note at a later stage.

Cradle posts one note per completed call. The body contains the direction, the parties, the staff member, duration, a "Listen to recording" link, the AI summary, and optionally the transcript. The subject is the AI call title when the firm has it on, otherwise a line such as "Missed call from Sam Rangi". The update matters because the call ends before the recording is transcribed and summarised: with an update, Cradle logs the call immediately and adds the summary when it's ready.

Create, at call end:

POST /notes
Authorization: Bearer <access_token>
Content-Type: application/json
{
  "external_id": "<Cradle conversation id>",
  "subject": "Inbound call from Sam Rangi",
  "body": "<p>Inbound call from Sam Rangi, answered by Priya Patel, 7 min 32 s.</p><p><a href=\"https://…\">Listen to recording</a></p>",
  "author_email": "priya@firm.co.nz",
  "occurred_at": "2026-09-11T02:15:30Z",
  "filed_against": [
    { "type": "contact", "id": "con_9001" },
    { "type": "client", "id": "cli_3021" },
    { "type": "group", "id": "grp_77" }
  ]
}

Response: { "id": "note_4402" }.

Update, once the summary is ready:

PUT /notes/note_4402
Authorization: Bearer <access_token>
Content-Type: application/json
{
  "subject": "GST return due date and August invoice",
  "body": "<p>Inbound call from Sam Rangi, answered by Priya Patel, 7 min 32 s.</p><p><a href=\"https://…\">Listen to recording</a></p><p><strong>Summary</strong><br>Sam asked about the GST return due date and confirmed the August invoice was paid on the 5th.</p>"
}

Response: { "id": "note_4402" }. Posting again with the same external_id instead of a PUT is also fine. A note that can only be filed against one record still works; Cradle files it on the closest match.

6.2 Messages (SMS and WhatsApp)

Same note mechanism. Cradle posts either one note per message or one note per thread updated as messages arrive, whichever reads better on the partner's timeline. Inbound messages have no staff member, so the author is either omitted or a user the firm nominates.

POST /notes
Authorization: Bearer <access_token>
Content-Type: application/json
{
  "external_id": "<Cradle message id>",
  "subject": "SMS from Sam Rangi",
  "body": "<p><strong>Sam Rangi</strong> 2:15 pm<br>Hi Priya, invoice attached.</p>",
  "occurred_at": "2026-09-11T02:15:30Z",
  "attachments": ["file_5a1"],
  "filed_against": [{ "type": "contact", "id": "con_9001" }]
}

Response: { "id": "note_4403" }.

6.3 Files (optional)

Provide a file upload that returns a file id and can be linked to the note or record it belongs to. A single multipart request is best.

POST /files
Authorization: Bearer <access_token>
Content-Type: multipart/form-data; boundary=----cradle

------cradle
Content-Disposition: form-data; name="file"; filename="invoice.jpg"
Content-Type: image/jpeg

<binary>
------cradle--

Response: { "id": "file_5a1" }, which Cradle then references from the note ("attachments": ["file_5a1"]) or files against the client.

6.4 Tasks (optional)

Provide a way to create a task with a title, a description, an assignee, a due date, the records it relates to, and an identifier of Cradle's own so a retried post doesn't create a duplicate.

POST /tasks
Authorization: Bearer <access_token>
Content-Type: application/json
{
  "external_id": "<Cradle task id>",
  "title": "Call back Sam Rangi (missed call)",
  "description": "Missed inbound call at 2:15 pm. Voicemail: …",
  "assignee_id": "usr_11",
  "due_at": "2026-09-12T04:00:00Z",
  "filed_against": [
    { "type": "contact", "id": "con_9001" },
    { "type": "client", "id": "cli_3021" }
  ]
}

Response: { "id": "task_771" }.

6.5 Record updates

Provide a way to add a phone number to an existing client or contact, and a way to create a contact against a client.

Add a phone number:

PATCH /contacts/con_9001
Authorization: Bearer <access_token>
Content-Type: application/json
{ "phone_numbers": [{ "number": "+64 21 555 0300", "type": "mobile" }] }

Cradle sends the number in international format with the country code. The number is added to the ones already on the record. Response: the updated contact, in the same shape as 5.2. PATCH /clients/{id} works the same way for a client.

Create a contact:

POST /contacts
Authorization: Bearer <access_token>
Content-Type: application/json
{
  "name": "New Caller",
  "phone_numbers": [{ "number": "+64 21 555 0400", "type": "mobile" }],
  "client_id": "cli_3021"
}

Response: the created contact, in the same shape as 5.2. If a contact with that phone number already exists, return 409 with the existing id rather than creating a second one.

6.6 Time entries (optional)

For platforms with timesheets, provide a way to book minutes against a client and a staff member, with a date in the practice's timezone and a plain-text note, with as few prerequisites as the data model allows. If the entry has to be coded to a job or task, provide the lists needed to configure that, and a default Cradle can fall back to.

POST /time_entries
Authorization: Bearer <access_token>
Content-Type: application/json
{
  "external_id": "<Cradle conversation id>",
  "client_id": "cli_3021",
  "staff_id": "usr_11",
  "date": "2026-09-11",
  "minutes": 12,
  "note": "Inbound call from Sam Rangi, 7m32s. Sam asked about…",
  "job_id": "job_88",
  "task_id": "task_12"
}

Response: { "id": "time_9f1" }. minutes is the call duration plus the firm's padding, rounded up to the firm's increment. If time can't be booked against the chosen job (closed, for example), return a 4xx with a reason rather than a 200.

If time has to be coded to a job or task, Cradle needs the two lists below to pick them.

6.6.1 List jobs

GET /clients/cli_3021/jobs?page=1&per_page=100
Authorization: Bearer <access_token>
{
  "results": [
    { "id": "job_88", "name": "Annual accounts 2026", "type": "Annual accounts", "status": "open", "modified_at": "2026-08-30T03:12:45Z" }
  ],
  "page": 1,
  "per_page": 100,
  "total": 3
}

Cradle books time to an open job on the client. status should say whether the job accepts time (open) or not (closed).

6.6.2 List tasks

GET /tasks?page=1&per_page=100
Authorization: Bearer <access_token>
{
  "results": [
    { "id": "task_12", "name": "Phone calls" }
  ],
  "page": 1,
  "per_page": 100,
  "total": 25
}

The task types time can be booked under. The firm's admin picks one in Cradle, and Cradle sends its id as task_id on each time entry.

7. Webhooks

Provide events when a client, contact or group is created, updated or deleted, carrying the record in the same shape as the list endpoint. This keeps caller ID current between daily syncs.

A shared secret verifies the sender. Cradle supplies it when it subscribes; the partner signs each delivery with it, by hashing the request body with the secret (HMAC-SHA256) and sending the result in a header, and Cradle recomputes the hash and rejects anything that doesn't match. That way a delivery can't be forged by someone who only knows the callback URL.

Cradle subscribes:

POST /webhooks
Authorization: Bearer <access_token>
Content-Type: application/json
{
  "url": "https://…cradle.io/…",
  "events": ["client.created", "client.updated", "client.deleted", "contact.created", "contact.updated", "contact.deleted", "group.created", "group.updated", "group.deleted"],
  "secret": "<shared secret>"
}

Response: { "id": "wh_12" }, the subscription's id. Cradle keeps it to remove the subscription when the firm disconnects.

Cradle unsubscribes, on disconnect:

DELETE /webhooks/wh_12
Authorization: Bearer <access_token>

Response: 204 No Content.

The partner delivers:

POST https://…cradle.io/…
Content-Type: application/json
X-Signature: sha256=<HMAC-SHA256 of the body, keyed with the shared secret>
{
  "id": "evt_01J7…",
  "type": "client.updated",
  "occurred_at": "2026-09-11T02:30:00Z",
  "tenant_id": "…",
  "data": { "id": "cli_3021", "name": "Acme Roofing Ltd", … }
}

Cradle answers 2xx promptly and processes the event afterwards. Failed deliveries should be retried, and a subscription should not be dropped after a run of failures without telling Cradle.

8. Deep links and click-to-call

  • Provide a web URL per record that opens it in the partner's web app. Cradle shows it wherever the record appears.
  • In the partner UI, render phone numbers as tel: or callto: links. The Cradle desktop app registers as the handler for both schemes on macOS and Windows and starts the call.