Guides

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:

  1. List appointment templates — show available service types
  2. Find a free slot — query availability
  3. Hold the slot — reserve it while the patient completes the form
  4. Find or create the patient — search existing records or create new, recording their GP
  5. Confirm the hold — turn it into the appointment

Required API Scopes

Your API key needs these scopes:

ScopePurpose
read_appointment_templatesList available service types
read_availabilityFind free slots
read_patientsSearch existing patients
write_patientsCreate new patients and record their GP
write_appointmentsHold 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 CodeHTTP StatusRecovery
VALIDATION_ERROR422Check the fields named in error.details.fields
SLOT_UNAVAILABLE409The slot has gone; refresh availability
HOLD_EXPIRED / HOLD_RELEASED410Hold the slot again, or choose another
HOLD_ALREADY_USED409Already booked with this hold; don't book twice
CONFLICT409Idempotency key reused with different body
RATE_LIMITED429Exponential backoff and retry
UNAUTHORISED401Check 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

On this page