Complete API integration guide for lab partners. Covers authentication, orders, users, test results, webhooks, and end-to-end examples.
Version: 1.0
Last Updated: February 16, 2026
Base URL: https://api.diagnostic.ly/api
API Documentation (Swagger): https://api.diagnostic.ly/docs
Diagnostic.ly (GDT - Global Diagnostic Technology) is a healthcare diagnostics orchestration platform. It manages the full lifecycle of diagnostic testing:
Order Placement -> Kit Shipment -> Specimen Collection -> Lab Processing -> Result Reporting -> Provider Review
As a lab partner, you will:
Before you can make API calls, you need the following (provisioned by Diagnostic.ly staff):
| Item | Description | Example |
|---|---|---|
| API Key | Your unique authentication key | sk_live_abc123... |
| Account ID | Your account identifier (one or both) | external_id: "LAB-001" or gdt_id: 42 |
| Permitted Methods | List of API methods enabled for your account | createOrder, getUsersList, getTestResults |
| Bundle SKUs | SKUs for the test bundles available to you | BDL-STI-001, BDL-COVID-002 |
| Webhook URL (optional) | Your endpoint for receiving outbound status updates | https://yourlab.com/webhooks/diagnostic |
| Webhook Key (optional) | Bearer token Diagnostic.ly will use when calling your webhook | wh_key_xyz789... |
Base URL: https://api.diagnostic.ly/api
Content-Type: application/json
Max Payload: 50 MB
Every API request must include two pieces of identification:
POST /api/order HTTP/1.1
Host: api.diagnostic.ly
Content-Type: application/json
api-key: YOUR_API_KEY_HERE
Important: The header name is lowercase
api-key(with a hyphen).
The account_id field is an object. You must provide at least one of:
{
"account_id": {
"external_id": "YOUR-EXTERNAL-ID",
"gdt_id": 42
}
}
| Field | Type | Max Length | Description |
|---|---|---|---|
external_id | string | 64 chars | Your system’s identifier for the account (alphanumeric) |
gdt_id | number | – | Diagnostic.ly’s internal integer ID |
You may provide both, but at least one is required.
api-key header and trimmedinbound_api_key stored in support_entity_credentials| Scenario | Status | Response |
|---|---|---|
| Missing API key | 400 | {"type": "ClientError", "statusCode": 400, "message": "API key is required"} |
| Missing account ID | 400 | {"type": "ClientError", "statusCode": 400, "message": "Account id is required"} |
| Account not found | 400 | {"type": "ClientError", "statusCode": 400, "message": "Account (X) not found or not linked with any support entity."} |
| Inactive account | 400 | {"type": "ClientError", "statusCode": 400, "message": "Account (X) is not active."} |
| Inactive support entity | 400 | {"type": "ClientError", "statusCode": 400, "message": "Support entity (X) is not active."} |
| Invalid API key | 400 | {"type": "ClientError", "statusCode": 400, "message": "Invalid API key"} |
| Method not permitted | 400 | {"type": "ClientError", "statusCode": 400, "message": "Invalid api method"} |
Many resources use the same ID object pattern for identification:
{
"external_id": "string (max 64 chars)",
"gdt_id": 123
}
At least one of the two fields must be provided. This pattern appears in account_id, user_id, group_id, etc.
| Context | Format | Example |
|---|---|---|
| Date of Birth (DOB) | YYYY-MM-DD | "1990-05-15" |
| API response dates | MM/DD/YYYY | "02/16/2026" |
| Date-time filter | YYYY-MM-DD HH:mm:ss | "2026-02-16 14:30:00" |
| Date filter (short) | YYYY-MM-DD | "2026-02-16" |
| ISO full | YYYY-MM-DDTHH:mm:ss.sssZ | "2026-02-16T14:30:00.000Z" |
All phone numbers must include the country code prefix:
Pattern: ^\+\d{10,15}$
Example: "+12125551234"
{
"statusCode": 200,
"message": "Descriptive success message",
"data": { ... }
}
{
"type": "ClientError",
"statusCode": 400,
"message": "Descriptive error message"
}
POST /api/user
Registers a new patient or staff member in the system.
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
account_id | object | YES | { external_id?, gdt_id? } – at least one | Your account identifier |
external_id | string | No | Max 64, alphanumeric (/^[a-zA-Z0-9\s]+$/) | Your system’s ID for this user |
name | string | YES | Max 64, alphanumeric, trimmed | Patient’s first name |
email | string | YES | Valid email, max 64 | Patient’s email address |
active | boolean | YES | true or false | Whether user is active |
country_code | string | YES | 2-3 chars, non-numeric (ISO2 or ISO3) | e.g., "US", "USA" |
phone_number | string | YES | Validated against country_code | e.g., "+12125551234" |
timezone_support | number | YES | 1 or 2 | Timezone preference |
timezone_id | string | No | Pattern: /^[\w/-]+$/, allows empty | e.g., "America/New_York" |
language_id | number | No | Integer | Language preference ID |
user_type | number | YES | See enum below | Determines available fields |
role | number | No | 1-10 | Contact type ID |
communication_preferences | number | No | 0 or 1 | Communication opt-in |
user_type Values| Value | Meaning | Extra Required Fields |
|---|---|---|
1 | Patient | Unlocks: dob, gender, race, ethnicity, group_name, custom_attributes |
2 | Account Admin | -- |
3 | Observer | approved_bundles_for_observation (array of bundle IDs) |
5 | Manager | manager_privilege (array of integers 1-10) |
7 | Provider | license_id, credentials (both required). Unlocks:
permit_provider_
, suppress_provider_ booleans |
8 | Medical Staff | license_id, credentials (both required) |
10 | Support Entity Manager | manager_privilege (array, values: 2, 11) |
user_type = 1)| Field | Type | Required | Validation |
|---|---|---|---|
dob | date | No | ISO date, stored as YYYY-MM-DD |
gender | string | No | "Male" or "Female" |
gender_identity | string | No | Max 100 chars, trimmed |
race | string | No | Validated against account’s allowed options |
ethnicity | string | No | Validated against account’s allowed options |
group_name | string | No | Must exist in system, trimmed |
custom_attributes | object | No | Keys: string max 100. Values: string max 255. Max 250 pairs. |
user_type = 7)| Field | Type | Required |
|---|---|---|
license_id | string | YES (alphanumeric, max 30) – NPI number |
credentials | string | YES (alphanumeric, max 30) |
permit_provider_to_manage_assigned_patients | boolean | No |
permit_provider_add_and_manage_new_patients | boolean | No |
suppress_provider_access_to_test_results | boolean | No |
suppress_provider_access_to_patient_device_data | boolean | No |
{
"account_id": { "external_id": "LAB-001" },
"external_id": "PAT-12345",
"name": "John Doe",
"email": "john.doe@example.com",
"active": true,
"country_code": "US",
"phone_number": "+12125551234",
"timezone_support": 1,
"timezone_id": "America/New_York",
"user_type": 1,
"dob": "1990-05-15",
"gender": "Male",
"gender_identity": "Man",
"race": "White",
"ethnicity": "Not Hispanic or Latino",
"communication_preferences": 1,
"custom_attributes": {
"department": "Engineering",
"employee_id": "E12345"
}
}
{
"message": "User created successfully!!!",
"data": {
"gdt_id": 456,
"external_id": "PAT-12345",
"created_at": "02/16/2026"
}
}
Note: Duplicate users (by email within the same account) will be rejected.
PUT /api/user/:id
Updates an existing user. The :id param is the user’s gdt_id or external_id.
All fields from the create schema are accepted but all are optional except account_id. The same conditional logic for user_type-dependent fields applies.
{
"account_id": { "external_id": "LAB-001" },
"name": "John Updated",
"phone_number": "+13105559876",
"active": true
}
{
"message": "User updated successfully!!!",
"data": {
"gdt_id": 456,
"external_id": "PAT-12345",
"updated_at": "02/16/2026"
}
}
GET /api/user?account_id=YOUR_ACCOUNT_ID
Returns all users for your account.
Note: No pagination is implemented. All users are returned at once.
{
"message": "Users fetched successfully!!!",
"data": [
{
"account_id": {
"internal_id": 42,
"external_id": "LAB-001"
},
"user_id": {
"internal_id": 456,
"external_id": "PAT-12345"
},
"email": "john.doe@example.com",
"name": "John",
"last_name": "Doe",
"dob": "1990-05-15",
"gender": "Male",
"race": "White",
"ethnicity": "Not Hispanic or Latino",
"gender_identity": "Man",
"phone_number": "+12125551234",
"active": true,
"account_active": 1
}
]
}
DELETE /api/user
Soft-deletes a user (sets is_deleted = 1, active = 0).
{
"account_id": { "external_id": "LAB-001" },
"user": {
"gdt_id": 456,
"external_id": "PAT-12345",
"email": "john.doe@example.com"
}
}
The
userobject requires at least one ofgdt_id,external_id, or
{
"message": "User with 456 deleted successfully!!!"
}
POST /api/order
Places a new diagnostic test order. This is the primary integration endpoint.
| Field | Type | Required | Default | Validation |
|---|---|---|---|---|
account_id | object | YES | – | { external_id?, gdt_id? } – at least one |
order_type | number | YES | – | 1, 2, or 3 (see enum below) |
bundles | array | YES | – | Array of bundle objects (min 1) |
ship_to | object | YES | – | Recipient/patient information |
preassign_recipient_as_assignee | string | No | "yes" | "yes" or "no" |
payment_responsibility | string | No | "ordering_entity" | "ordering_entity", "patient", "shared", or null |
ordering_entity_info | object | No | – | Provider/ordering entity details |
language_preference | string | No | – | Two-digit ISO 639-1 code (e.g., "en", "es") |
custom_attributes | object | No | – | Key-value pairs (both strings) |
external_shipment_information | object | No | – | If you handle your own shipping |
supplemental_order_data | object | No | – | Eligibility and behavioral Q&A |
order_type Values| Value | Name | Description |
|---|---|---|
1 | Shipment | Standard order – test kit is shipped to patient |
2 | Hand to Patient | Kit is handed directly to patient (no shipment created) |
3 | Registration | Registration-only order (no physical kit) |
bundles[] – Array of Bundle ObjectsEach order must include at least one bundle.
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
sku | string | One of | – | Bundle SKU (from your provisioned list) |
external_bundle_id | string | One of | – | Vendor bundle ID (alternative to SKU) |
quantity | number | YES | Min: 1 | Number of kits to order |
flavor_id | number | YES | 1, 2, or 3 | See flavor enum below |
diagnosis_code | string | No | Allows empty | ICD-10 diagnosis code (e.g., "Z11.3") |
billing_code | string | No | Allows empty | CPT billing code (e.g., "87491") |
submit_to_insurance | string | No | – | "Yes" or "No" |
Important: At least one of
skuorexternal_bundle_idmust be provided per bundle.
flavor_id Values| Value | Name | Description |
|---|---|---|
1 | TEST_ONLY | Test kit only |
2 | TEST_AND_EDUCATION | Test kit + educational content |
3 | EDUCATION_ONLY | Educational/observation content only |
ship_to – Recipient Object| Field | Type | Required | Validation |
|---|---|---|---|
first_name | string | YES | – |
last_name | string | YES | – |
email | string | YES | Valid email |
phone | string | YES | Pattern: ^\+\d{10,15}$ |
dob | date | No | ISO date format |
gender | string | No | "Male", "Female", "Others" (case-insensitive) |
gender_identity | string | No | – |
race | string | No | – |
ethnicity | string | No | – |
address | object | YES | See address fields below |
ship_to.address – Address Object| Field | Type | Required | Validation |
|---|---|---|---|
street_1 | string | YES | – |
street_2 | string | No | Allows null and empty string |
city | string | YES | – |
region | string | YES | State/province code (e.g., "CA", "NY") |
postal_code | string | YES | – |
country | string | YES | Exactly 2 characters (ISO 3166-1 alpha-2, e.g., "US") |
ordering_entity_info – Ordering Provider (Optional)| Field | Type | Required | Validation |
|---|---|---|---|
full_name | string | No | Allows empty |
ordered_by_entity_name | string | No | Allows empty |
ordered_by_id | string | No | Allows empty (NPI or internal ID) |
ordered_by_email | string | No | Valid email, allows empty |
ordered_by_phone | string | No | Pattern: ^\+\d{10,15}$, allows empty |
payment_responsibility and Shared Payment Fields| Value | Description | Additional Fields |
|---|---|---|
"ordering_entity" | Lab/ordering entity pays (default) | None |
"patient" | Patient pays | None |
"shared" | Split payment | Must provide exactly ONE of the fields below |
null | No payment info | None |
When payment_responsibility = "shared":
| Field | Type | Notes |
|---|---|---|
shared_responsibility_patient_copay | string | Fixed copay amount. Mutually exclusive with percentage. |
shared_responsibility_patient_percentage | string | Percentage patient pays. Mutually exclusive with copay. |
shared_responsibility_patient_nottoexceed | string | Maximum patient amount. Only allowed with percentage (forbidden with copay). |
external_shipment_information (Optional)Use this if your lab handles its own shipping instead of using Diagnostic.ly’s fulfillment.
| Field | Type | Required | Validation |
|---|---|---|---|
shipment_handled_externally | string | YES (within this object) | "Y", "Yes", "yes", "N", "No", "no" |
carrier_id | string | Conditional | Max 50 chars. Required if shipment_handled_externally is "Y"/"Yes"/"yes" |
tracking_id | string | Conditional | Max 50 chars. Required if shipment_handled_externally is "Y"/"Yes"/"yes" |
supplemental_order_data (Optional)| Field | Type | Description |
|---|---|---|
eligibility_qa | array | Array of eligibility Q&A objects |
behavioral_qa | array | Array of behavioral Q&A objects |
Each Q&A object:
{
"question": "Q1",
"questiontext": "Are you over 18?",
"answertext": "Yes"
}
custom_attributes (Optional)Arbitrary key-value metadata attached to the order:
{
"custom_attributes": {
"referral_source": "provider_portal",
"internal_case_id": "CASE-2024-001",
"notes": "Priority patient"
}
}
Both keys and values must be strings.
{
"account_id": {
"external_id": "LAB-001"
},
"order_type": 1,
"preassign_recipient_as_assignee": "yes",
"payment_responsibility": "ordering_entity",
"bundles": [
{
"sku": "BDL-STI-FULL",
"quantity": 1,
"flavor_id": 1,
"diagnosis_code": "Z11.3",
"billing_code": "87491",
"submit_to_insurance": "No"
},
{
"sku": "BDL-COVID-PCR",
"quantity": 2,
"flavor_id": 2
}
],
"ordering_entity_info": {
"full_name": "Dr. Jane Smith",
"ordered_by_entity_name": "ABC Laboratory",
"ordered_by_id": "1234567890",
"ordered_by_email": "drsmith@abclab.com",
"ordered_by_phone": "+12125551234"
},
"ship_to": {
"first_name": "John",
"last_name": "Doe",
"dob": "1990-05-15",
"gender": "Male",
"email": "johndoe@example.com",
"phone": "+13105559876",
"address": {
"street_1": "123 Main Street",
"street_2": "Apt 4B",
"city": "Los Angeles",
"region": "CA",
"postal_code": "90001",
"country": "US"
}
},
"language_preference": "en",
"custom_attributes": {
"referral_source": "provider_portal",
"internal_case_id": "CASE-2026-001"
}
}
{
"statusCode": 200,
"message": "Order created successfully!!!",
"data": {
"order": {
"order_Id": 12345,
"external_id": "T3JkZXItMTIzNDU="
},
"account_id": {
"external_id": "LAB-001",
"gdt_id": 42
},
"status": "Processing",
"preassign_recipient_as_assignee": "yes",
"order_type": "Shipment",
"bundles": [
{
"sku": "BDL-STI-FULL",
"quantity": 1,
"flavor_id": 1,
"diagnosis_code": "Z11.3",
"billing_code": "87491",
"submit_to_insurance": "No"
},
{
"sku": "BDL-COVID-PCR",
"quantity": 2,
"flavor_id": 2
}
],
"ordering_entity_info": { "..." : "..." },
"ship_to": { "..." : "..." },
"custom_attributes": { "..." : "..." },
"language_preference": "en",
"customer_wallet": {
"balance": 100,
"product_units_added": 50,
"service_units_added": 50
},
"selector_url": "",
"user_selected_bundles": [],
"warnings": []
}
}
"Pending Approval" instead of "Processing".enforce_validation enabled, gender, race, ethnicity, and gender_identity are validated against a pre-approved list.preassign_recipient_as_assignee is "yes" (default), the recipient is automatically created as a user if they don’t already exist (matched by email).PUT /api/order
Updates an existing order (e.g., cancel, modify status).
Authentication: api-key header + account_id in body.
PUT /api/order/assign
Assigns an order to a specific user/patient.
Authentication: api-key header + account_id in body.
GET /api/order?account_id=YOUR_ACCOUNT_ID
Retrieves all orders for your account.
Authentication: api-key header + account_id as query param.
GET /api/order/:order_id?account_id=YOUR_ACCOUNT_ID
Retrieves a specific order.
Authentication: api-key header + account_id as query param.
GET /api/test-results
Fetches lab results filtered by various criteria. Supports pagination.
This endpoint supports two authentication modes:
auth_token parameter)api-key header| Parameter | Type | Required | Max Length | Description |
|---|---|---|---|---|
gdt_client_id | string | YES | – | GDT client identifier |
account_id | string | No | 64 chars | Filter by account |
order_id | string | No | 40 chars | Filter by specific order ID |
patient_id | string | No | 64 chars | Filter by patient user ID |
phone | string | No | 20 chars | Filter by patient phone |
email | string | No | 255 chars | Filter by patient email (valid email format) |
start_date | string | No | – | Date range start (YYYY-MM-DD) |
end_date | string | No | – | Date range end (YYYY-MM-DD) |
page | number | No | – | Page number (default: 1) |
limit | number | No | Max: 100 | Records per page (default: 10) |
auth_token | string | YES | – | Authentication token or API key |
order_id, patient_id, phone/email, or date range (start_date + end_date)start_date is provided, end_date is required (and vice versa)start_date must be <= end_datephone and email are provided, they are combined with AND logicIf multiple filters are provided, they are processed in this priority:
order_id (highest priority)patient_idphone / emailstart_date + end_date (lowest priority)| Constraint | Limit |
|---|---|
| Max records per page | 100 |
| Max orders without pagination | 500 |
| Max orders per patient | 100 (most recent) |
| Max patients for phone/email search | 50 |
| Max orders for phone/email search | 200 |
{
"status": "success",
"statusCode": 200,
"message": "Test results retrieved successfully",
"data": [ ... ],
"pagination": {
"total": 42,
"page": 1,
"limit": 10,
"totalPages": 5
}
}
{
"account": {
"external_id": "LAB-001",
"gdt_id": "42"
},
"order": {
"external_id": "EXT-ORD-001",
"gdt_id": 12345
},
"patient_data": {
"external_patient_id": "PAT-12345",
"gdt_patient_id": "456",
"patient_name": "John Doe",
"email": "john.doe@example.com",
"phone": "+12125551234",
"address": {
"address": "123 Main Street",
"address_2": "Apt 4B",
"city": "Los Angeles",
"state_region": "CA",
"postal_code": "90001",
"country": "US"
},
"sex_at_birth": "Male",
"gender_identity": "Man",
"dob": "1990-05-15",
"race": "White",
"ethnicity": "Not Hispanic or Latino"
},
"results": [
{
"report": {
"dates": {
"collection_date": "2026-02-10",
"result_date": "2026-02-15"
},
"lab_information": {
"gdt_lab_id": "7",
"lab_name": "Reference Lab Inc.",
"lab_address": "456 Lab Drive, Chicago, IL"
},
"collection_site": {
"gdt_location_id": "12",
"address": {
"address": "789 Collection Ave",
"address_2": "",
"city": "Los Angeles",
"state_region": "CA",
"postal_code": "90001",
"country": "US"
}
},
"pdf_base64": "JVBERi0xLjQK...",
"report_comments": "All results within normal limits.",
"test_comments": "",
"test_type": "Molecular",
"has_positive_or_abnormal_result": 0,
"bundle_info": {
"bundle_name": "STI Full Panel",
"bundle_sku": "BDL-STI-FULL"
},
"data": [
{
"test": {
"test_name": "Chlamydia trachomatis",
"friendly_name": "Chlamydia Test",
"product_description": "NAA test for CT detection",
"test_id": 101,
"lab_report_id": "RPT-2026-001",
"value": "Negative",
"product_sku": "PRD-CT-NAA",
"product_category": "STI",
"positive_or_abnormal_result": 0,
"low_critical": "",
"low_range": "",
"high_range": "",
"high_critical_range": "",
"measurement_units": ""
}
},
{
"test": {
"test_name": "Neisseria gonorrhoeae",
"friendly_name": "Gonorrhea Test",
"product_description": "NAA test for NG detection",
"test_id": 102,
"lab_report_id": "RPT-2026-001",
"value": "Negative",
"product_sku": "PRD-NG-NAA",
"product_category": "STI",
"positive_or_abnormal_result": 0,
"low_critical": "",
"low_range": "",
"high_range": "",
"high_critical_range": "",
"measurement_units": ""
}
}
]
}
}
]
}
| Field | Description |
|---|---|
test_name | Official analyte/test name |
friendly_name | Patient-friendly test name |
product_description | Description of the test |
test_id | Internal test/product ID |
lab_report_id | Lab’s report identifier |
value | The result value (e.g., "Negative", "12.5", "Detected") |
product_sku | Product SKU identifier |
product_category | Category grouping (e.g., "STI", "Wellness") |
positive_or_abnormal_result | 0 = normal, 1 = positive/abnormal |
low_critical | Critical low threshold (gender-sensitive: uses male or female value based on patient’s sex) |
low_range | Normal range lower bound |
high_range | Normal range upper bound |
high_critical_range | Critical high threshold (gender-sensitive) |
measurement_units | Units of measurement (e.g., "mg/dL", "copies/mL") |
Gender-sensitive ranges: The system uses the patient’s
sex_at_birthto select the appropriate critical range. Male patients getlow_critical_male/high_critical_male; female patients (default) getlow_critical_female/high_critical_female. The normal range (low_range/high_range) is the same regardless of gender.
Your account’s support entity settings control what result data you receive:
| Setting: `permit_test_results_type` | Behavior |
|---|---|
1 or 3 | Full analyte-level data[] array included |
| Other | data[] is returned as an empty array |
| Setting: `test_results_delivery_option` | Behavior |
|---|---|
1 or 2 | Lab PDF included |
2 or 3 | GDT-generated PDF included |
GET /api/individual_result_report
Retrieves an individual result report for a specific order/patient combination.
Supports the same dual authentication as test results.
Groups organize users for bulk testing programs (corporate wellness, clinical studies, etc.).
POST /api/group
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
account_id | object | YES | ID object | – |
group_name | string | YES | Alphanumeric, max 30 | – |
external_id | string | No | Max 36, alphanumeric | Your system’s group ID |
logo_url | string | No | Valid URI, extensions: .jpg, .png, .gif, .webp, .heic | Group logo |
ordering_mode | number | No | 1, 2, or 3 | – |
discount_mode | number | No | 1, 2, or 3 | – |
discount_percentage | number | Conditional | 0-100 integer. Only when discount_mode = 2 | – |
promo_code | string | Conditional | Alphanumeric, 2-15 chars. Only when discount_mode = 2 | – |
all_bundles | number | YES | 0 or 1 | 1 = all bundles available, 0 = specify individual bundles |
bundles | array | Conditional | Array of { sku: string }. Required (min 1) when all_bundles = 0 | – |
attached_forms | number[] | YES | Array of positive integers, min 1 | Form IDs to attach |
communication_preferences | number | YES | 0 or 1 | – |
{
"account_id": { "external_id": "LAB-001" },
"group_name": "Corporate Wellness Q1",
"external_id": "GRP-CW-Q1",
"all_bundles": 0,
"bundles": [
{ "sku": "BDL-STI-FULL" },
{ "sku": "BDL-COVID-PCR" }
],
"attached_forms": [1, 5],
"communication_preferences": 1
}
PUT /api/group
Same fields as create but all optional except account_id and group_id:
{
"account_id": { "external_id": "LAB-001" },
"group_id": { "gdt_id": 55 },
"group_name": "Updated Group Name"
}
DELETE /api/group
{
"account_id": { "external_id": "LAB-001" },
"group_id": { "gdt_id": 55 }
}
Performs a soft delete.
POST /api/group/member
| Field | Type | Required | Validation |
|---|---|---|---|
account_id | object | YES | ID object |
group_id | object | YES | ID object |
members | array | YES | Min 1. Each: { external_id?, gdt_id?, email?, name? } |
Each member must have at least one of
external_id,gdt_id, or
If a member doesn’t exist, they are auto-created as a Patient (
user_type: 1).
If a member is already in another group, they are moved to this group.
{
"account_id": { "external_id": "LAB-001" },
"group_id": { "gdt_id": 55 },
"members": [
{ "email": "alice@example.com", "name": "Alice Smith" },
{ "gdt_id": 789 },
{ "external_id": "PAT-999" }
]
}
DELETE /api/group/member
Same structure as add members (without name):
{
"account_id": { "external_id": "LAB-001" },
"group_id": { "gdt_id": 55 },
"members": [
{ "email": "alice@example.com" },
{ "gdt_id": 789 }
]
}
These are endpoints where external systems push data INTO Diagnostic.ly.
If your LIMS is Crelio, these are the webhook endpoints that receive lab events:
| Endpoint | Event | Description |
|---|---|---|
POST /api/crelio/sample_received | Sample Received | Specimen has arrived at the lab |
POST /api/crelio/sample_dismissed | Sample Dismissed | Specimen was rejected by the lab |
POST /api/crelio/test_dismissed | Test Dismissed | A specific test was cancelled |
POST /api/crelio/report_submit | Report Submitted | Structured test results submitted |
POST /api/crelio/report_submit_pdf | PDF Report | PDF result report uploaded |
POST /api/crelio/consolidated_report_submit_pdf | Consolidated PDF | Multi-test consolidated PDF report |
POST /api/crelio/consolidated_final_report_submit | Final Report | Final consolidated structured report |
POST /api/test-results
For labs that aren’t using Crelio, this is the generic endpoint for submitting test results.
POST /api/netsoft/results
For labs submitting test results. This is the most relevant endpoint for a lab sending results back into Diagnostic.ly.
{
"gdt_client_id": "string (REQUIRED)",
"account_id": {
"external_id": "string (optional)",
"gdt_id": "string (REQUIRED)"
},
"order_id": {
"external_order_id": "string (optional)",
"gdt_order_id": "string (REQUIRED)"
},
"patient_info": {
"external_patient_id": "string (optional)",
"gdt_id": "string (REQUIRED)",
"patient_name": "string (optional)",
"patient_address": "string (optional)",
"patient_address2": "string (optional)",
"patient_city": "string (optional)",
"patient_region": "string (optional)",
"patient_postalcode": "string (optional)",
"patient_country": "string (optional)",
"patient_phone": "string (optional)",
"patient_email": "string (optional, valid email)"
},
"ordering_info": {
"filler_order_id": "string (optional)",
"universal_service_identifier": "string (optional)",
"ordering_provider": "string (optional)",
"placer_field_1": "string (optional)",
"specimen_priority": "string (optional)",
"result_status": "string (optional)"
},
"results": {
"report": [
{
"report_id": "string (REQUIRED)",
"dates": {
"result_date": "ISO date string (REQUIRED)"
},
"has_positive_or_abnormal_result": 0,
"has_inconclusive_result": 0,
"has_insufficient_quantity_result": 0,
"has_missing_specimen": 0,
"pdf": {
"pdf_url": "https://yourlab.com/reports/12345.pdf",
"pdf_base64": "JVBERi0xLjQK..."
},
"products": [
{
"product": {
"external_product_sku": "SKU-001",
"result_type": "Qualitative",
"observation_identifier": "CT-NAA",
"result_value": "Negative",
"interpretation": "No CT DNA detected",
"legal": "For clinical use only"
}
}
]
}
]
}
}
| Field | Type | Values | Description |
|---|---|---|---|
has_positive_or_abnormal_result | number | 0 or 1 | Any positive/abnormal result in this report |
has_inconclusive_result | number | 0 or 1 | Any inconclusive result |
has_insufficient_quantity_result | number | 0 or 1 | Insufficient specimen quantity |
has_missing_specimen | number | 0 or 1 | Specimen not received/missing |
You can provide the PDF report in either format (or both):
| Field | Description |
|---|---|
pdf_url | URL to the PDF (Diagnostic.ly will download and store in S3) |
pdf_base64 | Base64-encoded PDF content (Diagnostic.ly will decode and store in S3) |
test_results_reports and test_results_reports_items recordswebhook_logs for audit trail{
"success": true,
"message": "Results processed successfully"
}
{
"success": false,
"message": "Descriptive error message"
}
POST /api/pilotfish
For HL7-based integrations via PilotFish middleware.
POST /api/order/status_update
External systems can push order status changes into Diagnostic.ly.
POST /api/ (root webhook endpoint)
Generic inbound webhook for specimen and shipment data from support entities. Authentication is via api_key header matching or hostname-based matching.
Diagnostic.ly pushes status updates to your system via outbound webhooks.
Your webhook URL and bearer token are configured in the support_entity_credentials table:
outbound_webhook_url – Your endpoint URLoutbound_webhook_key – Bearer token included in the Authorization headerwebhook_logs{
"order_id": 12345,
"external_account_id": "LAB-001",
"account_id": {
"gdt_external_id": "LAB-001",
"gdt_id": 42
},
"order_status": "Arrived at Lab",
"order_type": "Shipment",
"recipient_info": {
"name": "John Doe",
"email": "johndoe@example.com",
"phone": "+13105559876",
"address": {
"address": "123 Main Street",
"address2": "Apt 4B",
"city": "Los Angeles",
"state": "CA",
"postal": "90001",
"country": "US"
}
},
"preassign_recipient_flag": 1,
"outbound_shipment": {
"carrier_code": "usps",
"service_level": "priority",
"tracking_id": "9400111899223100001234",
"shipment_date": "2026-02-10",
"arrival_date": "2026-02-13",
"tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100001234"
},
"inbound_shipment": {
"carrier_code": "usps",
"service_level": "priority",
"tracking_id": "9400111899223100005678",
"shipment_date": "2026-02-14",
"arrival_date": "",
"tracking_url": ""
},
"bundles": [
{
"internal_sku": "BDL-STI-FULL",
"external_sku": "VENDOR-STI-001",
"bundle_name": "STI Full Panel",
"collection_date": null,
"kit_id": "KIT-ABC-123",
"products": [
{
"product_name": "Chlamydia/Gonorrhea Test",
"internal_sku": "PRD-CT-NG",
"external_sku": "VENDOR-PRD-001",
"kit_id": "KIT-ABC-123",
"collection_date": null
}
]
}
]
}
In addition to order status webhooks, Diagnostic.ly also pushes test results to your webhook endpoint when new results are available.
enable_webhook_notification = 1{
"gdt_client_id": "string",
"gdt_client_name": "string",
"account": {
"external_id": "LAB-001",
"gdt_id": "42"
},
"order": {
"external_id": "EXT-ORD-001",
"gdt_id": "12345"
},
"patient_data": {
"patient_id": "456",
"external_patient_id": "PAT-12345",
"gdt_patient_id": "456",
"patient_name": "John Doe",
"email": "john.doe@example.com",
"phone": "+12125551234",
"address": {
"address": "123 Main Street",
"address_2": "Apt 4B",
"city": "Los Angeles",
"state_region": "CA",
"postal_code": "90001",
"country": "US"
},
"sex_at_birth": "Male",
"gender_identity": "Man",
"dob": "1990-05-15",
"race": "White",
"ethnicity": "Not Hispanic or Latino"
},
"results": [
{
"report": {
"dates": {
"collection_date": "2026-02-10",
"result_date": "2026-02-15"
},
"lab_information": {
"gdt_lab_id": "7",
"lab_name": "Reference Lab",
"lab_address": "456 Lab Drive"
},
"collection_site": {
"gdt_location_id": "12",
"address": { "..." }
},
"pdf_url": "https://s3.amazonaws.com/.../lab_report.pdf",
"pdf_base64": "JVBERi0xLjQK...",
"gdt_pdf_url": "https://s3.amazonaws.com/.../gdt_report.pdf",
"report_comments": "All within normal limits",
"test_comments": "",
"test_type": "Molecular",
"has_positive_or_abnormal_result": 0,
"bundle_info": {
"bundle_name": "STI Full Panel",
"bundle_sku": "BDL-STI-FULL"
},
"data": [
{
"test": {
"test_name": "Chlamydia trachomatis",
"friendly_test_name": "Chlamydia",
"product_description": "NAA test",
"product_sku": "PRD-CT-NAA",
"product_category": "STI",
"test_id": 101,
"lab_report_id": "RPT-001",
"value": "Negative",
"positive_or_abnormal_result": 0,
"low_critical": "",
"low_range": "",
"high_range": "",
"high_critical_range": "",
"measurement_units": ""
}
}
]
}
}
]
}
| Status | Meaning |
|---|---|
200 | All results sent successfully (or none to send) |
207 | Partial success – some results sent, some failed |
500 | All sends failed |
On any failure, an alert email is sent to the engineering team.
outbound_webhook_urlAuthorization: Bearer headerPOST /your-webhook-endpoint HTTP/1.1
Authorization: Bearer wh_key_xyz789...
Content-Type: application/json
POST /api/address-verify
Note: This endpoint does NOT require API key authentication.
Uses PostGrid for address verification.
| Field | Type | Required | Validation |
|---|---|---|---|
request_id | string | YES | Your tracking ID |
recipient_name | string | YES | – |
address_1 | string | YES | – |
address_2 | string | No | Allows empty/null |
city | string | YES | – |
region | string | YES | State/province |
urbanization | string | No | Allows empty/null |
postal_code | string | No | Allows empty/null |
country_code | string | YES | Exactly 2 chars (ISO-2) |
{
"request_id": "REQ-001",
"recipient_name": "John Doe",
"address_1": "123 Main St",
"address_2": "Apt 4B",
"city": "New York",
"region": "NY",
"postal_code": "10001",
"country_code": "US"
}
{
"message": "Address verified successfully.",
"verifiedAddress": { ... }
}
{
"message": "Address could not be verified. Suggested alternatives are provided.",
"alternatives": [ ... ]
}
POST /api/address-verify/suggest
{
"address": {
"line1": "123 Main St",
"line2": "",
"city": "New York",
"postalOrZip": "10001",
"provinceOrState": "NY",
"country": "US"
}
}
+------------------+
| Pending Approval | (if approval workflow enabled)
+--------+---------+
|
+--------v---------+
+-------> Processing <-------+
| +--------+---------+ |
| | |
| +--------v---------+ |
| | In Transit | |
| +--------+---------+ |
| | |
| +--------v---------+ |
| | Arrived | | Reshipment
| +--------+---------+ | Processing
| | | -> Reshipment
| +--------v---------+ | Arrived
| | Awaiting Return | |
| | Shipment | |
| +--------+---------+ |
| | |
| +--------v---------+ |
| | In Transit to Lab|------+
| +--------+---------+
| |
| +--------v---------+
| | Arrived at Lab |
| +--------+---------+
| |
| +--------v---------+
| |Completed Shipped |
| +------------------+
|
| (Exception paths)
|
+--- Delayed
+--- Delayed Beyond Max Days
+--- Exception
+--- Canceled
+--- Returned
+--- Refunded
| Status | Description |
|---|---|
Processing | Order created, awaiting fulfillment |
Pending Approval | Awaiting provider approval (if approval workflow enabled) |
Approved | Order approved by provider |
declined | Order denied by provider |
Snoozed | Order approval deferred |
Pending Shipment | Queued for shipment |
In Transit | Kit shipped, en route to patient |
Arrived | Kit delivered to patient |
Awaiting Return Shipment | Waiting for patient to return specimen |
In Transit to Lab | Specimen shipped, en route to lab |
Arrived at Lab | Specimen received at lab |
Completed Shipped | Results complete, order fulfilled |
Delayed | Shipment delayed |
Delayed Beyond Max Days | Shipment delayed past maximum SLA |
Exception | Order has an exception/issue |
Canceled | Order cancelled |
Returned | Kit returned to sender |
Refunded | Order refunded |
Reshipment Processing | Replacement kit being prepared |
Reshipment Arrived | Replacement kit delivered |
When approval workflows are enabled, individual bundles within an order can have these statuses:
| Status | Description |
|---|---|
approved | Bundle approved by provider |
denied | Bundle denied by provider |
not_submitted | Bundle not yet submitted for approval |
snoozed | Bundle approval deferred |
order_type| Value | Name | Description |
|---|---|---|
1 | Shipment | Kit shipped to patient |
2 | Hand to Patient | Kit given directly |
3 | Registration | No physical kit |
flavor_id| Value | Name |
|---|---|
1 | TEST_ONLY |
2 | TEST_AND_EDUCATION |
3 | EDUCATION_ONLY |
user_type| Value | Role |
|---|---|
1 | Patient |
2 | Account Admin |
3 | Observer |
5 | Manager |
7 | Provider |
8 | Medical Staff |
10 | Support Entity Manager |
payment_responsibility| Value | Description |
|---|---|
"ordering_entity" | Lab pays (default) |
"patient" | Patient pays |
"shared" | Split between lab and patient |
null | No payment info |
gender (Order ship_to)| Value |
|---|
"Male" |
"Female" |
"Others" |
Case-insensitive for orders. For users, only
"Male"and"Female"are accepted.
preassign_recipient_as_assignee| Value | Description |
|---|---|
"yes" | Auto-assign the recipient as the test taker (default) |
"no" | Do not auto-assign |
shipment_handled_externally| Value | Description |
|---|---|
"Y" / "Yes" / "yes" | You handle shipping (must provide carrier_id + tracking_id) |
"N" / "No" / "no" | Diagnostic.ly handles shipping |
submit_to_insurance| Value |
|---|
"Yes" |
"No" |
timezone_support| Value | Description |
|---|---|
1 | Timezone support enabled |
2 | Timezone support disabled |
communication_preferences| Value | Description |
|---|---|
0 | Opted out |
1 | Opted in |
| ID | Name | Description |
|---|---|---|
1 | CUSTOMER | Outbound shipment (to patient) |
2 | RETURN | Inbound shipment (specimen to lab) |
| Pattern | Regex | Use | |||||
|---|---|---|---|---|---|---|---|
| Phone (with country code) | ^\+\d{10,15}$ | All phone fields in orders | |||||
| Phone (alternate) | ^\+\d{1,3}\d{8,10}$ | Some phone fields in users/fax | |||||
| Alphanumeric | ^[a-zA-Z0-9\s]+$ | external_id, names, etc. | |||||
| Date filter | ^\d{4}-\d{2}-\d{2}$ | Date query parameters | |||||
| Date-time filter | `^\d{4}-(0[1-9]\ | 1[0-2])-(0[1-9]\ | [12]\d\ | 3[01]) (0\d\ | 1\d\ | 2[0-3]):([0-5]\d):([0-5]\d)$` | DateTime query parameters |
All errors follow this structure:
{
"type": "ClientError",
"statusCode": 400,
"message": "Descriptive error message"
}
| Type | Default Status | Description |
|---|---|---|
ClientError | 400 | Bad request, validation failure, auth failure |
ServerError | 500 | Internal server error |
ShipheroError | 400 | ShipHero fulfillment integration error |
ShipengineError | 400 | ShipEngine shipping integration error |
InterfaxError | 400 | InterFax faxing integration error |
| Scenario | Status | Message |
|---|---|---|
| Missing required field | 400 | "The field {field_name} is required" |
| Empty required field | 400 | "The field {field_name} cannot be empty" |
| Invalid field type | 400 | "Invalid characters ({value}) passed into {field_name}..." |
| Field too long | 400 | "The field length for {field_name} is {limit} but your data was {length} characters..." |
| Invalid email | 400 | "Invalid characters ({value}) passed into {field_name}..." |
| Missing API key | 400 | "API key is required" |
| Invalid API key | 400 | "Invalid API key" |
| Missing account ID | 400 | "Account id is required" |
| Account not found | 400 | "Account (X) not found or not linked with any support entity." |
| Inactive account | 400 | "Account (X) is not active." |
| API method not permitted | 400 | "Invalid api method" |
| Bundle not found | 400 | "Bundle with SKU {sku} not found" |
| Bundle not active | 400 | "Bundle {sku} is not active" |
| Geographical exclusion | 400 | Bundle/product excluded for recipient’s region |
| Duplicate user | 400 | User with same email already exists in account |
| Invalid JSON body | 400 | "Invalid data passed in request body. Valid data format: JSON" |
| Internal error | 500 | "Internal server error" |
Joi validation returns only the first error encountered. The message is extracted from the first error detail:
{
"type": "ClientError",
"statusCode": 400,
"message": "The field ship_to.email is required"
}
Here is the complete workflow for a lab integrating with Diagnostic.ly:
POST /api/user HTTP/1.1
Host: api.diagnostic.ly
Content-Type: application/json
api-key: sk_live_abc123
{
"account_id": { "external_id": "LAB-001" },
"external_id": "PAT-12345",
"name": "John Doe",
"email": "john.doe@example.com",
"active": true,
"country_code": "US",
"phone_number": "+12125551234",
"timezone_support": 1,
"user_type": 1,
"dob": "1990-05-15",
"gender": "Male"
}
Response: { "data": { "gdt_id": 456 } }
POST /api/address-verify HTTP/1.1
Host: api.diagnostic.ly
Content-Type: application/json
{
"request_id": "ADDR-001",
"recipient_name": "John Doe",
"address_1": "123 Main St",
"city": "Los Angeles",
"region": "CA",
"postal_code": "90001",
"country_code": "US"
}
POST /api/order HTTP/1.1
Host: api.diagnostic.ly
Content-Type: application/json
api-key: sk_live_abc123
{
"account_id": { "external_id": "LAB-001" },
"order_type": 1,
"bundles": [
{
"sku": "BDL-STI-FULL",
"quantity": 1,
"flavor_id": 1
}
],
"ordering_entity_info": {
"full_name": "Dr. Jane Smith",
"ordered_by_id": "1234567890",
"ordered_by_email": "drsmith@lab.com"
},
"ship_to": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+12125551234",
"dob": "1990-05-15",
"gender": "Male",
"address": {
"street_1": "123 Main St",
"city": "Los Angeles",
"region": "CA",
"postal_code": "90001",
"country": "US"
}
}
}
Response: { "data": { "order": { "order_Id": 12345 }, "status": "Processing" } }
Option A: Poll the API
GET /api/order/12345?account_id=LAB-001 HTTP/1.1
Host: api.diagnostic.ly
api-key: sk_live_abc123
Option B: Receive Outbound Webhooks (recommended)
Your endpoint receives status updates automatically:
// Your server receives:
{
"order_id": 12345,
"order_status": "In Transit",
"outbound_shipment": {
"tracking_id": "9400111899223100001234",
"tracking_url": "https://tools.usps.com/..."
},
...
}
If your lab processes the specimens and needs to push results back:
Via NetSoft Webhook:
POST /api/netsoft/results HTTP/1.1
Host: api.diagnostic.ly
Content-Type: application/json
{
"order_info": {
"order_id": { "gdt_order_id": 12345 },
"account_id": { "gdt_account_id": 42 }
},
"patient_info": {
"first_name": "John",
"last_name": "Doe",
"dob": "1990-05-15"
},
"ordering_entity_info": {
"provider_name": "Dr. Jane Smith",
"provider_npi": "1234567890"
},
"report": {
"results": [
{
"test_name": "Chlamydia trachomatis",
"result_value": "Negative",
"reference_range": "Negative",
"units": "",
"flag": "Normal"
}
],
"pdf": {
"pdf_url": "https://yourlab.com/reports/12345.pdf"
}
}
}
GET /api/test-results?account_id=LAB-001&order_id=12345&auth_token=sk_live_abc123 HTTP/1.1
Host: api.diagnostic.ly
api-key: YOUR_API_KEY
Content-Type: application/json
| Action | Method | Endpoint |
|---|---|---|
| Create User | POST | /api/user |
| Update User | PUT | /api/user/:id |
| List Users | GET | /api/user |
| Delete User | DELETE | /api/user |
| Create Order | POST | /api/order |
| Update Order | PUT | /api/order |
| List Orders | GET | /api/order |
| Get Order | GET | /api/order/:order_id |
| Get Results | GET | /api/test-results |
| Verify Address | POST | /api/address-verify |
| Create Group | POST | /api/group |
| Add Members | POST | /api/group/member |
https://api.diagnostic.ly/docshttps://api.diagnostic.ly/internal/docsGET /health HTTP/1.1
Host: api.diagnostic.ly
This document was generated from the diagnostic.ly-nodejs source code. For the latest updates, refer to the Swagger documentation at /docs.