Support API

Plug your system into our support desk

Your users raise issues inside your own product. With the Boolet Ltd Support API those issues become tickets for our developers, and replies, status changes and time come back to your system automatically.

It is a JSON REST API over HTTPS. Base URL:

https://portal.boolet.co.uk/api/v1

A typical integration: create a ticket when your user reports a problem, keep our ticket number (or use your own reference), and receive webhooks when our team replies or the status changes. Tickets also appear in your Boolet Ltd client portal, and time is taken from your support hours as usual.

Getting a key

The Support API is available by agreement. Talk to us about your system and the support you need. Once access is switched on for your account, we create an API key for you and send it securely. Keys can be limited to your servers' IP addresses and given an expiry date. Keep keys on your server; never put them in a browser or mobile app.

Authentication

Send the key as a bearer token on every request:

curl https://portal.boolet.co.uk/api/v1/me \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"

Keys have abilities: tickets:read to read tickets, messages and time, and tickets:write to create tickets, reply and attach files.

Errors

Errors use standard HTTP status codes and always have the same body:

{
    "error": {
        "code": "validation_failed",
        "message": "The title field is required.",
        "fields": {
            "title": [
                "The title field is required."
            ]
        }
    }
}
StatuscodeMeaning
401unauthenticatedMissing, wrong, revoked or expired key.
403forbiddenThe key may not do this, the request came from an address the key does not allow, or API access is off.
404not_foundNo such ticket on your account.
422validation_failedThe request is not valid; fields says why.
429rate_limitedToo many requests; wait for the seconds in Retry-After.
5xxserver_errorOur problem. Retry with backoff.

Rate limits

60 requests a minute per key. Over the limit you get a 429 with a Retry-After header. Webhooks mean you rarely need to poll.

Safe retries

Networks fail. When creating a ticket, send an Idempotency-Key header with a value unique to that ticket, or your own external_reference. If the same request arrives twice, the second gets the ticket already created (status 200) and no duplicate is made.

Pagination

Lists are cursor-paginated: follow links.next until it is null. Use updated_since to fetch only what changed since your last sync.

Endpoints

A ticket is identified by our number (e.g. TICKET142) or by ext: and your reference (e.g. ext:EXT-88412).

Values: type <code>bug</code>, <code>feature</code>, <code>task</code>, <code>other</code>; priority low, medium, high; platform web, mobile, desktop, other; status open, in_progress, client_responded, waiting_for_information, answered, closed.

Check your key

GET /me

Who the key belongs to, what it may do, and (for Support API keys) your plan and support hours balance.

Request

curl https://portal.boolet.co.uk/api/v1/me \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"

Response 200

{
    "data": {
        "owner": "MediTech Ltd",
        "key_name": "Production",
        "abilities": [
            "tickets:read",
            "tickets:write"
        ],
        "expires_at": null,
        "support": {
            "api_enabled": true,
            "plan": "Growth",
            "hours_balance": 4.5
        }
    }
}

Create a ticket

POST /tickets

Raise a ticket for our developers. Send an Idempotency-Key header, or your own external_reference, so a retry returns the ticket already created (200) instead of creating another. Needs tickets:write.

Parameters

NameInTypeDescription
Idempotency-Key header string Any unique value per ticket, up to 100 characters.

Request

curl -X POST https://portal.boolet.co.uk/api/v1/tickets \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a90-…" \
  -d '{
    "title": "Appointments not syncing to calendar",
    "description": "Since this morning, online bookings do not appear in the clinic calendar.",
    "type": "bug",
    "priority": "high",
    "platform": "web",
    "page_url": "https://app.example.com/bookings",
    "external_reference": "EXT-88412",
    "external_account": {
        "id": "site-204",
        "name": "Riverside Medical Centre"
    },
    "reporter": {
        "name": "Jane Doe",
        "email": "jane@riverside.example"
    },
    "metadata": {
        "app_version": "4.12.1",
        "environment": "production"
    }
}'

Response 201

{
    "data": {
        "id": "TICKET142",
        "external_reference": "EXT-88412",
        "title": "Appointments not syncing to calendar",
        "description": "Since this morning, online bookings do not appear in the clinic calendar.",
        "type": "bug",
        "priority": "high",
        "status": "open",
        "platform": "web",
        "page_url": "https://app.example.com/bookings",
        "source": "api",
        "external_account": {
            "id": "site-204",
            "name": "Riverside Medical Centre"
        },
        "reporter": {
            "name": "Jane Doe",
            "email": "jane@riverside.example"
        },
        "metadata": {
            "app_version": "4.12.1",
            "environment": "production"
        },
        "assignee": null,
        "attachments": [],
        "time": {
            "logged_minutes": 0,
            "billable_minutes": 0
        },
        "created_at": "2026-11-04T09:12:40+00:00",
        "updated_at": "2026-11-04T09:12:40+00:00",
        "closed_at": null
    }
}

List tickets

GET /tickets

Your tickets, newest first, 25 a page (up to 100). Follow links.next, or pass meta.next_cursor as cursor, for the next page. Needs tickets:read.

Parameters

NameInTypeDescription
status query string One or more statuses, comma-separated: open, in_progress, client_responded, waiting_for_information, answered, closed.
external_account_id query string Only tickets for one of your customers.
updated_since query date-time Only tickets changed since this time (ISO 8601). Handy for catching up after downtime.
per_page query integer 1 to 100. Default 25.
cursor query string From meta.next_cursor of the previous page.

Request

curl https://portal.boolet.co.uk/api/v1/tickets \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"

Response 200

{
    "data": [
        {
            "id": "TICKET142",
            "external_reference": "EXT-88412",
            "title": "Appointments not syncing to calendar",
            "description": "Since this morning, online bookings do not appear in the clinic calendar.",
            "type": "bug",
            "priority": "high",
            "status": "open",
            "platform": "web",
            "page_url": "https://app.example.com/bookings",
            "source": "api",
            "external_account": {
                "id": "site-204",
                "name": "Riverside Medical Centre"
            },
            "reporter": {
                "name": "Jane Doe",
                "email": "jane@riverside.example"
            },
            "metadata": {
                "app_version": "4.12.1",
                "environment": "production"
            },
            "assignee": null,
            "attachments": [],
            "time": {
                "logged_minutes": 0,
                "billable_minutes": 0
            },
            "created_at": "2026-11-04T09:12:40+00:00",
            "updated_at": "2026-11-04T09:12:40+00:00",
            "closed_at": null
        }
    ],
    "meta": {
        "per_page": 25,
        "next_cursor": null,
        "prev_cursor": null
    },
    "links": {
        "next": null,
        "prev": null
    }
}

Get a ticket

GET /tickets/{ticket}

One ticket, by our number (TICKET12) or by your reference (ext:EXT-88412). Needs tickets:read.

Parameters

NameInTypeDescription
ticket required path string Our ticket number (TICKET12) or ext: and your reference (ext:EXT-88412).

Request

curl https://portal.boolet.co.uk/api/v1/tickets/TICKET142 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"

Response 200

{
    "data": {
        "id": "TICKET142",
        "external_reference": "EXT-88412",
        "title": "Appointments not syncing to calendar",
        "description": "Since this morning, online bookings do not appear in the clinic calendar.",
        "type": "bug",
        "priority": "high",
        "status": "open",
        "platform": "web",
        "page_url": "https://app.example.com/bookings",
        "source": "api",
        "external_account": {
            "id": "site-204",
            "name": "Riverside Medical Centre"
        },
        "reporter": {
            "name": "Jane Doe",
            "email": "jane@riverside.example"
        },
        "metadata": {
            "app_version": "4.12.1",
            "environment": "production"
        },
        "assignee": null,
        "attachments": [],
        "time": {
            "logged_minutes": 0,
            "billable_minutes": 0
        },
        "created_at": "2026-11-04T09:12:40+00:00",
        "updated_at": "2026-11-04T09:12:40+00:00",
        "closed_at": null
    }
}

Update a ticket

PATCH /tickets/{ticket}

Change the priority, mark the ticket resolved (status closed) or reopen it (status open). Needs tickets:write.

Parameters

NameInTypeDescription
ticket required path string Our ticket number (TICKET12) or ext: and your reference (ext:EXT-88412).

Request

curl -X PATCH https://portal.boolet.co.uk/api/v1/tickets/TICKET142 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "priority": "medium",
    "status": "closed"
}'

Response 200

{
    "data": {
        "id": "TICKET142",
        "external_reference": "EXT-88412",
        "title": "Appointments not syncing to calendar",
        "description": "Since this morning, online bookings do not appear in the clinic calendar.",
        "type": "bug",
        "priority": "medium",
        "status": "closed",
        "platform": "web",
        "page_url": "https://app.example.com/bookings",
        "source": "api",
        "external_account": {
            "id": "site-204",
            "name": "Riverside Medical Centre"
        },
        "reporter": {
            "name": "Jane Doe",
            "email": "jane@riverside.example"
        },
        "metadata": {
            "app_version": "4.12.1",
            "environment": "production"
        },
        "assignee": null,
        "attachments": [],
        "time": {
            "logged_minutes": 0,
            "billable_minutes": 0
        },
        "created_at": "2026-11-04T09:12:40+00:00",
        "updated_at": "2026-11-04T09:12:40+00:00",
        "closed_at": null
    }
}

List messages

GET /tickets/{ticket}/messages

The conversation on a ticket, oldest first. Our internal notes are never included. Needs tickets:read.

Parameters

NameInTypeDescription
ticket required path string Our ticket number (TICKET12) or ext: and your reference (ext:EXT-88412).

Request

curl https://portal.boolet.co.uk/api/v1/tickets/TICKET142/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"

Response 200

{
    "data": [
        {
            "id": 1001,
            "body": "Thanks, we have found the cause and are deploying a fix now.",
            "author": {
                "name": "Sam Patel",
                "type": "team"
            },
            "via": "portal",
            "attachments": [],
            "created_at": "2026-11-04T14:20:03+00:00"
        }
    ]
}

Reply to a ticket

POST /tickets/{ticket}/messages

Add a message from your side, written by someone in your system. The ticket moves to client_responded (a closed ticket reopens) and the assigned developer is told. Needs tickets:write.

Parameters

NameInTypeDescription
ticket required path string Our ticket number (TICKET12) or ext: and your reference (ext:EXT-88412).

Request

curl -X POST https://portal.boolet.co.uk/api/v1/tickets/TICKET142/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "It is still happening for bookings made on mobile.",
    "author": {
        "name": "Jane Doe",
        "email": "jane@riverside.example"
    }
}'

Response 201

{
    "data": {
        "id": 1002,
        "body": "It is still happening for bookings made on mobile.",
        "author": {
            "name": "Jane Doe",
            "type": "client"
        },
        "via": "api",
        "attachments": [],
        "created_at": "2026-11-04T14:20:03+00:00"
    }
}

Attach files

POST /tickets/{ticket}/attachments

Send screenshots or logs as multipart/form-data in files[]. Up to 5 files a request, 10 MB each: png, jpg, jpeg, gif, webp, pdf, txt, log, csv, zip, docx, xlsx, mp4, mov. Needs tickets:write.

Parameters

NameInTypeDescription
ticket required path string Our ticket number (TICKET12) or ext: and your reference (ext:EXT-88412).

Request

curl -X POST https://portal.boolet.co.uk/api/v1/tickets/TICKET142/attachments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -F "files[]=@screenshot.png"

Response 201

{
    "data": {
        "id": "TICKET142",
        "external_reference": "EXT-88412",
        "title": "Appointments not syncing to calendar",
        "description": "Since this morning, online bookings do not appear in the clinic calendar.",
        "type": "bug",
        "priority": "high",
        "status": "open",
        "platform": "web",
        "page_url": "https://app.example.com/bookings",
        "source": "api",
        "external_account": {
            "id": "site-204",
            "name": "Riverside Medical Centre"
        },
        "reporter": {
            "name": "Jane Doe",
            "email": "jane@riverside.example"
        },
        "metadata": {
            "app_version": "4.12.1",
            "environment": "production"
        },
        "assignee": null,
        "attachments": [
            {
                "name": "screenshot.png",
                "size": 182044,
                "mime": "image/png"
            }
        ],
        "time": {
            "logged_minutes": 0,
            "billable_minutes": 0
        },
        "created_at": "2026-11-04T09:12:40+00:00",
        "updated_at": "2026-11-04T09:12:40+00:00",
        "closed_at": null
    }
}

List time logged

GET /tickets/{ticket}/time-entries

Time our developers logged on the ticket, and whether it came off your support hours (billable). Needs tickets:read.

Parameters

NameInTypeDescription
ticket required path string Our ticket number (TICKET12) or ext: and your reference (ext:EXT-88412).

Request

curl https://portal.boolet.co.uk/api/v1/tickets/TICKET142/time-entries \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"

Response 200

{
    "data": [
        {
            "id": 77,
            "worked_on": "2026-11-04",
            "minutes": 45,
            "billable": true,
            "description": "Fixed the calendar sync job",
            "logged_by": "Sam Patel",
            "created_at": "2026-11-04T15:02:11+00:00"
        }
    ],
    "meta": {
        "logged_minutes": 45,
        "billable_minutes": 45
    }
}

Webhooks

Rather than polling, give us an HTTPS address in your system and choose the events it should receive. We send a POST with a JSON body for each event. Answer with any 2xx status within 10 seconds; anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. An address that keeps failing for three days is switched off and we contact you.

EventWhen
ticket.createdA ticket was raised
ticket.status_changedA ticket changed status
ticket.message.createdThe team replied
ticket.assignedA developer was assigned
ticket.closedA ticket was closed
ticket.time_loggedTime was logged
pingA test event sent from our side

ticket.message.created is sent for our team's replies only: your own messages are not echoed back, and our internal notes are never sent.

Example body

{
    "id": "evt_01jbz8m6x4k2q7c9t0v5w3y1nr",
    "type": "ticket.message.created",
    "created_at": "2026-11-04T14:20:03+00:00",
    "data": {
        "ticket": {
            "id": "TICKET142",
            "external_reference": "EXT-88412",
            "title": "Appointments not syncing to calendar",
            "description": "Since this morning, online bookings do not appear in the clinic calendar.",
            "type": "bug",
            "priority": "high",
            "status": "answered",
            "platform": "web",
            "page_url": "https://app.example.com/bookings",
            "source": "api",
            "external_account": {
                "id": "site-204",
                "name": "Riverside Medical Centre"
            },
            "reporter": {
                "name": "Jane Doe",
                "email": "jane@riverside.example"
            },
            "metadata": {
                "app_version": "4.12.1",
                "environment": "production"
            },
            "assignee": null,
            "attachments": [],
            "time": {
                "logged_minutes": 0,
                "billable_minutes": 0
            },
            "created_at": "2026-11-04T09:12:40+00:00",
            "updated_at": "2026-11-04T09:12:40+00:00",
            "closed_at": null
        },
        "message": {
            "id": 1001,
            "body": "Thanks, we have found the cause and are deploying a fix now.",
            "author": {
                "name": "Sam Patel",
                "type": "team"
            },
            "via": "portal",
            "attachments": [],
            "created_at": "2026-11-04T14:20:03+00:00"
        }
    }
}

Headers: Boolet-Event (the event), Boolet-Delivery (unique per event; use it to ignore duplicates) and Boolet-Signature.

Checking signatures

Each webhook address has its own signing secret (whsec_…). The Boolet-Signature header looks like t=1730730003,v1=5257a869…: t is the time it was sent and v1 is the HMAC-SHA256 of t + "." + raw body with your secret. Reject events older than five minutes.

PHP

$header = $_SERVER['HTTP_BOOLET_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');

preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $m);
$expected = hash_hmac('sha256', $m[1].'.'.$body, $secret);

$valid = $m
    && abs(time() - (int) $m[1]) <= 300
    && hash_equals($expected, $m[2]);

Node.js

import crypto from 'node:crypto';

const [, t, v1] = /^t=(\d+),v1=([a-f0-9]{64})$/
  .exec(req.headers['boolet-signature'] ?? '') ?? [];
const expected = crypto.createHmac('sha256', secret)
  .update(`${t}.${rawBody}`).digest('hex');

const valid = !!v1
  && Math.abs(Date.now() / 1000 - t) <= 300
  && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));

Sandbox

Ask us for a test key while you build. It belongs to a separate test account, so your test tickets never mix with real ones, and you can point webhooks at your development server. When you go live, swap in your production key: the base URL stays the same.

Changelog

Version 1 only ever gains new fields, endpoints and events, so existing integrations keep working. Anything that would break an integration would come as /api/v2, with notice.

DateChange
25 Oct 2026Version 1: tickets, messages, attachments, time and webhooks.

Questions? Email info@boolet.co.uk.