Skip to main content

REST API

The Eirly REST API provides programmatic access to manage referrals, results, tests, biomarkers, and bundles, and to find the collection centres where patients can be tested. All API requests require authentication via OAuth 2.0.

New to the API?

See the Workflow overview first — it shows how these endpoints and webhooks fit together across referral ordering and results delivery.

Base URL: https://api.eirly.health

Current Version: v1

Swagger Documentation

For interactive API documentation and testing, visit our Swagger Documentation

API Versioning​

The API supports versioning through both URL paths and custom headers:

  • v1.0.0 (Default) - Blood tests only, single referral per request
  • v1.1.0 - Supports bundles, Medicare ordering, and multiple referrals per request

To use a specific version, include the Accept header:

Accept: application/vnd.bgt.v1.1.0+json

Authentication​

All API endpoints require authentication. Include your access token in the Authorization header:

Authorization: Bearer YOUR_ACCESS_TOKEN

Timeouts​

The ordering endpoint /referrals can take upwards of 10 seconds to complete, so please allow for this to avoid duplicate orders being sent if you have a retry strategy in place.

Core Resources​

Referrals​

Referrals represent test orders for patients. A referral contains patient information, requested tests, and metadata.

The referral payload will return an origin property, possible values are:

export enum ReferralOrigin {
ConsumerWebsite = 'CONSUMER_WEBSITE',
BundlePurchase = 'BUNDLE_PURCHASE',
Api = 'API',
StoreIntegration = 'STORE_INTEGRATION',
Unknown = 'UNKNOWN',
}

The referral status property progresses through the following values across the referral lifecycle:

export enum ReferralStatus {
PendingVerification = 'PENDING_VERIFICATION', // awaiting verification before issue
Created = 'CREATED', // created but not yet issued
Issued = 'ISSUED', // PDF generated and ready to deliver
ScheduledReissue = 'SCHEDULED_REISSUE', // a new PDF is being regenerated (e.g. after PATCH)
PartialResults = 'PARTIAL_RESULTS', // some but not all results returned
Complete = 'COMPLETE', // all results returned
InsufficientData = 'INSUFFICIENT_DATA', // unable to process due to missing data
Refunded = 'REFUNDED', // referral refunded
TestFailed = 'FAILED_TEST', // one or more tests failed at the lab
BillingFailed = 'BILLING_FAILED', // billing could not be completed
Transferred = 'TRANSFERRED', // referral transferred to another provider
}

Only referrals with status CREATED or ISSUED can be updated via PATCH. See Update Referral for details.

Create Referral​

POST /v1/referrals

Creates a new referral for a patient.

Request Body:

{
"firstName": "John",
"lastName": "Vein",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "123 Example Way",
"addressLine2": null,
"postCode": "4000",
"city": "Brisbane",
"country": "Australia",
"state": "QLD"
},
"dob": "1980-01-01",
"gender": "male",
"phone": "0412345678",
"tests": ["tft", "free-testosterone"],
"metadata": {
"internalId": "1234"
},
"reference": "48213907",
"referenceNumber": "12345678",
"emailReferralToCustomer": true
}

Optional Fields:

FieldTypeDefaultDescription
emailReferralToCustomerbooleantrueControls whether Eirly emails the referral PDF directly to the customer. Set to false if you want to deliver the referral to the customer yourself — when disabled, Eirly will not send the referral email and you are responsible for getting it to the patient.

For v1.1.0 - Bundle Support:

{
"firstName": "John",
"lastName": "Vein",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "123 Example Way",
"postCode": "4000",
"city": "Brisbane",
"country": "Australia",
"state": "QLD"
},
"dob": "1980-01-01",
"gender": "male",
"phone": "0412345678",
"bundleId": "56ce2120-5480-4657-9e47-7ae4ecf9229b",
"metadata": {
"internalId": "1234"
}
}

Choosing between bundleId and tests:

On v1.1.0, a referral can be created from a pre-configured bundle, an ad-hoc list of tests, or both. At least one of tests, bundleId, or medicareTests must be provided — sending none will return a 400 error.

ProvidedBehaviour
tests onlyThe referral is created with exactly the tests you listed.
bundleId onlyThe referral is created with the full set of tests defined on that bundle. The bundle's productId / productName will be attached to the referral.
bundleId and testsThe referral includes every test in the bundle plus any additional tests you pass in tests. Use this when you want to extend a standard bundle on a per-patient basis (e.g. add vitamin-d to a "Men's Health" bundle). Duplicate tests are de-duplicated server-side.

Notes:

  • bundleId is only accepted on v1.1.0 and above. Sending a bundleId against v1.0.0 will return a 400 error directing you to upgrade the version header.
  • The response's tests array reflects the fully-resolved list of tests on the referral, regardless of whether they came from a bundle, the tests field, or both.

For v1.1.0 - Medicare ordering:

A single request can also order Medicare tests by passing medicareTests. These tests are ordered under a doctor's provider number and claimed against Medicare — they are not billed to your organisation. Commercial tests (tests / bundleId) continue to be billed exactly as before.

note
medicareTests is not a guarantee that the patient pays nothing

The field name says how the test is ordered — under Medicare, with the practitioner's provider number. Whether Medicare covers a given test in full is determined by the collecting laboratory and the patient's Medicare eligibility at the time of collection, and the patient may still be charged a gap or the full fee. Do not present these tests to your customers as guaranteed to be free.

When medicareTests is provided, the request produces two separate referrals — one commercial referral for tests/bundleId and one Medicare referral for medicareTests (issued on a Medicare request form). The response is therefore an array of referrals, and the customer receives a single email with both referral PDFs attached.

Prerequisite — the provider must be set up on the platform first

orderingPractitioner.providerNumber is not accepted as a free-form value. Before it can be used in an API request, the provider must be configured on the Eirly platform:

  1. The provider number must be registered against the organisation that owns your API credentials. All API access is scoped to a single organisation, and the provider number you send must belong to a practitioner registered in that organisation. A provider number that isn't registered for your organisation is rejected with INVALID_PROVIDER_NUMBER.
  2. That practitioner must have a signature on file for the organisation. The signature is drawn and stored on the platform (Settings → Provider Numbers) — it cannot be supplied through the API — and it is what gets printed on the Medicare referral. If no signature is on file for the provider number, the request is rejected with MISSING_PROVIDER_SIGNATURE.

This is a one-time, per-practitioner, per-organisation setup done in the app, not via the API. See the Practitioners → Medicare guide for the full setup flow.

{
"firstName": "Jane",
"lastName": "Doe",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "456 Sample Street",
"postCode": "2000",
"city": "Sydney",
"country": "Australia",
"state": "NSW"
},
"dob": "1992-05-15",
"gender": "female",
"phone": "0498765432",
"tests": ["free-testosterone"],
"medicareTests": ["vitamin-d", "iron-studies"],
"orderingPractitioner": {
"name": "Dr Bill Smith",
"address": "123 Example St, Sydney NSW 2000",
"providerNumber": "2426621B"
},
"medicareCardNumber": "21234567011",
"dvaNumber": "QX123456",
"clinicalNotes": "Fatigue, ?anaemia",
"referenceNumber": "55667788"
}

Medicare fields:

FieldTypeRequiredDescription
medicareTestsstring[]—Tests to order under Medicare using the practitioner's provider number. Produces a separate Medicare referral. Not a guarantee that the patient pays nothing.
orderingPractitionerobjectwhen medicareTests is presentThe ordering doctor: name, address, and providerNumber (all required). The providerNumber must be registered — with a signature — against your organisation (see prerequisite above).
medicareCardNumberstringwhen medicareTests is presentPatient's Medicare card number including the IRN — 11 digits. The IRN is the single digit printed beside the patient's name on the card; see the note below. On a commercial order (no medicareTests) the 10-digit card number is accepted.
dvaNumberstring—Patient's DVA file number, if applicable.
clinicalNotesstring—The ordering doctor's clinical notes for the Medicare referral. Kept separate from Eirly's auto-generated clinical notes.

Notes:

  • Medicare ordering requires v1.1.0 and above. Sending medicareTests against v1.0.0 returns a 400 error directing you to upgrade the version header (the v1.0.0 single-referral response cannot represent two referrals).
  • The provider number must be registered against your organisation. The organisation is determined by the API credentials used for the request, and the providerNumber you send must belong to a practitioner registered in that organisation. If it isn't, the request is rejected with INVALID_PROVIDER_NUMBER.
  • A signature must be on file for that provider number. Signatures are captured on the platform (Settings → Provider Numbers), not via the API. A provider number with no signature on file is rejected with MISSING_PROVIDER_SIGNATURE. Omitting orderingPractitioner entirely returns MISSING_PROVIDER_NUMBER.
  • medicareCardNumber must include the IRN. The 10 digits printed across the top of a Medicare card identify the card, and everyone named on it shares them. The IRN — the single digit printed beside each person's name — is the only part that identifies one individual, so an order under Medicare has to carry it. Send all 11 digits. A 10-digit number with medicareTests returns a 400 with INVALID_PAYLOAD.
  • Medicare card numbers are checksum validated on every request, whether or not the order is under Medicare. A number that fails the checksum returns a 400. On a commercial order the card number is optional and the IRN is not required — there is no "which person on the card" question to answer — but if you send one it still has to be valid.
  • orderingPractitioner.providerNumber must also be a valid 8-character Medicare provider number. Malformed identifiers return a 400.
  • Only pathology (blood) tests can be ordered under Medicare. Including a non-eligible test in medicareTests is rejected with INVALID_MEDICARE_TEST.
  • The same test id may appear in both tests and medicareTests — ordering the same test as both commercial and Medicare is valid and yields it on both referrals. Duplicates within a single list are de-duplicated.
  • You may send medicareTests on its own (no tests/bundleId) to create a Medicare referral only.
  • Result routing is advisory, not a prerequisite. The Medicare referral is always created regardless of whether the provider number has result routing configured. The Medicare referral in the response carries a routingMessage describing whether (and from which labs) the practitioner's Medicare results will route back to them automatically, and — when routing isn't set up — how to configure it. See the routingMessage field in the response table below.

Response (200 OK):

{
"uuid": "254fa374-6d05-4a41-bfd8-254ba2039049",
"status": "ISSUED",
"object": "referral",
"tests": ["tft", "free-testosterone"],
"metadata": {
"internalId": "1234"
},
"productId": null,
"productName": null,
"origin": "API",
"person": {
"firstName": "John",
"lastName": "Vein",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "123 Example Way",
"addressLine2": null,
"postCode": "4000",
"city": "Brisbane",
"country": "Australia",
"state": "QLD"
},
"phone": "0412345678",
"dob": "1980-01-01",
"gender": "male"
},
"referralType": "blood",
"reference": "48213907",
"referenceNumber": "12345678",
"referralAttachment": "https://api.eirly.health/download/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWxlVXVpZCI6IjJhNGRkYjALTk5MItNDUxMy1iMTAxLTMzMjBmZmYzMzAwMSIsInByb2ZpbGVVdWlkIjoiMTQxZjQ5NTgtjYzJkYi00NDY1LIwMTEtODc3MmZmZjQzMzU3IiwicmFuZG9tVmsdWUiOiJBTHJrOXNYM01iWnFXcGc4UFNRdGZmS3Fd6dGRzZ24iLCJvcmdhbmljzYXRpb25VdWlkIjoiNWQ0NDkxOTQtYzVjMS00DUzLWIyNjTQtZGRhNzg0MjdkZjllIiwiZm9y2VEb3dubG9hZCI6dHJ1ZSwiaWF0IjjoxNzc5NDI2MjExLCJleHAiOjdE3Nzk0MjcxMTF9.x7Hj2vP9aQmLZc5dRngK4tUyB1eF6oI3wJxN8sVrTYg",
"emailSentAt": "2024-06-04T05:29:09.385Z",
"createdAt": "2024-06-04T05:29:06.508Z",
"updatedAt": "2024-06-04T05:29:09.385Z",
"deletedAt": null,
"resultsUuid": null,
"medicare": false,
"orderingPractitioner": null,
"medicareCardNumber": null,
"dvaNumber": null,
"orderingPractitionerClinicalNotes": null,
"routingMessage": null
}

Notable response fields:

FieldTypeDescription
referencestringOur unique reference for this referral (8 digits). Distinct from referenceNumber, which is the value you supplied on create.
referralAttachmentstring (url) | nullSigned URL to the referral PDF. null until the PDF has been generated (e.g. while status is CREATED or SCHEDULED_REISSUE), and null for deleted referrals. URLs are time-limited — re-fetch the referral to get a fresh URL.
updatedAtstring (ISO 8601)Time the referral record was last modified. Updated whenever the referral status changes or fields are edited via PATCH.
deletedAtstring (ISO 8601) | nullSet when the referral has been soft-deleted via DELETE; null otherwise.
emailSentAtstring (ISO 8601) | nullTime the referral email was dispatched to the customer. null if emailReferralToCustomer was false, or before delivery has occurred.
resultsUuidstring (uuid) | nullUUID of the associated result once results are available (i.e. once status reaches PARTIAL_RESULTS or COMPLETE).
medicarebooleantrue if this referral was ordered under Medicare, false for a commercial referral. Not a statement about what the patient was ultimately charged.
orderingPractitionerobject | nullThe ordering doctor (name, address, providerNumber). Present on Medicare referrals only.
medicareCardNumberstring | nullPatient's Medicare card number. Present on Medicare referrals only.
dvaNumberstring | nullPatient's DVA file number. Present on Medicare referrals only.
orderingPractitionerClinicalNotesstring | nullThe ordering doctor's clinical notes. Present on Medicare referrals only.
routingMessagestring | nullAdvisory, human-readable summary of whether (and from which labs) the Medicare results will route back to the practitioner automatically. Advisory only — never blocks the order. Populated on the Medicare referral returned from Create; null on commercial referrals and on all other endpoints.

Note: When a request includes medicareTests, this endpoint returns an array of referrals (one commercial, one Medicare) rather than a single object.

Response (200 OK) — Medicare request (array):

[
{
"uuid": "254fa374-6d05-4a41-bfd8-254ba2039049",
"status": "ISSUED",
"object": "referral",
"tests": ["free-testosterone"],
"metadata": null,
"productId": null,
"productName": null,
"origin": "API",
"person": {
"firstName": "Jane",
"lastName": "Doe",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "456 Sample Street",
"addressLine2": null,
"postCode": "2000",
"city": "Sydney",
"country": "Australia",
"state": "NSW"
},
"phone": "0498765432",
"dob": "1992-05-15",
"gender": "female"
},
"referralType": "blood",
"reference": "48213907",
"referenceNumber": "55667788",
"referralAttachment": "https://api.eirly.health/download/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"emailSentAt": "2024-06-04T05:29:09.385Z",
"createdAt": "2024-06-04T05:29:06.508Z",
"updatedAt": "2024-06-04T05:29:09.385Z",
"deletedAt": null,
"resultsUuid": null,
"medicare": false,
"orderingPractitioner": null,
"medicareCardNumber": null,
"dvaNumber": null,
"orderingPractitionerClinicalNotes": null,
"routingMessage": null
},
{
"uuid": "9b1f3c7a-2e54-4d0b-8a61-1c2d3e4f5a6b",
"status": "ISSUED",
"object": "referral",
"tests": ["vitamin-d", "iron-studies"],
"metadata": null,
"productId": null,
"productName": null,
"origin": "API",
"person": {
"firstName": "Jane",
"lastName": "Doe",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "456 Sample Street",
"addressLine2": null,
"postCode": "2000",
"city": "Sydney",
"country": "Australia",
"state": "NSW"
},
"phone": "0498765432",
"dob": "1992-05-15",
"gender": "female"
},
"referralType": "blood",
"reference": "48213907",
"referenceNumber": "55667788",
"referralAttachment": "https://api.eirly.health/download/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"emailSentAt": "2024-06-04T05:29:09.385Z",
"createdAt": "2024-06-04T05:29:06.508Z",
"updatedAt": "2024-06-04T05:29:09.385Z",
"deletedAt": null,
"resultsUuid": null,
"medicare": true,
"orderingPractitioner": {
"name": "Dr Bill Smith",
"address": "123 Example St, Sydney NSW 2000",
"providerNumber": "2426621B"
},
"medicareCardNumber": "21234567011",
"dvaNumber": "QX123456",
"orderingPractitionerClinicalNotes": "Fatigue, ?anaemia",
"routingMessage": "Result routing isn't set up for provider number 2426621B, so Medicare results won't route back to you automatically. Set up result routing at https://app.eirly.health/org/your-org/settings/result-routing."
}
]

Get Referral​

GET /v1/referrals/{uuid}

Retrieves a specific referral by UUID.

Response (200 OK): Same structure as Create Referral response

List Referrals​

GET /v1/referrals

Returns all referrals for your organisation with pagination support.

Query Parameters:

  • count (integer, optional) - Number of results to return (max 20, default 20)
  • cursor (string, optional) - Cursor for pagination
  • sort (string, optional) - Sort order: asc or desc (default: asc)

Response (200 OK):

{
"cursor": {
"cursor": "MjAyNC0wNi0wNFQxMjo1OTowMS44MDJafGNyZWF0ZWRBdHxhc2M=",
"count": 20,
"resultCount": 5
},
"referrals": [
{
"uuid": "254fa374-6d05-4a41-bfd8-254ba2039049",
"status": "COMPLETE",
"object": "referral",
"tests": ["tft", "free-testosterone"],
"metadata": {
"internalId": "1234"
},
"productId": "prod_abc123",
"productName": "Comprehensive Health Check",
"origin": "API",
"person": {
"firstName": "John",
"lastName": "Vein",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "123 Example Way",
"postCode": "4000",
"city": "Brisbane",
"state": "QLD"
},
"phone": "0412345678",
"dob": "1980-01-01",
"gender": "male"
},
"referralType": "blood",
"reference": "48213907",
"referenceNumber": "12345678",
"referralAttachment": "https://api.eirly.health/download/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"emailSentAt": "2024-06-04T05:29:09.385Z",
"createdAt": "2024-06-04T05:29:06.508Z",
"updatedAt": "2024-06-04T05:29:09.385Z",
"deletedAt": null,
"resultsUuid": "d9d820a4-72e1-41bc-9fac-ffb23f18dfb4",
"medicare": false,
"orderingPractitioner": null,
"medicareCardNumber": null,
"dvaNumber": null,
"orderingPractitionerClinicalNotes": null,
"routingMessage": null
}
]
}

Update Referral​

PATCH /v1/referrals/{uuid}

Updates person details and/or metadata on an existing referral. A new referral PDF will be generated after the update.

Status Restriction

Only referrals with a status of CREATED or ISSUED can be updated. Attempting to update a referral with any other status (e.g. COMPLETE, PARTIAL_RESULTS, REFUNDED) will return an error.

All fields are optional — only the fields you include in the request body will be updated. Omitted fields remain unchanged.

Request Body:

FieldTypeDescription
firstNamestringPatient's first name (min 2 chars)
lastNamestringPatient's last name (min 2 chars)
emailstringPatient's email address
dobstringDate of birth (format: YYYY-MM-DD)
genderstringmale or female
phonestringPhone number (min 6 chars)
addressobjectAddress object (see Create Referral)
metadataobjectKey-value pairs (see Metadata Merging)

Metadata Merging:

Metadata is merged with the existing metadata on the referral rather than replaced. To remove a key, set its value to null:

{
"metadata": {
"newKey": "new-value",
"existingKey": "updated-value",
"keyToRemove": null
}
}

Example — update name and email:

{
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com"
}

Example — update address:

{
"address": {
"addressLine1": "456 New Street",
"addressLine2": "Unit 3",
"postCode": "2000",
"city": "Sydney",
"country": "Australia",
"state": "NSW"
}
}

Response (200 OK):

{
"uuid": "254fa374-6d05-4a41-bfd8-254ba2039049",
"status": "SCHEDULED_REISSUE",
"object": "referral",
"tests": ["tft", "free-testosterone"],
"metadata": {
"internalId": "5678"
},
"productId": "prod_abc123",
"productName": "Comprehensive Health Check",
"origin": "API",
"person": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"address": {
"addressLine1": "123 Example Way",
"addressLine2": null,
"postCode": "4000",
"city": "Brisbane",
"country": "Australia",
"state": "QLD"
},
"phone": "0412345678",
"dob": "1980-01-01",
"gender": "male"
},
"referralType": "blood",
"reference": "48213907",
"referenceNumber": "12345678",
"referralAttachment": "https://api.eirly.health/download/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"emailSentAt": "2024-06-04T05:29:09.385Z",
"createdAt": "2024-06-04T05:29:06.508Z",
"updatedAt": "2024-06-04T06:15:00.000Z",
"deletedAt": null,
"resultsUuid": null,
"medicare": false,
"orderingPractitioner": null,
"medicareCardNumber": null,
"dvaNumber": null,
"orderingPractitionerClinicalNotes": null,
"routingMessage": null
}
tip

After a successful update, the referral status will change to SCHEDULED_REISSUE while a new PDF is generated. Once the PDF is ready, the status will return to ISSUED.

Delete Referral​

DELETE /v1/referrals/{uuid}

Soft-deletes a referral. The referral will be marked as deleted but not permanently removed. The referralAttachment URL is cleared on deletion.

Response (200 OK):

{
"uuid": "254fa374-6d05-4a41-bfd8-254ba2039049",
"status": "COMPLETE",
"object": "referral",
"tests": ["tft", "free-testosterone"],
"metadata": {
"internalId": "1234"
},
"productId": "prod_abc123",
"productName": "Comprehensive Health Check",
"origin": "API",
"person": {
"firstName": "John",
"lastName": "Vein",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "123 Example Way",
"addressLine2": null,
"postCode": "4000",
"city": "Brisbane",
"country": "Australia",
"state": "QLD"
},
"phone": "0412345678",
"dob": "1980-01-01",
"gender": "male"
},
"referralType": "blood",
"reference": "48213907",
"referenceNumber": "12345678",
"referralAttachment": null,
"emailSentAt": "2024-06-04T05:29:09.385Z",
"createdAt": "2024-06-04T05:29:06.508Z",
"updatedAt": "2024-06-04T07:00:00.000Z",
"deletedAt": "2024-06-04T07:00:00.000Z",
"resultsUuid": "d9d820a4-72e1-41bc-9fac-ffb23f18dfb4",
"medicare": false,
"orderingPractitioner": null,
"medicareCardNumber": null,
"dvaNumber": null,
"orderingPractitionerClinicalNotes": null,
"routingMessage": null
}

Simulate Results (Staging Only)​

PUT /v1/referrals/{uuid}
Staging only

This endpoint is only available in the staging environment and will return 404 in production. It is intended for integrators testing end-to-end flows without waiting for a real lab to return results.

Generates a set of random dummy results for the referral, allowing you to exercise the result-related endpoints, webhooks, and your downstream handling code without involving a real pathology lab.

Query Parameters:

  • partial (flag, optional) — if present (e.g. ?partial), only a partial set of results is generated and the referral transitions to PARTIAL_RESULTS instead of COMPLETE. Useful for testing how your integration handles PARTIAL_RESULTS → COMPLETE transitions and the associated webhooks.

Examples:

PUT /v1/referrals/254fa374-6d05-4a41-bfd8-254ba2039049
PUT /v1/referrals/254fa374-6d05-4a41-bfd8-254ba2039049?partial

Response (200 OK): Returns the updated referral. Once results have been generated, resultsUuid will be populated and the corresponding result can be fetched via GET /v1/results/{resultsUuid}.

Results​

Results contain biomarker data and test outcomes for completed referrals.

Get Result​

GET /v1/results/{uuid}

Retrieves detailed results for a specific result UUID.

Response (200 OK):

{
"uuid": "d9d820a4-72e1-41bc-9fac-ffb23f18dfb4",
"referralUuid": "254fa374-6d05-4a41-bfd8-254ba2039049",
"status": "COMPLETE",
"object": "result",
"reference": "48213907",
"referenceNumber": "12345678",
"productId": "prod_abc123",
"productName": "Comprehensive Health Check",
"origin": "API",
"resultAttachment": "https://api.eirly.health/download/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWxlVXVpZCI6IjNiNmVjYzA4LTE0gmEtNDk4Ni1iYjYzLTdkgDJiZGZjNzU3ZSIsInByb2ZpbGVVdWlkIjoiMTQxZjQ5NTgtYzJkYi00NDY1LWIwMTEtODc3MmZmZjQzMzU3IiwicmFuZG9tVmFsdWUiOiJJWk5vN2VKOEFmcWM4bFpjS0lRcmt3YXlzZ1o2RjMiLCJvcmdhbmlzYXRpb25VdWlkIjoiNWQ0NDkxOTQtYzVjMS00NDUgLWIyNTQtZGRhNzg0MjdkZjllIiwiZm9yY2VEb3dubG9hZCI6dHJ1ZSwiaWF0IjoxNzc5NDI2MjExLCJleHAiOjE3Nzk0MjcxMTF9.A5jHeOt7W71IqN5sbxnK7jsYiZ9LO6NB7wK42F5aTCw",
"createdAt": "2024-06-04T05:30:08.699Z",
"updatedAt": "2024-06-04T05:30:08.699Z",
"deletedAt": null,
"resultDate": "2024-05-29T15:29:00.000Z",
"reviewedBy": null,
"person": {
"firstName": "John",
"lastName": "Vein",
"email": "john.vein@eirly.health",
"address": {
"addressLine1": "123 Example Way",
"postCode": "4000",
"city": "Brisbane",
"state": "QLD"
},
"phone": "0412345678",
"dob": "1980-01-01",
"gender": "male"
},
"metadata": {
"internalId": "1234"
},
"biomarkerSummary": {
"normal": 18,
"abnormal": 2,
"critical": 0,
"total": 20
},
"biomarkers": [
{
"commonName": "Active Vitamin B12",
"description": "Active Vitamin B12 (Holotranscobalamin) is the bioavailable portion...",
"formattedValue": "2 pmol/L",
"id": "active-vitamin-b12",
"isAbnormal": false,
"name": "Active Vitamin B12",
"object": "biomarker",
"unit": "pmol/L",
"mostCommonUnit": "pmol/L",
"sampleType": "blood",
"notes": "Sample was collected fasting. Serum separator tube used.",
"category": {
"id": "vitamins-and-minerals",
"name": "Vitamins & Minerals",
"description": "Vitamins and minerals are important for the body...",
"object": "biomarker-category"
},
"valueType": "numeric",
"value": "2",
"subValueType": null,
"refIntervalHigh": 4,
"refIntervalLow": 1,
"additionalRanges": null,
"canonical": {
"status": "already-standard",
"reason": null,
"value": "2",
"formattedValue": "2 pmol/L",
"unit": "pmol/L",
"subValueType": null,
"refIntervalHigh": 4,
"refIntervalLow": 1,
"additionalRanges": null,
"isAbnormal": false
}
}
]
}

Notable fields:

FieldTypeDescription
referralUuidstring (uuid)UUID of the referral this result was generated from.
resultAttachmentstring (url) | nullSigned URL to the result PDF. May be null if a PDF has not yet been generated. URLs are time-limited — re-fetch the result to obtain a fresh URL.
originstring | nullWhere the originating referral came from (e.g. API).
createdAtstring (ISO 8601)Time the result record was created.
deletedAtstring (ISO 8601) | nullSet if the result has been soft-deleted.
biomarkerSummaryobject | nullAggregated counts of normal, abnormal, critical, and total biomarkers on the result.
biomarkers[].sampleTypestring | nullThe sample type used for this biomarker (e.g. blood, urine-spot).
biomarkers[].unitstring | nullThe unit the laboratory reported the value in, exactly as it reported it. Laboratories can differ in unit and in spelling (U/L and IU/L for the same enzyme). For a value you can compare across laboratories, read canonical.
biomarkers[].mostCommonUnitstring | nullThe unit the major laboratories most often send for this biomarker, as we have observed it. A description of the data, not a standard; see List Biomarkers.
biomarkers[].canonicalobjectThe result in the biomarker's canonical unit, so results from different laboratories can be compared. Always present, on text results too. See Canonical values.
biomarkers[].notesstring | nullLab-provided notes for the individual biomarker, if any.
biomarkers[].additionalRangesarray | nullAdditional reference ranges for numeric biomarkers (e.g. menstrual cycle-phase bands for female hormones, or lipid risk targets). null for string-valued biomarkers, and null (or omitted) on numeric biomarkers that have no extra ranges. See below for the entry shape and examples.

additionalRanges entry shape:

Each entry in additionalRanges describes one alternative reference interval that applies in a specific context (cycle phase, lipid risk target, etc.). The biomarker's top-level refIntervalLow / refIntervalHigh remain the default range — additionalRanges are supplementary and consumers should continue to use the primary range for the default in/out-of-range determination.

FieldTypeDescription
idstringIdentifier for the range (e.g. cycle phase key, "high-risk").
namestringHuman-readable name for the range (e.g. "Follicular phase", "Lipid risk target").
categorystringCategory the range belongs to (e.g. "female-hormone", "lipid-risk-target").
lowRangenumber | nullLower bound of the range. null means open-ended on the low side (e.g. < highRange).
highRangenumber | nullUpper bound of the range. null means open-ended on the high side (e.g. > lowRange).

Example — female sex hormone with cycle-phase ranges (e.g. Oestradiol, FSH, LH, Progesterone):

For female hormone biomarkers, the lab cannot determine the patient's cycle phase, so the primary refIntervalLow / refIntervalHigh are typically null and all phase-specific ranges are returned via additionalRanges:

{
"refIntervalLow": null,
"refIntervalHigh": null,
"additionalRanges": [
{ "id": "follicular", "name": "Follicular phase", "category": "female-hormone", "lowRange": 46, "highRange": 607 },
{ "id": "ovulation", "name": "Ovulation", "category": "female-hormone", "lowRange": 315, "highRange": 1828 },
{ "id": "luteal", "name": "Luteal phase", "category": "female-hormone", "lowRange": 161, "highRange": 774 },
{ "id": "post-menopause", "name": "Post-menopausal", "category": "female-hormone", "lowRange": null, "highRange": 201 }
]
}

Example — lipid risk targets (e.g. Total Cholesterol, LDL, HDL, Triglycerides, Non-HDL):

Some lipid panels include a separate cardiovascular risk target (e.g. NVDPA high-risk targets) alongside the standard reference range. The standard range is reported as the primary refIntervalLow / refIntervalHigh, and the risk target is exposed as a "high-risk" entry under additionalRanges:

LDL Cholesterol — the primary range is the lab's standard reference; the additional entry is the high-cardiovascular-risk target (< 2.0 mmol/L):

{
"refIntervalLow": null,
"refIntervalHigh": 3.4,
"additionalRanges": [
{ "id": "high-risk", "name": "Lipid risk target", "category": "lipid-risk-target", "lowRange": null, "highRange": 2.0 }
]
}

HDL Cholesterol — the high-risk target is a minimum (> 1.0 mmol/L), so highRange is null:

{
"refIntervalLow": 1.0,
"refIntervalHigh": null,
"additionalRanges": [
{ "id": "high-risk", "name": "Lipid risk target", "category": "lipid-risk-target", "lowRange": 1.0, "highRange": null }
]
}

Most biomarkers will return "additionalRanges": null — only those with clinically meaningful contextual ranges (female sex hormones, lipid risk targets) populate the array.

Canonical values​

Laboratories don't all report a biomarker the same way. The unit can differ in scale (mmol/L or umol/L) or only in spelling (U/L or IU/L for the same enzyme). If you store each result exactly as reported, a patient's trend line can mix scales without anything telling you.

Every result biomarker therefore carries a canonical object. It holds the same result expressed in the biomarker's canonical unit, the one unit we have agreed for that biomarker. The laboratory's own value, unit and ranges on the biomarker are always returned unchanged. canonical is an addition, never a replacement.

canonical has the same fields as the biomarker itself, so the code that shows a result can show its canonical value too:

FieldTypeDescription
statusstringWhat canonical holds. Always set. See the table below.
reasonstring | nullWhy there is no canonical value, for not-convertible and no-standard. null otherwise.
valuestring | nullThe value in the canonical unit.
formattedValuestring | nullThe value with its unit, formatted as for the biomarker itself.
unitstring | nullThe canonical unit, as listed for this biomarker by List Biomarkers (canonicalUnit).
subValueTypestring | nullCarried over from the result. A value of <0.5 in mmol/L is <500 in umol/L.
refIntervalHighnumber | nullThe upper reference interval in the canonical unit.
refIntervalLownumber | nullThe lower reference interval in the canonical unit.
additionalRangesarray | nullAny additional ranges, in the canonical unit.
isAbnormalboolean | nullThe laboratory's own abnormal flag, copied. Converting a value and its range by the same factor cannot change it.

Every field except status and reason is null unless status is already-standard, converted or lab-reported.

status​
StatusValues?Meaning
already-standardYesThe laboratory reported in the canonical unit, possibly spelt differently. Its numbers are returned as they are, and the unit is spelt the canonical way.
convertedYesThe laboratory reported in another unit of the same kind, and the value and ranges were converted into the canonical unit.
lab-reportedYes, unconvertedThe laboratory is not one whose results are standardised (see below), so these are its own value, unit and ranges, copied. Never compare these with standardised values, even when the unit looks the same.
not-convertibleNoThe laboratory's unit can't safely be converted for this biomarker. reason says why.
unknown-unitNoWe could not read the laboratory's unit.
no-standardNoThis biomarker has no canonical unit. reason says whether one is coming.
reason​
ReasonWithMeaning
units-not-interchangeablenot-convertibleThe two units measure different things for this biomarker (for example a mass and an amount of substance), or no approved conversion joins them.
invalid-valuenot-convertibleThe value, or a range bound, is not a usable number.
under-reviewno-standardNo decision yet. Every new biomarker starts here, and a standard may or may not follow.
awaiting-conversionno-standardA standard is decided, and applies once conversion between unit families is available.
not-standardisedno-standardDecided to have none, because laboratories use units that can't be compared.
unavailableeitherWe couldn't work it out this time. Fetch the result again later rather than treating this as permanent.
Which laboratories are standardised​

Results are standardised for the major pathology networks, with all of their laboratories: 4Cyte, ACL, Healius and Sonic. SA Pathology, SydPath and results recorded by Eirly are standardised too.

Results from any other laboratory are lab-reported. That covers specialist providers that use their own units and assays, and results a patient uploads themselves. A specialist laboratory's value in a familiar-looking unit can still be on a different scale from everyone else's, which is why these are copied under their own status instead of being called standard.

How values are converted​
  • All or nothing. The value, the reference interval and any additional ranges are converted together. If any one of them can't be converted, none is, and the status is not-convertible.
  • Rounded to the laboratory's precision. Each converted number is rounded to the significant figures it had as reported, so 0.08 mmol/L becomes 80 umol/L rather than 80.00000000000001. Each range bound is rounded to its own figures in the same way.
  • Within a unit family only. Today a value converts between prefixes and spellings of the same kind of unit (mmol/L to umol/L, IU/L read as U/L), never between kinds (mass to amount of substance). Biomarkers waiting on that are no-standard with reason awaiting-conversion.
  • Text results (valueType: "string", such as A+ or Detected) have no number to convert, so the value is copied. One with no unit, or in the canonical unit, is already-standard. One from a laboratory that is not standardised is lab-reported. A text result in any other unit, such as <2 in a unit that isn't the standard, gets the status a number in that unit would, with no values.
Examples​

An illustrative creatinine result reported in mmol/L, converted to the canonical umol/L:

{
"id": "creatinine",
"valueType": "numeric",
"value": "0.08",
"formattedValue": "0.08 mmol/L",
"unit": "mmol/L",
"mostCommonUnit": "umol/L",
"refIntervalLow": 0.06,
"refIntervalHigh": 0.11,
"isAbnormal": false,
"canonical": {
"status": "converted",
"reason": null,
"value": "80",
"formattedValue": "80 umol/L",
"unit": "umol/L",
"subValueType": null,
"refIntervalLow": 60,
"refIntervalHigh": 110,
"additionalRanges": null,
"isAbnormal": false
}
}

A biomarker with no standard yet. The laboratory's own value is still on the biomarker:

"canonical": {
"status": "no-standard",
"reason": "under-review",
"value": null,
"formattedValue": null,
"unit": null,
"subValueType": null,
"refIntervalLow": null,
"refIntervalHigh": null,
"additionalRanges": null,
"isAbnormal": null
}
Using it​
  • To compare or chart results across laboratories, use canonical.value where status is already-standard or converted. Those two are in the same unit for a given biomarker, whichever laboratory sent them.
  • Keep lab-reported values apart. Show them with their own unit, and don't put them on the same chart as standardised values.
  • For everything else, fall back to the laboratory's value. A result without a canonical value is still a valid result. Show value and unit as reported.
  • Branch on status, not on whether value is null. New reasons may be added, so treat any reason you don't recognise as "no canonical value".
  • A standard can arrive later. When a biomarker that was no-standard gets a canonical unit, its results start coming back already-standard or converted. Results are re-derived on every request, so re-fetching an old result picks this up. Once a biomarker has a canonical unit, that unit doesn't change.

List Results​

GET /v1/results

Returns all results for your organisation with pagination support.

Query Parameters:

  • count (integer, optional) - Number of results to return (max 20, default 20)
  • cursor (string, optional) - Cursor for pagination
  • sort (string, optional) - Sort order: asc or desc (default: asc)

Response (200 OK): Array of results with cursor metadata

Tests​

Tests represent the available tests, their pricing, and the biomarkers each one is expected to report.

List Tests​

GET /v1/tests

Returns all available tests for your organisation.

Query Parameters:

  • include (string, optional) - Comma-separated list of optional sections to add to each test. The only supported value today is biomarkers. An unrecognised value returns 400 INVALID_PARAMETER.

By default the response does not carry biomarkers, and the key is absent rather than an empty array. [] would mean "this test reports no biomarkers", which is a different fact from "you did not ask for them".

This matters for size: the full catalogue is 374 KB by default and 893 KB with ?include=biomarkers, so the biomarkers are 58% of the larger response. Ask for them when you need them, and cache the result rather than fetching them on every call.

GET /v1/tests?include=biomarkers

Response (200 OK):

The example below is the ?include=biomarkers response. Without the parameter it is identical except that the biomarkers key is absent; no other field changes.

[
{
"id": "cortisol",
"name": "Cortisol",
"shortName": "Cortisol",
"testType": "blood",
"price": 32.03,
"priceCents": 3203,
"medicareListed": true,
"object": "test",
"biomarkers": [
{
"id": "cortisol",
"name": "Cortisol",
"commonName": "Cortisol",
"object": "biomarker",
"valueType": "numeric",
"sampleType": "blood",
"unit": "nmol/L",
"canonicalUnit": "nmol/L",
"mostCommonUnit": "nmol/L",
"standardStatus": "standardised",
"description": "A steroid hormone released by the adrenal glands.",
"category": {
"id": "hormone",
"name": "Hormones",
"object": "biomarker-category",
"description": "Hormones and their metabolites"
}
}
],
"turnAroundTimes": [
{
"maximumDays": null,
"minimumDays": null,
"providerId": "4cyte",
"providerName": "4Cyte Pathology",
"object": "providerTurnaroundTime"
}
],
"turnAroundTimeAverage": {
"maximumDays": 7,
"minimumDays": 7,
"object": "turnaroundTime"
}
}
]

Fields:

FieldTypeDescription
pricenumberThe price of the test to your organisation, in dollars. Kept for backwards compatibility.
priceCentsnumberThe same price in cents, as a whole number. Prefer this for any arithmetic, and for consistency with POST /v1/calculate, which returns cents.
testTypestringThe kind of sample the test uses, e.g. blood.
medicareListedbooleanWhether this test can be ordered under Medicare through us, i.e. whether it may appear in medicareTests on an order. See the caution below: this is not a statement that the test is Medicare-eligible for a given patient.
biomarkersarrayOnly present with ?include=biomarkers. The biomarkers this test is expected to report, each with the same shape as an item from List Biomarkers. Empty array if the test has none.
turnAroundTimesarray | nullPer-provider turnaround times. A null on minimumDays / maximumDays means we have no published figure for that provider, not that it is instant.
turnAroundTimeAverageobject | nullThe average turnaround across the providers that publish one.
caution
medicareListed is our answer, not the lab's

medicareListed says whether we will accept the test in the medicareTests array on an order. It is not a statement that the test is Medicare-eligible for a particular patient, and it is not a promise that the test will be bulk billed.

Eligibility and bulk billing are determined by the collecting laboratory and the patient's Medicare eligibility at the time of collection, and the patient may still be charged a gap or the full fee. A test with "medicareListed": false will be rejected with INVALID_MEDICARE_TEST if you send it in medicareTests.

caution
price is in dollars, not cents

price is the only money field on this API expressed in dollars. It is divided by 100 before it is returned, so it can carry a fractional part (32.03). Every other money value, including everything returned by POST /v1/calculate, is a whole number of cents. Use priceCents to avoid the mismatch.

Notes:

  • biomarkers lists what the test is configured to report. It is the expected set, not a guarantee that the laboratory returns a value for every one of them on a given result.
  • Biomarkers are largely a pathology concept: they are populated for blood tests, and most collection-kit products (stool, saliva, urine, bloodspot, allergy) return an empty array.
  • The API version does not change this response. Both v1.0.0 and v1.1.0 return the full catalogue, every test type included. The version matters when you order: bundleId and medicareTests on Create Referral require v1.1.0, and v1.1.0 returns an array of referrals where v1.0.0 returns a single one. So a test appearing here does not by itself mean your version can order it.

Biomarkers​

Biomarkers are the individual measurements a test reports. This endpoint is the full catalogue, independent of any patient or result. Use it to build a local lookup of units, categories and descriptions that you can join result biomarkers against by id.

List Biomarkers​

GET /v1/biomarkers

Returns every biomarker in the catalogue.

Response (200 OK):

[
{
"id": "cortisol",
"name": "Cortisol",
"commonName": "Cortisol",
"object": "biomarker",
"valueType": "numeric",
"sampleType": "blood",
"unit": "nmol/L",
"canonicalUnit": "nmol/L",
"mostCommonUnit": "nmol/L",
"standardStatus": "standardised",
"description": "A steroid hormone released by the adrenal glands.",
"category": {
"id": "hormone",
"name": "Hormones",
"object": "biomarker-category",
"description": "Hormones and their metabolites"
}
}
]

Fields:

FieldTypeDescription
idstringStable identifier. Matches biomarkers[].id on a result and on a test.
namestringThe formal name of the biomarker.
commonNamestring | nullA more familiar name for the same biomarker, where one exists.
unitstring | nullThe unit the biomarker is reported in. null for biomarkers that have no unit.
canonicalUnitstring | nullThe agreed unit this biomarker's results are standardised to: the unit of canonical on every result. null until one is decided.
mostCommonUnitstring | nullThe unit the standardised laboratories most often send for this biomarker, as we have observed it. null where there is too little data.
standardStatusstringWhether this biomarker has a canonical unit, or why not: standardised, under-review, awaiting-conversion or not-standardised.
valueTypestringnumeric or string. A string biomarker reports a text value and has no reference interval.
sampleTypestring | nullThe sample the biomarker is measured from, e.g. serum, urine.
categoryobject | nullThe grouping the biomarker belongs to. null for uncategorised biomarkers.

Notes:

  • The same shape appears nested under each test returned by List Tests, so one biomarker parser serves both endpoints.
  • canonicalUnit is a decision; mostCommonUnit is an observation. They usually agree, but they don't have to. mostCommonUnit can be set where there is no canonicalUnit yet, can differ from it, and can change when we survey the data again. Don't treat it as a standard. Where canonicalUnit is null, it is the best guide to the unit you will see.
  • standardStatus matches the reason a result gets when there is no standard: standardised (canonicalUnit is set and won't change), under-review (no decision yet, as for every new biomarker), awaiting-conversion (decided, and arrives once conversion between unit families is available) or not-standardised (decided to have none). A biomarker can move from under-review or awaiting-conversion to standardised.
  • This is catalogue data with no patient value attached. Reference intervals are returned per-result, on biomarkers[] of a result, because they depend on the reporting laboratory.

Bundles​

Bundles are pre-configured groups of tests that can be purchased together (v1.1.0+ only).

List Bundles​

GET /v1/bundles

Headers Required:

Accept: application/vnd.bgt.v1.1.0+json

Query Parameters:

  • count (integer, optional) - Number of results to return (max 20, default 20)
  • cursor (string, optional) - Cursor for pagination
  • sort (string, optional) - Sort order: asc or desc (default: asc)

Response (200 OK):

{
"cursor": {
"cursor": "MjAyNC0wNi0wNFQxMjo1OTowMS44MDJafGNyZWF0ZWRBdHxhc2M=",
"count": 20,
"resultCount": 5
},
"bundles": [
{
"id": "254fa374-6d05-4a41-bfd8-254ba2039049",
"name": "Comprehensive Health Panel",
"object": "bundle",
"tests": [
{
"id": "tft",
"name": "Thyroid Function Test",
"object": "test"
}
],
"createdAt": "2024-06-04T05:29:06.508Z"
}
]
}

Providers​

A provider record is a doctor's Medicare provider number, the details printed on their referrals, and their signature, held against your organisation. It is what lets you order Medicare tests on that doctor's behalf, and the practitioner never needs an account with us.

Setting one up has two halves, and they are independent:

  1. Ordering. Create the record with a provider number and signature, and that practitioner can order Medicare tests immediately.
  2. Results coming back. A pathology lab returns a result carrying only the ordering provider number. It will route that result to us only once it has confirmed with us that the number belongs to your practice. Until then, results for that practitioner go wherever they go today.

Creating the record does not start the second half. Call Request Result Routing for that — it queues the confirmation request with us, and our team then approaches the labs.

"Vendor" means a lab

The API says vendor where this page says lab: GET /v1/vendors, vendorIds, vendorId. They are the same thing. The API uses the broader word because the list can also include a message-routing service rather than a laboratory, and you request routing from both the same way. Read vendor as "somewhere results come from".

A connection is one lab's routing state for one provider number. A doctor with two labs has two connections, which is what connections[] holds on every provider payload.

You want toCall
Let a doctor order Medicare testsCreate Provider
Learn which labs you can askList Labs
Make that doctor's results come backRequest Result Routing
Check whether a lab has confirmedGet Provider
See everything you holdList Providers
Correct details, or replace a signatureUpdate Provider
Stop a doctor orderingRetire Provider

For the end-to-end sequence, including what happens between your routing request and a lab confirming, see Medicare provider setup.

Create Provider​

POST /v1/providers

Registers a doctor against your organisation. If the provider number is already held by a live record in your own organisation, that record is updated in place rather than rejected, so re-sending a doctor is safe and idempotent.

Request Body:

  • providerNumber (string, required) - Medicare provider number: a 5 or 6 digit stem, a practice location character and a check character, so 7 or 8 characters in total (a leading zero on the stem may be dropped). Validated with Medicare's checksum, which confirms it is well formed, not that it is registered
  • name (string, required) - The doctor's name as it should appear on referrals
  • address (string, required) - The practice address as it should appear on referrals
  • encodedSignature (string, required) - Base64 PNG, with or without a data:image/png;base64, prefix. PNG only — the signature is stamped onto the referral as an image, so other formats are rejected rather than converted
  • title (string, optional) - Honorific (Dr, A/Prof, Prof)
  • phone (string, optional) - Contact phone for this doctor at this practice
  • email (string, optional) - Contact email for this doctor at this practice
  • firstName / lastName (string, optional) - Used only to label the record; derived from name when omitted

Response (200 OK):

{
"object": "provider",
"providerNumber": "2426621B",
"name": "Jane Vein",
"address": "1 Practice Street, Brisbane QLD 4000",
"title": "Dr",
"phone": "0412345678",
"email": "jane.vein@example.com",
"hasSignature": true,
"signatureSignedAt": "2026-09-15T02:11:00.000Z",
"encodedSignature": "data:image/png;base64,iVBORw0KGgo...",
"createdAt": "2026-09-15T02:10:59.000Z",
"updatedAt": "2026-09-15T02:11:00.000Z",
"connections": []
}

connections is empty until you request routing.

A number held by a different organisation returns 409 PROVIDER_NUMBER_IN_USE. A provider number belongs to one practice at a time, because it is the only thing identifying who ordered a result. We do not say which organisation holds it.

List Labs​

GET /v1/vendors

The labs you can request result routing from. The id of each is what you pass in vendorIds on Request Result Routing.

Only labs we can currently reach are listed, so anything returned here is safe to request. Read this rather than hardcoding ids — the list changes as labs are added or retired, and a hardcoded id fails the moment one does.

Response (200 OK):

{
"vendors": [
{
"object": "vendor",
"id": "4cyte",
"name": "4Cyte Pathology",
"route": "via Medical Objects"
},
{
"object": "vendor",
"id": "healius",
"name": "Healius",
"route": "direct to Healius"
}
]
}

route describes how results reach us from that lab, and is informational only: it does not change how you call the API. Some labs send to us through Medical Objects, a clinical messaging service widely used in Australian pathology, and others send to us directly.

Request Result Routing​

POST /v1/providers/{providerNumber}/routing

The practitioner is identified by the provider number in the path; the body carries only the labs and your attestation. Get the vendorIds from List Labs.

What this call does: it records your attestation and creates one pending request per lab you named. That is all. It sends no email and makes no call to the lab. Nothing leaves our system when you call it.

What happens next: those requests sit at NOT_SUBMITTED until our team notifies the labs, which we do in batches because the labs are non-technical and one email per doctor gets lost. The status becomes PENDING_CONFIRMATION when a batch goes out, and CONNECTED when the lab writes back confirming — usually days to weeks later. Poll Get Provider and read connections[].status to follow it.

Why you have to call it: a lab returns a result carrying only the ordering provider number, and routes it to us only once it has confirmed with us that the number belongs to your practice. CONNECTED is that confirmation. Until a lab reaches it, results for this practitioner go wherever they go today, no matter how many orders you place.

It is a separate call rather than part of creating the record because sending a doctor's name, provider number, address and signature to a third party is a disclosure. We require an explicit act with an attestation attached rather than doing it as a side effect of creating a record.

Preconditions: the provider record exists and has a signature on file. The signature is the authority the lab is shown, so a record without one is rejected with MISSING_SIGNATURE.

Request Body:

  • vendorIds (string[], required) - The labs to ask, from List Labs. Each must be a currently enabled lab
  • attestation.attestedBy (string, required) - Who authorised this disclosure on the doctor's behalf (a name, email or staff id)
  • attestation.doctorSignedAt (string, required) - When the doctor signed the authority you hold, ISO 8601
  • attestation.reference (string, optional) - Your own reference for that authority

Response (200 OK):

{
"connections": [
{
"object": "providerConnection",
"vendorId": "4cyte",
"vendorName": "4Cyte Pathology",
"status": "NOT_SUBMITTED",
"consentedAt": "2026-09-15T02:12:00.000Z",
"confirmedAt": null,
"actionNeededReason": null
}
]
}

Routing statuses:

StatusMeaning
NOT_SUBMITTEDAuthorised, waiting for our team's next batch to the lab
PENDING_CONFIRMATIONThe lab has been asked and has not replied yet. Usually days to weeks
CONNECTEDThe lab confirmed. Results for this provider number now route to you
ACTION_NEEDEDThe lab could not proceed. See actionNeededReason
STOP_REQUESTEDRouting is being wound down at that lab

consentedAt is when you made the routing request, not when the doctor signed your paperwork. confirmedAt is when the lab confirmed, and is null until then.

Get Provider​

GET /v1/providers/{providerNumber}

Returns one provider record with its signature and the current routing status at each lab. This is the endpoint to poll while waiting for labs to confirm.

Response (200 OK): the same shape as Create Provider, with connections populated.

List Providers​

GET /v1/providers

Every provider record your organisation holds, newest first, whether it was created through this API or by a practitioner in the portal.

Query Parameters:

  • count (integer, optional) - Number of results to return (max 20, default 20)
  • cursor (string, optional) - Cursor from a previous page
  • sort (string, optional) - Sort order: asc or desc (default: desc)

Response (200 OK):

{
"providers": [
{
"object": "provider",
"providerNumber": "2426621B",
"name": "Jane Vein",
"address": "1 Practice Street, Brisbane QLD 4000",
"title": "Dr",
"phone": "0412345678",
"email": "jane.vein@example.com",
"hasSignature": true,
"signatureSignedAt": "2026-09-15T02:11:00.000Z",
"encodedSignature": "data:image/png;base64,iVBORw0KGgo...",
"createdAt": "2026-09-15T02:10:59.000Z",
"updatedAt": "2026-09-15T02:11:00.000Z",
"connections": []
}
],
"cursor": {
"cursor": "MjAyNi0wOS0xNVQwMjoxMDo1OS4wMDBafGNyZWF0ZWRBdHxkZXNj",
"count": 20,
"resultCount": 1
}
}

Every provider payload has the same shape, whichever endpoint returned it.

A cursor that did not come from this endpoint is rejected with INVALID_PARAMETER rather than silently returning the first page.

Update Provider​

PUT /v1/providers/{providerNumber}

Updates the details printed on this doctor's referrals, and optionally replaces the signature on file. Omit encodedSignature to leave the existing one alone.

The provider number itself cannot be changed: it is the record's identity, and any lab routing already confirmed is bound to it. Retire the record and create the new number instead.

Request Body: any of name, address, title, phone, email, encodedSignature.

Response (200 OK): the full provider record.

Retire Provider​

DELETE /v1/providers/{providerNumber}

Retires the record and clears the signature, so the number can no longer order.

Refused with 409 PROVIDER_HAS_ACTIVE_ROUTING while any lab is still routing results for it. The lab has to be told to stop first, or it keeps sending us results for a doctor you no longer represent. Contact support to wind routing down.

Response (200 OK):

{ "object": "provider", "providerNumber": "2426621B", "deleted": true }

Collection Centres​

A collection centre is somewhere a patient can walk in to have a sample taken: every collection centre of the labs we work with, about 5,900 across Australia. Use them to show a patient where they can go once you have ordered a referral, on a map or as a list.

Each centre belongs to a pathology provider, the brand on the door (dorevitch, laverty), and each provider belongs to a vendor, its parent company (healius). We refresh every lab's listing weekly.

You want toCall
Find the centres nearest a patientGET /v1/collection-centres?lat=…&lng=…&count=5
Load a map showing what is on screenGET /v1/collection-centres?north=…&south=…&east=…&west=…&count=10000
Load every centre at once, to cacheGET /v1/collection-centres?count=10000
Search as someone types a suburb, street or clinicGET /v1/collection-centres?search=…
Only show the labs a referral is foradd pathologyProviders=… or vendors=… to any of the above
Fetch the centres a webhook namedGET /v1/collection-centres?uuids=…, or one by uuid

List Collection Centres​

GET /v1/collection-centres

Query Parameters: every parameter is optional. List parameters take comma-separated values (states=VIC,NSW); repeating the key (states=VIC&states=NSW) works too.

ParameterTypeMatchesWhen you'd use it
uuidslistOnly these centres, up to 100, whatever their state: RETIRED centres and ones with unmappable coordinates, which every other query leaves out, are returned.Fetching the centres a collection_centres.updated delivery named.
pathologyProviderslistThe provider, by id (see Provider ids).The patient's referral is for Dorevitch, so only Dorevitch centres will accept it: pathologyProviders=dorevitch.
vendorslistThe parent company: 4cyte, acl, healius, sonic.You deal with a group, not one brand: vendors=sonic shows all ten Sonic labs.
testingTypeslistPATHOLOGY, IMAGING.Keeping a pathology map free of imaging sites. Every centre today is PATHOLOGY; IMAGING is accepted and matches none until imaging sites are listed.
stateslistState abbreviation: NSW, VIC, QLD, SA, WA, TAS, NT, ACT.A state-level list, or keeping a nationwide search to the states you operate in.
postcodeslistFour-digit postcode, exactly (0810, not 810).A patient gave you a postcode rather than a location: postcodes=3000,3002.
searchtextPart of the name, venue name, street address or suburb, ignoring case. At most 100 characters.A search box. search=Keilor finds Keilor, Keilor Downs, Keilor East and centres on Keilor Road.
changedSincetextOnly centres created, changed, gone missing, come back or retired at or after this time (ISO 8601). Also returns centres RETIRED since then, which no other query returns.Keeping your own copy in sync: pass data.ranAt from the collection_centres.updated webhook, or the time of your last sync.
lat, lngnumberSorts nearest first and adds distanceKm. Both are required together.You know where the patient is: from their address, or the browser's location.
radiusKmnumberOnly centres within this many kilometres of lat/lng. At most 5000. Needs lat and lng."Anything within 10 km of home": you would rather show nothing than something too far to be useful.
north, south, east, westnumberEverything inside the box, edges included. All four are required together.A map: pass the viewport's edges on every pan and zoom. north is the top edge, west the left.
countnumberCentres per page. Default 50; a larger value than 10000 is clamped to 10000. Below 1 or not a whole number is 400 INVALID_PARAMETER.count=5 for "the five nearest"; count=10000 for everything in one call.
cursortextThe cursor.cursor from the previous page.Fetching the next page. Send the same filters with it.

An unknown parameter or value is rejected with 400 INVALID_PARAMETER, never ignored. A typo such as pathologyProvider=dorevitch (singular) or states=Victoria would otherwise drop the filter and return every centre in the country as if it were the answer. Every problem in the request is listed in errorList, so you can fix them in one go:

{
"error": true,
"code": "INVALID_PARAMETER",
"message": "The parameter provided is invalid",
"errorList": [
"Unsupported parameter: pathologyProvider. Supported parameters: pathologyProviders, vendors, testingTypes, states, postcodes, search, lat, lng, radiusKm, north, south, east, west, count, cursor",
"A search around a point needs all of lat, lng"
]
}
How the filters combine​
  • Different filters narrow each other (AND). A centre must match every filter you pass. vendors=healius&states=VIC is Healius centres in Victoria.
  • Values within one filter widen it (OR). A centre may match any one of them. states=VIC,NSW is centres in Victoria or New South Wales.
  • A filter you leave out does not constrain. With no parameters at all you get every centre, 50 at a time, by name.
  • Provider and vendor together is allowed, but only useful when they agree: vendors=healius&pathologyProviders=dorevitch is the same as pathologyProviders=dorevitch alone, and vendors=sonic&pathologyProviders=dorevitch is empty, because Dorevitch is a Healius lab.
  • search is one more filter. search=collins&states=VIC is centres in Victoria whose name, venue, address or suburb contains "collins".
  • Order depends on whether you gave a point. With lat and lng, results are nearest first. Without them, they are by name. No other parameter changes the order.
Keeping a copy in sync​

If you store the centres yourself, subscribe to collection_centres.updated. After each weekly refresh that changes anything, it tells you which centres changed. Then fetch just those:

GET /v1/collection-centres?changedSince=2026-09-27T16:00:00.000Z&count=10000

changedSince returns every centre created, changed, gone missing, come back or retired since that time. It is the only query that returns retired centres (status: "RETIRED"), so you learn to remove them. It combines with every other filter, and a missed delivery costs nothing: query from the time of your last sync instead.

Get Collection Centre​

GET /v1/collection-centres/{uuid}

One centre by its uuid, in the same shape as a list entry, whatever its state. A centre that is MISSING, RETIRED, or has coordinates too wrong to put on a map is still returned here, although listings leave the last two out. So every uuid a collection_centres.updated delivery names can be fetched.

  • 400 INVALID_PARAMETER if {uuid} is not a uuid.
  • 404 COLLECTION_CENTRE_NOT_FOUND if it has never been a centre.

To fetch many at once, use ?uuids= on List Collection Centres, up to 100 per request.

Around a point: with or without a radius​

lat and lng on their own return every centre, nearest first, and count decides how many you see. This is how to find "the closest five" when you cannot know how far away they are:

GET /v1/collection-centres?lat=-25.898&lng=139.352&count=5

That is Birdsville, where the nearest centre is 565 km away in Longreach, and the next two are across the border in South Australia. A 500 km radius would have returned nothing.

Add radiusKm when a distant centre is no use to you. The radius is honoured exactly and never widened, so an empty result means there is nothing within it:

GET /v1/collection-centres?lat=-37.8136&lng=144.9631&radiusKm=2

Without a radius, totalResults is every centre that matches your other filters, because nothing has been excluded, only sorted. Use count to take what you need rather than paging through the rest.

near is by straight-line distance, so it crosses state borders freely. Add states if you need to stay inside one.

A map viewport​

Pass the four edges of what is on screen, and a count high enough for the busiest view:

GET /v1/collection-centres?north=-37.70&south=-37.90&east=145.10&west=144.80&count=10000

Swapped edges (a south greater than north) are rejected rather than returning an empty map. A box crossing the antimeridian is not supported; nothing in Australia needs one.

A point and a box can be combined: centres inside the box, nearest the point first, which suits a map that also highlights the centres closest to the patient.

Everything at once​

count goes up to 10,000, above the total number of centres, so a single call returns them all. A larger count is clamped to 10,000 rather than refused, and cursor.count tells you the size applied:

GET /v1/collection-centres?count=10000

Every centre with every field is several megabytes. If you are loading them to draw a map, cache the response: the data changes weekly, not by the minute.

Paging​

Paging works exactly as on every other list endpoint (see Pagination): every page with results carries a cursor, the last one included. Pass its cursor value back as the cursor parameter for the next page, and stop when cursor.resultCount is less than cursor.count. Compare with cursor.count from the response, not the count you sent, because a larger request is clamped.

Send the same filters with the cursor. A cursor is a position in one result set, so changing the filters between pages gives you a page of a different list. totalResults is the count across every page, so you can show "1 to 20 of 49" before fetching the rest.

Provider ids​
Vendor (vendors)Pathology providers (pathologyProviders)
4cyte4cyte
aclacl
healiusabbott, dorevitch, laverty, qml, tml, wdp
soniccapital, clinipath (WA), clinpath (SA), douglass-hanly-moir, hobart, launceston, melbourne-pathology, north-west-pathology, snp, southern-iml

Every pathologyProviderId in a response is hyphenated. For multi-word ids the underscore spelling some of our other data uses (douglass_hanly_moir) is accepted as a filter too, and matches the same centres.

Opening hours​

openingHours is the centre's regular week: always seven days, Monday first, whatever order the lab lists them in.

"openingHours": [
{ "day": "MONDAY", "status": "OPEN", "periods": [{ "opens": "07:30", "closes": "12:30" }, { "opens": "13:00", "closes": "15:30" }], "text": "7:30AM to 12:30PM, 1PM to 3:30PM" },
{ "day": "SATURDAY", "status": "CLOSED", "periods": [], "text": "Closed" },
{ "day": "SUNDAY", "status": "UNKNOWN", "periods": [], "text": null }
]
  • Times are local to the centre, in 24-hour HH:MM. A period that ends at midnight closes at 24:00, so closes is always later than opens.
  • A day with a break has more than one period.
  • UNKNOWN means we will not guess. The lab's listing for that day is not plain enough to read (a typo such as 8:30 AM to 1:30 AM, or a note true only on the day we read it), or it left the day out. text carries what the lab wrote, so a person can still read it.
  • openingHours is null when a lab publishes no hours at all, which today is every Sonic lab. Treat that as "unknown", not "closed".
  • Public holidays and one-off closures are not in openingHours; the lab's notices are in notes.

Response (200 OK):

{
"collectionCentres": [
{
"object": "collectionCentre",
"uuid": "cbbe9ea7-2a14-4a67-ab82-c3998869a5be",
"pathologyProviderId": "dorevitch",
"pathologyProviderName": "Dorevitch Pathology",
"vendorId": "healius",
"vendorName": "Healius",
"name": "Melbourne CBD",
"venueName": null,
"address": "Collins Arcade Level 1,, 260 Collins Street",
"city": "Melbourne",
"state": "VIC",
"postcode": "3000",
"country": "AU",
"lat": -37.8152725,
"lng": 144.9650986,
"phone": "03 9099 0008",
"notes": null,
"services": ["AHPRA", "Breath Test (Urea) - C14"],
"bookingUrl": null,
"openingHours": [
{ "day": "MONDAY", "status": "OPEN", "periods": [{ "opens": "08:00", "closes": "16:00" }], "text": "8AM to 4PM" },
{ "day": "TUESDAY", "status": "OPEN", "periods": [{ "opens": "08:00", "closes": "16:00" }], "text": "8AM to 4PM" },
{ "day": "WEDNESDAY", "status": "OPEN", "periods": [{ "opens": "08:00", "closes": "16:00" }], "text": "8AM to 4PM" },
{ "day": "THURSDAY", "status": "OPEN", "periods": [{ "opens": "08:00", "closes": "16:00" }], "text": "8AM to 4PM" },
{ "day": "FRIDAY", "status": "OPEN", "periods": [{ "opens": "08:00", "closes": "16:00" }], "text": "8AM to 4PM" },
{ "day": "SATURDAY", "status": "CLOSED", "periods": [], "text": "Closed" },
{ "day": "SUNDAY", "status": "CLOSED", "periods": [], "text": "Closed" }
],
"testingTypes": ["PATHOLOGY"],
"status": "ACTIVE",
"distanceKm": 0.26,
"lastSeenAt": "2026-09-24T13:04:49.353Z"
}
],
"totalResults": 8,
"cursor": {
"cursor": "MnxwYWdlfGFzYw==",
"count": 1,
"resultCount": 1
}
}

That is the nearest of 8 Healius centres within 2 km of Melbourne CBD (?lat=-37.8136&lng=144.9631&radiusKm=2&vendors=healius&count=1).

The fields worth knowing:

  • name is the lab's own. Some labs use the street address as the name, and several centres in one town can share a name (seven Dorevitch centres are called "Wodonga"), so show address alongside it.
  • venueName is the building or clinic the centre sits in, where the lab gives one.
  • postcode is a string, so NT's leading zero survives. Where a lab gives no postcode we look it up from the centre's coordinates, so a few may differ from the Australia Post postcode.
  • services is what the site offers, in the lab's own words. Each lab words them differently (Holter Monitor, holter), so treat them as display text, not as values to filter on.
  • status is ACTIVE, or MISSING when the lab's latest listing left the centre out. That is usually a hiccup on their side; a centre missing for three weeks running is retired and no longer returned, except as RETIRED to a changedSince query.
  • distanceKm is only set when you searched around a point, and is straight-line distance, not driving distance.

Webhooks​

A webhook sends events to a URL you control whenever something you subscribe to happens: a referral is issued, a result is complete, and so on. The events and the payloads your handler receives are described in Webhooks. These endpoints let you manage the subscriptions themselves over the API, instead of in the portal.

Webhooks set up here and in the portal are the same webhooks: one created over the API appears in Settings > Api Keys, and one created in the portal is listed here.

You want toCall
Start receiving eventsCreate Webhook
Learn the event names you can subscribe toList Webhook Events
See one webhookGet Webhook
See everything you have set upList Webhooks
Change the URL or events, or pause deliveriesUpdate Webhook
Stop receiving eventsDelete Webhook
The signing secret is not available over the API

Every delivery is signed with your organisation's signing secret in the x-bgt-hmac-sha256 header (see Security). The secret is never returned by any endpoint. Copy it from Settings > Api Keys in the portal. You do not need to open the portal before creating a webhook: the secret exists from the moment the API is enabled for your organisation.

Rules that apply to every create and update:

  • The URL must use https and must not point at a private or internal address (loopback, private networks, link-local). A host that does not resolve yet, for example one you registered minutes ago or a tunnel that is still starting, is accepted; if it never comes up, deliveries to it fail.
  • An event can be sent to a given URL by only one of your webhooks. A second webhook subscribing the same event to the same URL is refused with 409 DUPLICATE_WEBHOOK_SUBSCRIPTION, because it would deliver every event twice. Different events on the same URL, or the same events on different URLs, are fine.
  • The webhook's id is assigned by us. A request body that sends its own id, or any field not listed below, is refused with INVALID_PAYLOAD.
  • URLs are stored normalised. https://Example.com is stored and returned as https://example.com/, so compare against the url we return rather than the string you sent.

Create Webhook​

POST /v1/webhooks

Request Body:

{
"url": "https://partner.example.com/hooks/bgt",
"events": ["referral.created", "result.complete"],
"active": true
}
  • url (string, required) - An https URL on a public host
  • events (string array, required) - One or more names from List Webhook Events
  • active (boolean, optional) - Defaults to true. An inactive webhook keeps its settings but receives nothing

Response (200 OK):

{
"object": "webhook",
"id": "8c0dc7f0-d29b-4cd3-a05f-6cd993726bc8",
"url": "https://partner.example.com/hooks/bgt",
"events": ["referral.created", "result.complete"],
"active": true,
"createdAt": "2026-09-29T23:39:00.759Z",
"updatedAt": "2026-09-29T23:39:00.830Z"
}

Every webhook payload has this shape, whichever endpoint returned it.

Errors: 400 INVALID_WEBHOOK_URL (with the reason, for example it must use https.), 400 INVALID_WEBHOOK_EVENTS (naming any unknown event), 400 INVALID_PAYLOAD, 409 DUPLICATE_WEBHOOK_SUBSCRIPTION.

List Webhook Events​

GET /v1/webhook-events

The event names you can pass in events. The list grows as events are added, so read it rather than hardcoding names.

Response (200 OK):

{
"events": [
"referral.created",
"referral.issued",
"referral.complete",
"result.created",
"result.complete"
]
}

Get Webhook​

GET /v1/webhooks/{id}

Returns one of your webhooks.

Response (200 OK): the same shape as Create Webhook.

A webhook that does not exist, or that belongs to another organisation, returns 404 WEBHOOK_NOT_FOUND. A malformed id returns 400 INVALID_PARAMETER.

List Webhooks​

GET /v1/webhooks

Every webhook your organisation holds, newest first, whether it was created through this API or in the portal.

Query Parameters:

  • count (integer, optional) - Number of results to return (max 20, default 20)
  • cursor (string, optional) - Cursor from a previous page
  • sort (string, optional) - Sort order: asc or desc (default: desc)

Response (200 OK):

{
"webhooks": [
{
"object": "webhook",
"id": "8c0dc7f0-d29b-4cd3-a05f-6cd993726bc8",
"url": "https://partner.example.com/hooks/bgt",
"events": ["referral.created", "result.complete"],
"active": true,
"createdAt": "2026-09-29T23:39:00.759Z",
"updatedAt": "2026-09-29T23:39:00.830Z"
}
],
"cursor": {
"cursor": "MjAyNi0wOS0yOVQyMzozOTowMC43NTlafGNyZWF0ZWRBdHxkZXNj",
"count": 20,
"resultCount": 1
}
}

Paging works as on every other list endpoint; see Pagination.

Update Webhook​

PATCH /v1/webhooks/{id}

Changes a webhook's URL, events or active flag. Fields you leave out keep their current value, and at least one is required.

events replaces the whole list. Send every event the webhook should receive, not just the ones to add: sending ["result.complete"] to a webhook subscribed to three events leaves it subscribed to one.

Request Body: any of url, events, active. To pause deliveries without losing the webhook's settings:

{ "active": false }

Response (200 OK): the full webhook.

The same rules as Create Webhook apply, and a webhook you do not hold returns 404 WEBHOOK_NOT_FOUND.

Delete Webhook​

DELETE /v1/webhooks/{id}

Stops the webhook receiving new events. Deliveries already queued for it are still sent.

Response (200 OK):

{
"object": "webhook",
"id": "8c0dc7f0-d29b-4cd3-a05f-6cd993726bc8",
"deleted": true
}

Common Response Codes​

CodeDescription
200Success
400Bad Request - Invalid parameters or payload
401Unauthorized - Missing or invalid access token
404Not Found - Resource does not exist
500Internal Server Error

Error Response Format​

All error responses follow this structure:

{
"error": true,
"code": "ERROR_CODE",
"message": "Human readable error message",
"errorList": ["Additional error details"]
}

Common Error Codes​

CodeMessage
MISSING_AUTHORIZATION_HEADERMissing Authorization header
INVALID_JWTInvalid access token
INVALID_JWT_SIGNATUREInvalid JWT Signature
INVALID_PARAMETERThe parameter provided is invalid
INVALID_PAYLOADThe payload provided is invalid
REFERRAL_NOT_FOUNDUnable to find the specified referral
RESULT_NOT_FOUNDUnable to find the specified result
COLLECTION_CENTRE_NOT_FOUNDUnable to find the specified collection centre
MISSING_PROVIDER_NUMBERAn ordering practitioner is required for Medicare tests. Returned when the missing practitioner is the only problem with the payload; alongside other validation failures you get INVALID_PAYLOAD with every issue listed in errorList
INVALID_PROVIDER_NUMBERProvider number is not registered for your organisation (or is malformed)
MISSING_PROVIDER_SIGNATURENo Medicare signature on file for the provided provider number
INVALID_BUNDLE_IDThe bundleId provided does not match a bundle for your organisation
INVALID_STATUSThe referral cannot be updated in its current status — only CREATED and ISSUED referrals can be changed
INVALID_MEDICARE_TESTOnly pathology (blood) tests can be ordered under Medicare
INVALID_MEDICARE_NUMBERThe patient's Medicare card number is missing, fails the checksum, or is missing the IRN on a Medicare order. Returned when the card number is the only problem with the payload; alongside other validation failures you get INVALID_PAYLOAD with every issue listed in errorList
PROVIDER_NUMBER_IN_USEThat provider number is already registered to another organisation. A provider number belongs to one practice at a time; we do not say who holds it
PROVIDER_NOT_FOUNDNo provider record with that number in your organisation
PROVIDER_HAS_ACTIVE_ROUTINGThe provider cannot be retired while a lab is still routing its results. Contact support to wind routing down first
INVALID_SIGNATUREThe signature is not a base64-encoded PNG, or is too large. Checked on the decoded bytes, so a renamed JPEG is rejected
MISSING_SIGNATUREA signature must be on file for this provider before routing can be requested
INVALID_PROVIDER_DETAILSProvider name and address are both required
MISSING_ATTESTATIONA routing request needs attestedBy and a valid doctorSignedAt
NO_VENDORS_SELECTEDA routing request needs at least one lab in vendorIds
UNKNOWN_VENDOROne or more of the labs in vendorIds is unknown or not currently enabled
WEBHOOK_NOT_FOUNDNo webhook with that id in your organisation. Returned for another organisation's webhook too
DUPLICATE_WEBHOOK_SUBSCRIPTION(409) This URL already receives one of these events from another of your webhooks. Edit that webhook instead of adding a second one
INVALID_WEBHOOK_URLThe webhook URL is not valid: not https, not a URL, carries credentials, or points at a private or internal address. The message says which
INVALID_WEBHOOK_EVENTSThe event list is empty or names an unknown event. The message names it

Pagination​

Every list endpoint pages the same way, with a cursor:

  1. Make the first request without a cursor. Set count for the page size if you want something other than the default.
  2. Every page that has records carries a cursor object: { "cursor": "…", "count": 20, "resultCount": 20 }. count is the page size the server applied, and resultCount is how many records this page has.
  3. Pass cursor.cursor back as the cursor query parameter, with the same filters and sort, for the next page.
  4. Stop when cursor.resultCount is less than cursor.count, or when a page comes back with no cursor (an empty page).
let cursor = null
do {
const page = await get(`/v1/referrals?count=20${cursor ? `&cursor=${cursor}` : ''}`)
handle(page.referrals)
cursor =
page.cursor && page.cursor.resultCount === page.cursor.count
? page.cursor.cursor
: null
} while (cursor)
warning
Compare with cursor.count, not the count you sent

A larger count than an endpoint allows is capped, not refused: ask for 50 on an endpoint whose maximum is 20 and you get pages of 20, with cursor.count: 20. An integration that compares resultCount with the count it sent sees 20 is less than 50 and stops after the first page, missing everything after it. Before September 2026, cursor.count always read 20 whatever page size applied, so if your integration was written against the value you send, check it against the rule above.

EndpointDefault countMaximum count
List Referrals, List Results, List Providers, List Bundles2020
List Collection Centres5010,000

Collection centres also return totalResults, the number of records across every page.

A cursor is a position in one result set. It only works on the endpoint that issued it, and only with the same filters and sort. A negative count, a sort other than asc or desc, or a cursor that is unreadable or came from another endpoint returns 400 INVALID_PARAMETER, with the problem in errorList:

{
"error": true,
"code": "INVALID_PARAMETER",
"message": "The parameter provided is invalid",
"errorList": [
"cursor is not one this endpoint issued. Use the cursor from a previous page, or leave it out to start from the beginning"
]
}

Metadata​

Custom metadata can be attached to referrals for correlation purposes:

{
"metadata": {
"internalId": "your-internal-id",
"customField": "custom-value"
}
}

Metadata is returned with the referral and results objects, allowing you to correlate Eirly records with your internal systems.

Rate Limiting​

API rate limits apply per organisation. Contact Eirly if you require higher limits.

Support​

For API support:

  • Review the Swagger Documentation for interactive examples
  • See Webhooks for real-time notifications
  • Contact the Eirly API team for integration assistance