Creating Appointments
Step-by-step guide to creating appointments with the Jump EHR API.
This guide walks you through building an appointment booking flow using the Jump EHR API. You'll list appointment templates, find a free slot, hold it while the patient fills in their details, register the patient (with their NHS GP), and confirm the booking.
Overview
The booking flow consists of these steps:
- List appointment templates — show available service types
- Find a free slot — query availability
- Hold the slot — reserve it while the patient completes the form
- Find or create the patient — search existing records or create new, recording their GP
- Confirm the hold — turn it into the appointment
Required API Scopes
Your API key needs these scopes:
| Scope | Purpose |
|---|---|
read_appointment_templates | List available service types |
read_availability | Find free slots |
read_patients | Search existing patients |
write_patients | Create new patients and record their GP |
write_appointments | Hold slots and book appointments |
Step 1: Fetch Appointment Templates
Load the available appointment types to let users select a service.
const API_BASE = 'https://api.usejump.co.uk/v1';
async function getAppointmentTemplates() {
const response = await fetch(`${API_BASE}/appointment-templates?is_active=true`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const { data } = await response.json();
return data;
}Step 2: Find a Free Slot
async function getSlots(templateId, dateFrom, dateTo) {
const params = new URLSearchParams({
appointment_template_id: templateId,
date_from: dateFrom, // YYYY-MM-DD
date_to: dateTo, // at most 14 days after date_from
});
const response = await fetch(`${API_BASE}/availability?${params}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const { data } = await response.json();
return data.slots;
}Step 3: Hold the Slot
When the patient picks a slot, hold it so nobody else can book it while they fill in the form. A held slot disappears from availability (including the practice's own online booking) until you confirm it, release it, or it expires.
async function holdSlot(slot) {
const response = await fetch(`${API_BASE}/holds`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
// Pass the slot's fields exactly as /availability returned them.
body: JSON.stringify({
appointment_template_id: slot.appointment_template_id,
clinician_profile_id: slot.clinician_profile_id,
location_id: slot.location_id,
start_time: slot.start_time
})
});
if (response.status === 409) {
throw new Error('SLOT_TAKEN'); // refresh availability and let them choose again
}
if (!response.ok) {
throw new Error('Failed to hold slot');
}
const { data } = await response.json();
return data; // { id, status: 'active', expires_at, start_time, end_time, ... }
}Holds last 15 minutes by default (set hold_minutes, 5-35). Show a countdown from expires_at, and call DELETE /holds/{id} if the patient abandons the form so the slot is freed straight away.
Step 4: Find or Create the Patient
Search for existing patients to avoid duplicates:
async function searchPatients(query) {
const response = await fetch(
`${API_BASE}/patients?search=${encodeURIComponent(query)}&limit=5`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const { data } = await response.json();
return data;
}Create a new patient if no match is found:
async function createPatient(patientData) {
const response = await fetch(`${API_BASE}/patients`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
first_name: patientData.firstName,
last_name: patientData.lastName,
date_of_birth: patientData.dateOfBirth,
email: patientData.email,
phone: patientData.phone,
// Optional: the patient's NHS GP, named by the surgery's ODS code.
// gp_practice_name is only needed the first time your organisation
// registers a patient at that surgery.
gp_registration: patientData.gpOdsCode ? {
gp_practice_ods_code: patientData.gpOdsCode,
gp_practice_name: patientData.gpPracticeName,
registration_status: 'registered'
} : undefined
})
});
const { data } = await response.json();
return data;
}Step 5: Confirm the Hold
Book the held slot. The time, clinician and template come from the hold.
async function confirmHold(holdId, patientId, notes) {
const response = await fetch(`${API_BASE}/holds/${holdId}/confirm`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID() // safe to retry on a network failure
},
body: JSON.stringify({ patient_id: patientId, notes })
});
if (!response.ok) {
const { error } = await response.json();
// HOLD_EXPIRED / HOLD_RELEASED: hold the slot again (it may have gone).
// HOLD_ALREADY_USED: this hold is already booked; don't book twice.
// SLOT_UNAVAILABLE: the slot was taken; choose another.
throw new Error(error.code);
}
const { data } = await response.json();
return data; // the appointment
}POST /appointments with hold_id and patient_id does the same thing. You can also book without a hold by sending title, appointment_type_id and a start_time taken from GET /availability to POST /appointments. It must be a slot on offer at that moment, or you get 409 SLOT_UNAVAILABLE, and nothing holds it while your patient fills in the form. To book a time that is not a slot, send booking_mode: "custom".
Error Handling
| Error Code | HTTP Status | Recovery |
|---|---|---|
VALIDATION_ERROR | 422 | Check the fields named in error.details.fields |
SLOT_UNAVAILABLE | 409 | The slot has gone; refresh availability |
HOLD_EXPIRED / HOLD_RELEASED | 410 | Hold the slot again, or choose another |
HOLD_ALREADY_USED | 409 | Already booked with this hold; don't book twice |
CONFLICT | 409 | Idempotency key reused with different body |
RATE_LIMITED | 429 | Exponential backoff and retry |
UNAUTHORISED | 401 | Check API key |
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}Next Steps
- Learn about Patient Sync for managing patient records
- Set up Episodes to track post-appointment workflows
- Explore the API Reference for all endpoints
