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:
- Initial import - Migrating existing patients to Jump EHR
- Ongoing sync - Keeping records updated as changes occur
- Deduplication - Preventing and resolving duplicate records
Required API Scopes
| Scope | Purpose |
|---|---|
read_patients | Search and retrieve patients |
write_patients | Create 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
| Field | Required | Notes |
|---|---|---|
first_name | Yes | Patient's given name |
last_name | Yes | Patient's family name |
date_of_birth | Yes | Format: YYYY-MM-DD |
nhs_number | No | UK NHS number (10 digits) |
email | No | Contact email |
phone | No | Contact phone |
address_line_1 | No | Street address |
city | No | City or town |
postcode | No | Postal code |
gender | No | male, female, or other |
custom_fields | No | Organization-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
| Field | Purpose |
|---|---|
external_id | Your system's patient identifier |
insurance_provider | Healthcare insurance details |
preferred_language | Communication preference |
marketing_consent | GDPR consent tracking |
referral_source | How 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
| Priority | Criteria | Confidence |
|---|---|---|
| 1 | NHS number exact match | High |
| 2 | Name + DOB exact match | High |
| 3 | Email exact match | Medium |
| 4 | Name fuzzy + similar DOB | Low |
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:
| Event | Trigger |
|---|---|
patient.created | New patient registered |
patient.updated | Patient 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
- Build a Booking Widget using patient records
- Learn about Recording Consultations
- Set up Webhooks for real-time sync