Written by Technical Team | Last updated 25.09.2026 | 20 minute read
Integrating Organisation Data Service data into an NHS application now means working with the Organisation Data Terminology FHIR API, the strategic FHIR R4 interface for ODS data. The older ODS FHIR STU3 API has been retired, so new implementations should target the R4 service rather than building dependencies against the previous STU3 model.
The API exposes organisation and site information maintained by the Organisation Data Service through standard FHIR resources and supporting terminology resources. A typical integration can retrieve an organisation directly from its ODS code, search across organisations, identify organisational roles, obtain organisational relationships, process changes incrementally and resolve supporting ODS terminology.
The implementation is REST-based and uses FHIR R4 version 4.0.1. The production base URL is: https://api.service.nhs.uk/organisation-data-terminology-api/fhir
Separate sandbox and integration environments are available, so applications should treat the base URL as an environment-specific configuration value rather than embedding it directly throughout application code.
The primary resource is Organization. ODS relationships are represented through OrganizationAffiliation, while information that cannot be represented cleanly in the core FHIR resources is available through ODS-specific FHIR extensions and CodeSystem resources.
A robust integration should therefore be designed as more than a simple HTTP lookup. The application needs to understand FHIR search Bundles, ODS identifiers, organisation roles, extensions, reference terminology, pagination, incremental synchronisation and the difference between a current organisation record and the historic organisational structures surrounding it.
Key integration point: New NHS applications using Organisation Data Service (ODS) data should integrate with the Organisation Data Terminology FHIR API using FHIR R4. The legacy ODS FHIR STU3 API has been retired, so implementations should use the R4 Organization and OrganizationAffiliation resources, ODS-specific FHIR extensions and supporting terminology CodeSystems rather than building new dependencies on STU3.
Start by building an API client around the environment base URL. For production, requests are made beneath:
https://api.service.nhs.uk/organisation-data-terminology-api/fhir
For sandbox development, use:
https://sandbox.api.service.nhs.uk/organisation-data-terminology-api/fhir
The integration environment is:
https://int.api.service.nhs.uk/organisation-data-terminology-api/fhir
Keep these URLs in deployment configuration. The rest of the client should operate on relative FHIR resource paths such as /Organization, /Organization/{id} and the relevant terminology endpoints. This makes the same client deployable across development, test and production without changing application logic.
The API uses application-restricted authentication. The application itself is authenticated rather than an individual clinician or patient. Production access therefore requires the appropriate NHS API onboarding and credentials. Credentials should be injected through a secret store or deployment environment and never compiled into application binaries or committed to source control.
For example, a server-side client can read the API key and base URL from its environment and request an organisation by ODS code without embedding either credential in application code:
curl \
--request GET \
--header "Accept: application/fhir+json" \
--header "apikey: ${NHS_API_KEY}" \
"${ODS_FHIR_BASE_URL}/Organization/RJY"
When the ODS code is already known, retrieve the organisation directly rather than performing a search:
GET /Organization/RJY
The ODS code functions as the logical identifier used in the resource URL. A successful response is a FHIR Organization resource rather than a search Bundle.
A production client should validate resourceType before processing the payload. It should not assume that every HTTP 200 response contains the expected shape simply because the endpoint was correct.
At minimum, map the following fields into your application model where present:
The resource id normally represents the ODS code. The identifier structure provides the formal business identifier and should still be parsed rather than discarded. A typical identifier uses the ODS organisation-code identifier system and contains the ODS code as its value.
This distinction matters in application architecture. Treat the ODS code as a business identifier in your domain model even when it is also being used as the FHIR logical ID. Avoid designing database relationships around assumptions that every FHIR API will use a business identifier as its URL-level resource ID.
The active property should be persisted separately from your own local deletion status. An inactive ODS organisation is still an organisation record. Do not delete an organisation locally merely because active becomes false. Existing clinical records, referrals, reports, transactions or audit records may continue to reference that ODS code.
Organisation roles require additional parsing. ODS represents role data through an England-specific organisation-role extension rather than reducing all organisational classification to Organization.type. Each role instance can contain a role code, instance identifier, active state and associated legal or operational periods.
The safest implementation is therefore to create a domain structure such as OrganisationRole containing the role code, display value, active state, legal dates and operational dates rather than flattening the organisation to a single organisationType string.
An organisation can have one primary role and multiple non-primary roles. A system that stores only one role can silently discard information required later for filtering, access decisions or organisational reporting.
Dates need similar care. ODS can distinguish legal dates from operational dates. Do not collapse both into a generic openedAt or closedAt property unless the consuming domain explicitly requires that transformation. Preserve the semantics at the integration boundary and derive simplified properties further downstream.
Address handling should also be defensive. Do not assume every organisation has three address lines, a postcode or a UPRN. FHIR arrays may be absent or contain varying numbers of values. Store address lines as an ordered collection, then construct display addresses separately.
Where the UK Core AddressKey extension provides a UPRN, parse it independently of the textual address. A UPRN is useful as a structured property identifier and should not be inferred by attempting to normalise an address string.
Finally, use the FHIR meta block for diagnostic and integration purposes rather than confusing it with ODS business dates. meta.lastUpdated, for example, is FHIR resource metadata and should not automatically be treated as the ODS business-level Last Change Date. Parse the ODS-specific last-change extension when implementing ODS synchronisation.
Use GET /Organization when the application does not already know the ODS code. The endpoint returns a FHIR searchset Bundle, even when no organisations match.
A name search can be performed with:
GET /Organization?name=Leeds
The standard name behaviour searches from the beginning of the organisation name. It is therefore not interchangeable with a general substring search. If a user enters Leeds, the query can match an organisation whose name starts with Leeds but should not be treated as equivalent to finding Leeds anywhere in the name.
For substring behaviour, use:
GET /Organization?name:contains=Leeds
For an exact organisation name, use:
GET /Organization?name:exact=LEEDS TEACHING HOSPITALS NHS TRUST
The distinction should be reflected explicitly in your application. A search box used by humans normally benefits from name:contains, while deterministic matching during data reconciliation is generally better implemented using an ODS identifier or an appropriately constrained exact query.
Name searching should not become your mechanism for organisation identity. Organisation names change and are not guaranteed to be unique. Persist the ODS code when an organisation has been selected and use that code for subsequent processing.
The API also supports organisation status:
GET /Organization?active=true
This can be combined with other parameters, allowing an application to restrict interactive searches to currently active organisations while retaining inactive records internally.
For postcode searching, distinguish prefix, containment and exact searches:
GET /Organization?address-postalcode=LS1
GET /Organization?address-postalcode:contains=4DZ
GET /Organization?address-postalcode:exact=LS1%204DZ
Equivalent search patterns are available for address city and other supported address properties. Encode query-string values properly rather than manually inserting spaces and punctuation into URLs.
Role-based queries provide considerably more control than filtering organisation names. For example, a role query can be constructed using:
GET /Organization?roleCode=RO197
Role codes are defined by the ODS organisation-role terminology. Avoid hard-coding the descriptive label as the business rule. Persist the code, and resolve its display text separately from the terminology data.
The API supports multiple role semantics. Comma-separated role values can express an OR-style search, while repeated role parameters can be used to require multiple roles. The API also exposes roleCode:not for exclusions and activeRoleCode when only currently active role assignments should be considered.
This creates an important implementation distinction. roleCode can represent roles regardless of whether the individual role is currently active, whereas activeRoleCode is appropriate when a workflow specifically needs an organisation currently performing that role.
For example, a historic reporting process may need organisations that have held a particular role at any point represented in ODS data. A current routing or organisation-selection workflow is more likely to require the active-role search.
Sites are returned through the same Organization interface rather than through a dedicated /Site endpoint. Your local data model should therefore examine record class and organisational metadata instead of inferring that every Organization represents the same kind of real-world entity.
Search responses need to be parsed as Bundles. Iterate over Bundle.entry, validate that each entry.resource.resourceType is Organization, and preserve the fullUrl only if the application needs the canonical retrieval location.
A JavaScript service can therefore treat entry as optional and explicitly filter the Bundle to Organization resources before mapping them into its application model:
const response = await fetch(
`${baseUrl}/Organization?name:contains=Leeds&active=true`,
{
headers: {
Accept: "application/fhir+json",
apikey: process.env.NHS_API_KEY
}
}
);
if (!response.ok) {
throw new Error(`ODS request failed: ${response.status}`);
}
const bundle = await response.json();
const organisations = (bundle.entry ?? [])
.map(entry => entry.resource)
.filter(resource => resource?.resourceType === "Organization");
Do not treat an empty result as an exceptional response. A valid FHIR search can return HTTP 200 with a searchset Bundle containing no matching entries. Your service layer should translate that into an empty search result rather than throwing a transport error.
When the user already supplies an ODS code but the application chooses to use FHIR search semantics, the identifier query can contain just the code or the formal identifier system and value:
GET /Organization?identifier=https://fhir.nhs.uk/Id/ods-organization-code|G81086
In most deterministic retrieval flows, direct retrieval through /Organization/{odsCode} remains simpler when the code is known.
A common implementation mistake is to deserialize Organization using only the base FHIR R4 properties and assume that all meaningful ODS information has been captured. The API deliberately combines standard FHIR elements with NHS England extensions and supporting CodeSystems.
Build the mapping layer so that unknown extensions are tolerated while known ODS extensions are parsed explicitly. Rejecting a resource whenever a new extension appears makes the client unnecessarily brittle.
A practical architecture has three layers: the raw FHIR representation, an ODS-specific mapping layer and the application-domain model.
The raw FHIR layer should remain as close as possible to the received resource. If a FHIR SDK is being used, preserve the original Organization object for debugging and future compatibility. The mapping layer should locate extensions by canonical URL, not by array position.
Never write logic such as “the first extension is the role and the second extension is the change date”. Extension ordering is not a reliable contract.
Instead, find extensions using the canonical URL. For organisation roles, identify the relevant England OrganisationRole extension, then parse its nested extensions by their own URLs or defined names. Extract the role code, active state, instance ID and typed periods into an internal role object.
Typed periods need nested parsing because their meaning is carried by a coding alongside the period. Your mapper should read the period type first and then assign the associated start and end values to the correct domain properties.
The same approach applies to typed date-time extensions such as ODS Last Change Date. Search for the matching extension URL, inspect the nested type coding, and only interpret its value as the ODS change date when its code identifies it as such.
Supporting CodeSystems should be treated as reference data rather than copied piecemeal into source code. ODS exposes terminology covering organisation roles, relationships, record classes, record-use types and additional instance datasets.
Relevant terminology includes ODSOrganisationRole, which defines valid organisational roles and identifies which roles are primary; ODSRelationship, which describes relationship types; ODSRecordClass, which distinguishes classes such as organisations and sites; and ODSRecordUseType, which describes whether a record is a full record or a reduced record maintained for referential integrity.
Additional instance-oriented CodeSystems expose data that does not fit neatly into the standard FHIR Organization representation. These include organisation relationship instances, organisation role instances, succession information, practitioner data and postcode/geographic data.
The ods-organization-code CodeSystem is particularly important when the consuming system needs ODS attributes beyond the standard Organization resource. It can expose additional organisational properties, related entities, succession information and geographic mappings.
Do not assume that every CodeSystem can sensibly be downloaded in its entirety. Some reference CodeSystems are compact enough to retrieve as a whole; large instance CodeSystems contain substantial datasets and should instead be queried for individual concepts or the specific information required by the application.
Cache stable reference terminology locally, but give that cache an explicit refresh policy. ODS terminology can change. A code that is unknown to the application should not immediately be classified as invalid simply because a locally bundled enum has not been updated.
A resilient model therefore stores codes and systems as strings. Application logic can maintain known-code mappings, but the underlying parser should still accept new values.
When displaying organisation roles to users, resolve Coding.display when supplied, but keep the formal Coding.system and Coding.code as the authoritative machine-readable pair. Never build business logic from the display string.
Relationships require another FHIR resource: OrganizationAffiliation. When searching organisations, related relationship resources can be requested using the supported _revinclude behaviour. The resulting Bundle can then contain both matched Organization resources and included OrganizationAffiliation resources.
Your Bundle parser must consequently inspect resourceType rather than assuming every entry is an organisation. Separate matching resources from included resources and link affiliations to their referenced organisations using FHIR references.
This is preferable to relying on entry ordering. A relationship should be resolved through its explicit reference, not by assuming an affiliation immediately follows the organisation it describes.
Where an application needs organisational hierarchies, build them from the relevant ODS relationship semantics rather than assuming that one FHIR partOf value gives the complete NHS structure. Healthcare organisational relationships can represent commissioning, operational, geographic and other relationships, and those concepts should remain distinguishable in the local model.
For systems that repeatedly need organisation information, calling the live API for every screen render or transaction is rarely the best NHS integration pattern. Build a local reference-data store and synchronise it where the application requires low-latency querying, complex filtering or resilience from upstream connectivity.
The API provides lastChangeDate searching specifically suited to incremental processing. It supports greater-than, less-than and equality comparisons and can be compounded to create date ranges.
A synchronisation query can therefore request organisations changed after a known checkpoint. Store the checkpoint independently of the records themselves so a partially completed import does not move the sync position prematurely.
A safe synchronisation sequence is:
This pattern prevents a failure halfway through page processing from creating an unrecoverable gap.
Make the import idempotent. Reprocessing an organisation with the same ODS code should update or replace its ODS-derived state rather than insert a duplicate row. Role instances should likewise have deterministic identities where the upstream data supplies an instance identifier.
Do not use organisation names as upsert keys.
Pagination is controlled using _count and _offset. The default search returns a limited result set, so production code must never assume that the first Bundle represents the complete query.
A request can specify a result count:
GET /Organization?_count=1000&active=true
For subsequent pages, apply an offset or follow the Bundle’s pagination links. Following the server-provided next link is generally safer than rebuilding the URL manually because it preserves all query parameters and any server-specific pagination behaviour.
For example, a synchronisation worker can request records changed after its last committed checkpoint and continue until the Bundle no longer supplies a next link:
import requests
url = (
f"{base_url}/Organization"
f"?lastChangeDate=gt{checkpoint}&_count=500"
)
while url:
response = requests.get(
url,
headers={
"Accept": "application/fhir+json",
"apikey": api_key
},
timeout=30
)
response.raise_for_status()
bundle = response.json()
for entry in bundle.get("entry", []):
resource = entry.get("resource", {})
if resource.get("resourceType") == "Organization":
upsert_organisation(resource)
url = next(
(link["url"] for link in bundle.get("link", [])
if link.get("relation") == "next"),
None
)
The API also supports _summary=count when only the number of matching resources is required. Use this for planning or reporting operations that do not need individual organisation payloads rather than downloading an entire result set simply to count it.
Control page size according to response size, not just record count. API gateway limits are based on payload size, and large organisation resources can cause a response to exceed the configured maximum even when the nominal number of records appears reasonable. If a response becomes too large, reduce _count and continue paging.
The current service also applies a throughput limit. Design the client with a shared rate-control mechanism rather than allowing every thread or application node to fire requests independently. HTTP 429 must be considered an expected recoverable condition.
For retries, use exponential back-off with jitter on temporary responses such as rate limiting and appropriate 5xx failures. Do not blindly retry every 4xx response. A malformed search parameter will remain malformed however many times it is sent.
For long synchronisation runs, checkpoint at logical boundaries. If the import processes tens of thousands of records, record page progress or use restartable time windows while ensuring that the final change-date boundary is only committed when all pages for that boundary are safely persisted.
Also account for deletions and partial deletions using the facilities exposed by the service rather than assuming that all data lifecycle changes appear solely as active=false. A reference-data synchroniser needs explicit handling for records no longer represented in their previous form.
The most reliable database design separates the ODS mirror from application-owned organisation data. For example, keep canonical ODS attributes in dedicated tables or documents while storing customer-specific labels, configuration or workflow metadata elsewhere. A synchronisation run can then replace ODS-controlled values without overwriting application-maintained data.
Store at least the ODS code, active state, canonical name, organisation/site classification, address, contacts, role instances, upstream change date and the timestamp at which your system last synchronised the record. Keep upstream timestamps separate from your own ingestion timestamp.
Treat FHIR errors as structured responses. ODS uses standard HTTP statuses and can return FHIR OperationOutcome resources containing diagnostics about invalid parameters or other processing failures.
The HTTP status should drive the high-level behaviour, while the OperationOutcome should provide diagnostic detail.
A 400 response usually indicates that the request needs correction. A 404 during direct retrieval means the requested organisation was not found. A 429 indicates rate limiting. Server-side 5xx responses should generally enter controlled retry logic.
Do not expose raw upstream diagnostics directly to end users. Log the structured diagnostic information alongside a correlation identifier, then return a domain-appropriate application message.
At the HTTP boundary, log request method, resource path, response status, latency, retry count and a generated correlation ID. Avoid logging secrets or authentication material. ODS organisation reference data does not normally create the same confidentiality profile as patient data, but the same disciplined integration logging pattern should be retained across NHS APIs.
Set both connection and request timeouts explicitly. An HTTP client with no sensible timeout can leave application workers blocked when network connectivity degrades.
Use connection pooling. Creating a new TCP and TLS connection for every organisation lookup increases latency and resource consumption. A long-lived HTTP client with pooled connections is preferable in most server-side environments.
Place the ODS integration behind an application service rather than exposing the NHS API directly to browsers or mobile clients. A backend service can protect credentials, apply caching, enforce rate limits, normalise errors and shield the rest of the product from upstream schema changes.
For interactive organisation search, introduce short-lived caching for repeat queries. For direct ODS-code lookup, longer caching can be appropriate when the application also has a reliable refresh mechanism. Do not cache “not found” indefinitely because organisations and sites can be created or changed in ODS.
When a workflow depends on current status, check the active property and the relevant active role rather than merely confirming that an ODS code exists. Existence, organisation activity and role activity are separate conditions.
For example, a workflow selecting a currently operational organisation of a particular category should verify the organisation is active and that the required role instance is active. A historic organisation might remain retrievable even though it should no longer appear as a selectable destination.
Build automated contract tests against representative FHIR responses. Tests should cover at least an organisation with several roles, an inactive organisation, a site, missing optional address fields, a UPRN extension, multiple typed periods, included OrganizationAffiliation resources, an empty search Bundle, pagination and an OperationOutcome.
Do not make tests depend solely on one hard-coded real organisation. Real reference records change. Maintain controlled fixtures representing the FHIR structures your parser must support, then use the NHS sandbox or integration environment for additional end-to-end verification.
Schema handling should be permissive for additions and strict for information your workflow genuinely requires. FHIR resources are extensible, so parsers should ignore unknown fields and extensions while preserving known semantics. A client that fails because NHS England adds an extension it does not use is unnecessarily coupled to the upstream representation.
Keep terminology handling similarly forward-compatible. Avoid language-level enums that make deserialisation fail when an unfamiliar ODS role code appears. Represent the external coding as system, code and display, then map known codes into business behaviour separately.
When an organisation cannot be resolved, keep the failure state explicit. Do not silently fabricate an organisation name from another dataset or reuse the previous organisation record unless the product has deliberately defined stale-data behaviour.
For systems using a local ODS replica, expose data freshness internally. A useful integration status record contains the time of the last successful synchronisation, latest processed ODS change boundary, number of records processed, number rejected, number retried and any unresolved failures. Monitoring should alert when synchronisation stops advancing rather than merely when a scheduled job crashes.
Deployments should also separate upstream integration failures from application failures. If ODS is temporarily unavailable but the application has a sufficiently current local reference store, read-only organisation resolution can continue against that store. Operations that require guaranteed current upstream information can then fail explicitly instead of taking the entire application offline.
The final integration boundary should present a small domain-focused interface to the rest of the software. Methods such as getOrganisationByOdsCode, searchOrganisations, getActiveOrganisationsByRole and synchroniseChangedOrganisations keep FHIR-specific details inside the adapter.
Behind that interface, retain the full ODS semantics: FHIR Organization for organisation and site records, OrganizationAffiliation for relationships, ODS extensions for role and date information, supporting CodeSystems for terminology and additional instance information, and Last Change Date queries for synchronisation.
That structure allows the rest of the healthcare application to work with meaningful organisation objects while keeping FHIR parsing, NHS API authentication, pagination, rate limiting, terminology resolution and change processing contained within a dedicated NHS integration layer. It also leaves the implementation ready to absorb new ODS roles, additional FHIR extensions and evolving NHS organisation structures without forcing those changes through every part of the application.
Is your team looking for help with NHS ODS FHIR API integration? Click the button below.
Get in touch