API Documentation is in beta. Report issues to developers@jump.health
Syncing Patient Data

Syncing Patient Data

This guide covers best practices for synchronizing patient records between your system and Jump EHR. You'll learn how to create, update, search, and deduplicate patient data.

Overview

Patient synchronization typically involves:

  1. Initial import - Migrating existing patients to Jump EHR
  2. Ongoing sync - Keeping records updated as changes occur
  3. Deduplication - Preventing and resolving duplicate records

Required API Scopes

ScopePurpose
read_patientsSearch and retrieve patients
write_patientsCreate and update patients

Creating Patients

Basic Patient Creation

const API_BASE = 'https://app.usejump.co.uk/functions/v1/api-v1';
 
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, // YYYY-MM-DD
      nhs_number: patientData.nhsNumber,
      email: patientData.email,
      phone: patientData.phone,
      address_line_1: patientData.address1,
      address_line_2: patientData.address2,
      city: patientData.city,
      postcode: patientData.postcode,
      country: patientData.country || 'United Kingdom',
      gender: patientData.gender // male, female, other
    })
  });
 
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.details || 'Failed to create patient');
  }
 
  const { data } = await response.json();
  return data;
}

Required vs Optional Fields

FieldRequiredNotes
first_nameYesPatient's given name
last_nameYesPatient's family name
date_of_birthYesFormat: YYYY-MM-DD
nhs_numberNoUK NHS number (10 digits)
emailNoContact email
phoneNoContact phone
address_line_1NoStreet address
cityNoCity or town
postcodeNoPostal code
genderNomale, female, or other
custom_fieldsNoOrganization-specific data

Searching for Patients

Before creating a patient, always search to avoid duplicates.

Search by Name

async function searchPatients(query, limit = 10) {
  const params = new URLSearchParams({
    search: query,
    limit: limit.toString()
  });
 
  const response = await fetch(`${API_BASE}/patients?${params}`, {
    headers: { 'Authorization': `Bearer ${API_KEY}` }
  });
 
  const { data } = await response.json();
  return data;
}
 
// Usage
const matches = await searchPatients('John Smith');

Search by NHS Number

The NHS number is a unique identifier for UK patients. Always search by NHS number when available.

async function findPatientByNhsNumber(nhsNumber) {
  // Format: remove spaces, search as-is
  const cleanNhs = nhsNumber.replace(/\s/g, '');
 
  const results = await searchPatients(cleanNhs, 5);
 
  // Find exact match
  return results.find(p =>
    p.nhs_number?.replace(/\s/g, '') === cleanNhs
  );
}

The NHS number is the most reliable way to match UK patients. Always check for an existing patient by NHS number before creating a new record.


Updating Patients

Use PATCH to update specific fields without overwriting the entire record.

async function updatePatient(patientId, updates) {
  const response = await fetch(`${API_BASE}/patients/${patientId}`, {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(updates)
  });
 
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.details || 'Failed to update patient');
  }
 
  const { data } = await response.json();
  return data;
}
 
// Update just the phone number
await updatePatient('pat_123', { phone: '+44 7700 900456' });
 
// Update address
await updatePatient('pat_123', {
  address_line_1: '456 New Street',
  city: 'Manchester',
  postcode: 'M1 1AA'
});

Using Custom Fields

Custom fields let you store organization-specific data that isn't part of the standard patient schema.

Setting Custom Fields

await updatePatient('pat_123', {
  custom_fields: {
    external_id: 'YOUR-SYSTEM-ID-123',
    insurance_provider: 'Bupa',
    preferred_language: 'English',
    marketing_consent: true,
    referral_source: 'website'
  }
});

Use Cases for Custom Fields

FieldPurpose
external_idYour system's patient identifier
insurance_providerHealthcare insurance details
preferred_languageCommunication preference
marketing_consentGDPR consent tracking
referral_sourceHow patient found you
⚠️

Custom fields are merged, not replaced. To remove a custom field, explicitly set it to null.


Deduplication Strategy

Before Creating: Search First

Always implement a search-before-create pattern:

async function findOrCreatePatient(patientData) {
  // 1. Try NHS number first (most reliable)
  if (patientData.nhsNumber) {
    const byNhs = await findPatientByNhsNumber(patientData.nhsNumber);
    if (byNhs) {
      console.log('Found by NHS number:', byNhs.id);
      return { patient: byNhs, isNew: false };
    }
  }
 
  // 2. Search by name + DOB
  const nameQuery = `${patientData.firstName} ${patientData.lastName}`;
  const byName = await searchPatients(nameQuery, 10);
 
  // Check for exact DOB match
  const exactMatch = byName.find(p =>
    p.date_of_birth === patientData.dateOfBirth
  );
 
  if (exactMatch) {
    console.log('Found by name + DOB:', exactMatch.id);
    return { patient: exactMatch, isNew: false };
  }
 
  // 3. No match found - create new patient
  const newPatient = await createPatient(patientData);
  console.log('Created new patient:', newPatient.id);
  return { patient: newPatient, isNew: true };
}

Matching Criteria

PriorityCriteriaConfidence
1NHS number exact matchHigh
2Name + DOB exact matchHigh
3Email exact matchMedium
4Name fuzzy + similar DOBLow

For low-confidence matches, consider flagging for manual review rather than automatically linking records.


Batch Import

For initial migrations, process patients in batches to avoid rate limits.

async function batchImportPatients(patients, batchSize = 10) {
  const results = {
    created: [],
    matched: [],
    errors: []
  };
 
  for (let i = 0; i < patients.length; i += batchSize) {
    const batch = patients.slice(i, i + batchSize);
 
    // Process batch in parallel
    const batchResults = await Promise.all(
      batch.map(async (patient) => {
        try {
          const { patient: result, isNew } = await findOrCreatePatient(patient);
          return { success: true, patient: result, isNew, source: patient };
        } catch (error) {
          return { success: false, error: error.message, source: patient };
        }
      })
    );
 
    // Categorize results
    for (const result of batchResults) {
      if (!result.success) {
        results.errors.push(result);
      } else if (result.isNew) {
        results.created.push(result);
      } else {
        results.matched.push(result);
      }
    }
 
    // Progress update
    console.log(`Processed ${Math.min(i + batchSize, patients.length)}/${patients.length}`);
 
    // Rate limiting: pause between batches
    if (i + batchSize < patients.length) {
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
 
  return results;
}

Import Report

function generateImportReport(results) {
  console.log('\n=== Import Complete ===');
  console.log(`Created: ${results.created.length}`);
  console.log(`Matched existing: ${results.matched.length}`);
  console.log(`Errors: ${results.errors.length}`);
 
  if (results.errors.length > 0) {
    console.log('\nFailed records:');
    results.errors.forEach(err => {
      console.log(`- ${err.source.firstName} ${err.source.lastName}: ${err.error}`);
    });
  }
}

Webhook Integration

Set up webhooks to receive notifications when patient data changes in Jump EHR:

EventTrigger
patient.createdNew patient registered
patient.updatedPatient details changed

This enables bidirectional sync - push changes to Jump EHR and receive changes back.

// Webhook handler example
app.post('/webhooks/jump', (req, res) => {
  const event = req.body;
 
  switch (event.type) {
    case 'patient.created':
      syncToLocalSystem(event.data.object);
      break;
 
    case 'patient.updated':
      updateLocalSystem(event.data.object, event.data.previous);
      break;
  }
 
  res.status(200).send('OK');
});

See the Webhooks documentation for setup instructions.


Data Mapping

Common External Systems

Map fields from common healthcare systems:

// Example: Mapping from EMIS
function mapFromEmis(emisPatient) {
  return {
    firstName: emisPatient.forename,
    lastName: emisPatient.surname,
    dateOfBirth: emisPatient.dob,
    nhsNumber: emisPatient.nhsNo,
    email: emisPatient.email,
    phone: emisPatient.mobile || emisPatient.homePhone,
    address1: emisPatient.address?.line1,
    address2: emisPatient.address?.line2,
    city: emisPatient.address?.city,
    postcode: emisPatient.address?.postcode,
    custom_fields: {
      emis_guid: emisPatient.patientGuid
    }
  };
}
 
// Example: Mapping from SystmOne
function mapFromSystmOne(s1Patient) {
  return {
    firstName: s1Patient.givenName,
    lastName: s1Patient.familyName,
    dateOfBirth: formatDate(s1Patient.birthDate),
    nhsNumber: s1Patient.identifier?.nhsNumber,
    email: s1Patient.telecom?.email,
    phone: s1Patient.telecom?.phone,
    address1: s1Patient.address?.street,
    city: s1Patient.address?.city,
    postcode: s1Patient.address?.postalCode,
    custom_fields: {
      systmone_id: s1Patient.id
    }
  };
}

Best Practices

Do:

  • Always search before creating new patients
  • Use NHS number as primary matching key when available
  • Store external system IDs in custom_fields for traceability
  • Process batch imports with rate limiting
  • Log all sync operations for audit purposes
⚠️

Don't:

  • Create patients without checking for duplicates
  • Overwrite the entire patient record when only updating one field
  • Ignore rate limits during bulk operations
  • Store sensitive data in custom_fields without encryption

Next Steps