⚠️ This API is in beta. Endpoint shapes, event payloads, and behaviour may change before general availability. Breaking changes will be communicated to registered integrators in advance.
BLS Portal Client API - Overview#
The BLS Portal Client API lets agency integrators programmatically manage Basic Life Support training bookings, discover available timeslots, and receive real-time webhook notifications for booking lifecycle events.Base URL: https://bls.hbcompliance.co.uk/api/v1
How BLS Bookings Work#
BLS training is delivered remotely — candidates receive a training kit (mannequin, AED trainer, etc.), complete the course, and return the equipment. This affects two important API behaviours:Timeslots are dynamic. There are no fixed training events — instead, available date/time slots are generated based on trainer capacity. Query them via GET /timeslots, hold one via POST /timeslots/hold, then confirm with POST /bookings.
Certificates require equipment return. GET /bookings/{id}/certificate is only available once training_status = completed and equipment_status = in (equipment received back by TDTA). The booking.certificate_available webhook fires at this point.
Authentication#
All protected endpoints require a Bearer token in the Authorization header:Authorization: Bearer <access_token>
Obtain a token pair via POST /auth/token using your hb_client_id and an active API key. Access tokens expire after 1 hour; use POST /auth/refresh (valid 7 days) to rotate without re-entering credentials.
Endpoints#
Authentication#
| Method | Path | Auth | Description |
|---|
POST | /auth/token | — | Exchange hb_client_id + api_key for a token pair |
POST | /auth/refresh | — | Rotate access token using a refresh token |
GET | /auth/token-info | ✓ | Inspect the current token |
POST | /auth/revoke | ✓ | Revoke the current token |
Client#
| Method | Path | Auth | Description |
|---|
GET | /booking-url | ✓ | Your account's unique public booking URL |
GET | /credits | ✓ | Per-course credit balances and GBP money balance |
GET | /transactions | ✓ | Paginated wallet transaction history |
Courses#
| Method | Path | Auth | Description |
|---|
GET | /courses | ✓ | Active BLS courses — use IDs for timeslot and booking queries |
Timeslots#
| Method | Path | Auth | Description |
|---|
GET | /timeslots | ✓ | Available date/time slots for a course |
POST | /timeslots/hold | ✓ | Reserve a timeslot for 30 minutes |
DELETE | /timeslots/hold/{holdHash} | ✓ | Release a timeslot hold early |
Bookings#
| Method | Path | Auth | Description |
|---|
GET | /bookings | ✓ | List bookings (filterable, paginated) |
POST | /bookings | ✓ | Create a booking using a held timeslot |
GET | /bookings/{id} | ✓ | Single booking detail |
GET | /bookings/{id}/certificate | ✓ | Download completion certificate (PDF) |
Webhook Endpoints#
| Method | Path | Auth | Description |
|---|
GET | /webhooks | ✓ | List registered webhook endpoints |
POST | /webhooks | ✓ | Register a new endpoint (secret shown once) |
GET | /webhooks/{id} | ✓ | Get an endpoint |
PUT | /webhooks/{id} | ✓ | Update name, URL, events, timeout, active state |
DELETE | /webhooks/{id} | ✓ | Delete an endpoint |
POST | /webhooks/{id}/rotate-secret | ✓ | Generate a new signing secret |
POST | /webhooks/{id}/test | ✓ | Send a test delivery |
Timeslot & Booking Flow#
The standard booking flow is a three-step process:1. GET /timeslots?course_id=1&months_ahead=2 → pick a date/time
2. POST /timeslots/hold → hold_hash (valid 30 min)
3. POST /bookings → confirmed booking
Step 1 — Query available slotsGET /timeslots?course_id=1&months_ahead=2&date=2026-07-15
Returns a list of { date, time, course } objects. Optionally filter to a specific date.POST /timeslots/hold
{
"course_id": 1,
"date": "2026-07-15",
"time": "10:00"
}
Response includes a hold_hash and expires_at (30 minutes). Only one active hold is permitted per account at a time — a 409 is returned if one already exists.Step 3 — Create the bookingPOST /bookings
{
"hold_hash": "Xy7kRtQmNpLsWvBzJcFdEaHgUiOeYtAn",
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone": "+441234567890",
"address": "42 Acme Street",
"city": "Manchester",
"postcode": "M1 1AA",
"job_role": "Registered Nurse",
"pay_option": "credits",
"agency_contact_name": "John Doe",
"agency_contact_email": "john@acme.co.uk",
"agency_email": "invoices@acme.co.uk"
}
The hold is released and a booking is created in one step.
Payment Options#
pay_option | Behaviour |
|---|
credits | Deducts 1 booking credit for the course. Confirmed immediately. |
balance | Deducts the course cost from your GBP money balance. Confirmed immediately. |
invoice | Raised as an invoice to your account. Confirmed immediately. Requires invoice_payments enabled and sufficient credit limit. |
candidate_pays | Candidate receives a Stripe payment link by email. Booking is pending until paid. |
Note: BLS uses invoice (not inv) for invoice-based payments.
Credits & Balances#
{
"booking_credits": [
{ "course_id": 1, "course_name": "BLS Practical", "credits": 12 }
],
"booking_money_balance": 480.00,
"currency": "GBP"
}
Booking credits — 1 credit = 1 booking for the specified course.
Booking money balance — a pre-paid GBP balance deducted at the course rate when pay_option: balance.
Certificates#
GET /bookings/{id}/certificate
Returns a PDF binary stream. Available only when both conditions are met:1.
training_status = completed — training was marked complete by TDTA staff.
2.
equipment_status = in — the training kit has been returned to TDTA.
Returns 422 otherwise. Subscribe to booking.certificate_available to know exactly when the certificate is ready.
All list endpoints return a consistent envelope:{
"data": [ ... ],
"meta": {
"current_page": 1,
"per_page": 20,
"last_page": 4,
"total": 71
},
"links": {
"next": "https://bls.hbcompliance.co.uk/api/v1/bookings?page=2",
"prev": null
}
}
Rate Limiting#
60 requests per minute per IP. Exceeding this returns 429 Too Many Requests.
Errors#
| Status | Meaning |
|---|
401 | Missing or invalid bearer token |
404 | Resource not found or not owned by your account |
409 | Conflict (e.g. active timeslot hold already exists) |
422 | Validation failed or business rule violation |
429 | Rate limit exceeded |
500 | Server error — contact support |
Webhook Events#
The following outbound events are available for subscription:| Event | Fires when |
|---|
booking.created | A new booking is confirmed (immediately for credits/balance/invoice; or after Stripe payment for candidate_pays) |
booking.confirmed | A candidate_pays booking transitions from pending to booked after Stripe payment |
booking.rescheduled | A booking is moved to a different timeslot |
booking.cancelled | A booking is cancelled |
booking.attendance_marked | Training attendance is recorded by TDTA staff |
booking.equipment_late | Equipment has not been returned by the expected date |
booking.certificate_available | Training completed and equipment returned — certificate is ready |
webhook.test | Manual test triggered via POST /webhooks/{id}/test |
All events share the same envelope:{
"event_id": "018f4a2b-1234-7000-8abc-def012345678",
"event_type": "booking.created",
"occurred_at": "2026-06-05T10:00:00+00:00",
"data": { ... }
}
Signature Verification#
Every webhook request includes these headers:| Header | Description |
|---|
X-TDTA-Event | Event type, e.g. booking.created |
X-TDTA-Event-Id | UUID of the event — use for idempotency |
X-TDTA-Delivery-Id | Numeric delivery attempt ID |
X-TDTA-Timestamp | Unix timestamp of dispatch (seconds) |
X-TDTA-Signature | sha256=<hmac> — see below |
Computing the expected signature:signature = HMAC-SHA256(signing_secret, "{timestamp}.{raw_json_body}")
expected = "sha256=" + signature
Compare using a constant-time comparison. Reject requests where |now() - X-TDTA-Timestamp| > 300 (5-minute replay window).Retries#
Deliveries are retried up to 5 times with a 15-minute backoff on any non-2xx response or timeout. Return any 2xx status to acknowledge receipt.
For urgent issues affecting live bookings, email with the subject line [BLS API URGENT] and include your hb_client_id and any relevant booking_id.