Healthier Business - Virtual BLS

llms.txt
📌
⚠️ 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 slots
GET /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.
Step 2 — Hold a slot
JSON
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 booking
JSON
POST /bookings
{
"hold_hash": "Xy7kRtQmNpLsWvBzJcFdEaHgUiOeYtAn",
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"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": "[email protected]",
"agency_email": "[email protected]"
}
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

GET /credits returns:
JSON
{
"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.
  1. 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.



Pagination

All list endpoints return a consistent envelope:
JSON
{
"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:
JSON
{
"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).
PHP:
PHP
function verifySignature(string $body, string $timestamp, string $signature, string $secret): bool {
if (abs(time() - (int)$timestamp) > 300) return false;
$expected = 'sha256=' . hash_hmac('sha256', "{$timestamp}.{$body}", $secret);
return hash_equals($expected, $signature);
}
Python:
Python
import hmac, hashlib, time

def verify(body: bytes, timestamp: str, signature: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = 'sha256=' + hmac.new(
secret.encode(), f'{timestamp}.{body.decode()}'.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

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.



Contact & Support



Support email
Bug reports / feedback
Provider
Healthier Business Group
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.
Last updated 28 Oct 20267 min readNeed help? Contact support