RPh Labs · Partner API v1
Order RPh Labs tests for your customers, and follow each one to results.
The Partner API lets an approved business read our live test catalog, place orders on behalf of its customers, and track every order through physician review, kit shipping, sample receipt and lab results. Every request needs an API key that RPh Labs issues to you.
https://rphlabs.com/wp-json/rph-labs/partner/v1
How payment works
Your customer pays RPh Labs directly. When you create an order it starts in awaiting_payment and the response includes a secure payment link. Send that link to your customer, or ask us to email it for you. Once the order is paid it runs through our standard pipeline: physician review by our medical partner, kit shipping, sample testing and results. No card data ever passes through the API.
After payment we create an RPh Labs account for the patient (if they do not already have one) and email them a link to set a password. Their results appear under My Account › My Reports.
Quick start
- Get your API key. RPh Labs creates your partner profile and sends you a key that starts with
rphk_. See Authentication. - Check the key with
GET /ping. - Load the catalog with
GET /testsand keep each test'sid. - Create an order with
POST /orders, passing your ownexternal_id. - Send the customer the
payment.urlfrom the response. - Follow progress by checking
GET /orders?updated_after=…every few minutes. See Tracking order changes.
curl https://rphlabs.com/wp-json/rph-labs/partner/v1/ping \
-H "X-RPH-Api-Key: $RPH_API_KEY"
const res = await fetch("https://rphlabs.com/wp-json/rph-labs/partner/v1/ping", {
headers: { "X-RPH-Api-Key": process.env.RPH_API_KEY },
});
console.log(await res.json());
import os, requests
r = requests.get(
"https://rphlabs.com/wp-json/rph-labs/partner/v1/ping",
headers={"X-RPH-Api-Key": os.environ["RPH_API_KEY"]},
timeout=30,
)
print(r.json())
$ch = curl_init('https://rphlabs.com/wp-json/rph-labs/partner/v1/ping');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['X-RPH-Api-Key: ' . getenv('RPH_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
Authentication
Every endpoint requires an API key. Keys are created by RPh Labs staff and tied to your partner profile. There is no self-serve signup and no way to call the API without a key.
Key format
A key looks like rphk_3f9a1c07e2_9b4e…: the prefix rphk_, your 10-character partner ID, an underscore, and 48 random hex characters. Anything else is rejected before it is checked.
Sending the key
Send it in the X-RPH-Api-Key header, or as a bearer token. Both work the same way. Requests must use HTTPS. Never send a key to an http:// address, even once: the key leaves your server before any redirect can protect it.
X-RPH-Api-Key: rphk_3f9a1c07e2_9b4e…
# or
Authorization: Bearer rphk_3f9a1c07e2_9b4e…
How keys are handled
- Shown once. We see your key only at the moment it is created. RPh Labs stores a SHA-256 hash, never the key itself, so we cannot look it up for you later.
- One active key per partner. If a key is lost or exposed, ask us for a new one. The old key stops working the moment the new one is issued.
- Revocable. RPh Labs can disable your profile at any time. A disabled profile's key is refused.
- Server-side only. Keep the key on your servers. Never put it in a browser, a mobile app or a public repository.
- Valid for one year. A key expires 365 days after it is issued. Every response carries an
X-RPH-Key-Expiresheader andGET /pingreturnskey_expires_at; ask us for a new key before that date. An expired key returns401 key_expired. - Headers only, never the URL. A request with the key anywhere in the query string is refused with
400 key_in_urland counted as a failed attempt, because URLs end up in proxy and server logs. If a key has been sent that way, ask us to rotate it.
Requesting access. Send RPh Labs your company name, a technical contact email, and the public IP addresses of the servers that will call the API. If you need result PDFs through the API, we will also ask for a signed Business Associate Agreement. Your key is delivered through a secure channel, never by ordinary email.
Access controls
- Server-to-server only. Requests that carry an
Originheader (which every browser sends on cross-site calls) are refused, and the API sends no CORS headers. - IP allowlist. Give us the public IP addresses or CIDR ranges of the servers that will call the API. Once they are on file, your key is refused from any other address.
- Lockout. An IP address that sends 20 missing or invalid keys is blocked for 15 minutes after its last failure.
- Audit trail. Every request is logged with the time, your partner ID, the calling IP, the endpoint, the order it touched and the result. Request bodies and patient details are never written to the log.
- Request size. Bodies over 32 KB are refused with
413 payload_too_large. A complete order is under 2 KB. - No discovery. The REST index and route listings on rphlabs.com are closed to anonymous callers. Only the endpoints documented here answer to your key; anything else returns
401.
Rate limit
Each partner can make 120 requests per clock minute. Request number 121 in the same minute returns 429 rate_limited. Wait for the next minute and retry, ideally with backoff. Checking for changes every few minutes uses a tiny fraction of this.
Conventions
| Topic | Rule |
|---|---|
| Format | Requests and responses are JSON (UTF-8). Send Content-Type: application/json on POST. |
| Dates | ISO 8601 in UTC, for example 2026-10-16T14:05:00Z. Dates of birth are YYYY-MM-DD. |
| Money | Decimal strings with two places, such as "299.00", plus a currency code (USD). |
| IDs | Tests and orders use RPh Labs numeric IDs. Your own reference for an order is its external_id. |
| Pagination | page and per_page parameters; totals in the body and in X-WP-Total / X-WP-TotalPages headers. |
| Scope | You only ever see orders created with your own key. Another partner's order ID returns 404. |
| Coverage | US addresses only. Kits cannot ship to CA, MD, NY, PA or RI. |
Errors
Errors return a non-2xx status and a JSON body with a stable code you can branch on, a readable message, and data.status. Validation errors add data.fields, keyed by field path.
{
"code": "invalid_order",
"message": "Some fields are missing or invalid.",
"data": {
"status": 422,
"fields": {
"patient.dob": "The patient must be 18 or older.",
"shipping_address.state": "Kits cannot ship to NY. Not available in: NY, MD, PA, RI, CA."
}
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not a JSON object, or the content type is wrong. |
| 401 | missing_api_key | No key was sent, or it is not in the rphk_ format. |
| 401 | invalid_api_key | The key is unknown, was replaced, or the partner is disabled. |
| 403 | baa_required | Result PDFs need a Business Associate Agreement on file. |
| 404 | test_not_found | No test with that ID is available to partners. |
| 404 | order_not_found | No order with that ID belongs to your account. |
| 404 | result_not_found | No ready result with that ID is on this order. |
| 409 | not_eligible | The patient cannot order this test right now (limit one per person per year). |
| 409 | cannot_cancel | Only unpaid orders can be cancelled through the API. |
| 422 | invalid_order | One or more fields failed validation. See data.fields. |
| 403 | https_required | The request did not use HTTPS. |
| 403 | browser_not_allowed | The request carried an Origin header, so it came from a browser. Call the API from your server. |
| 403 | ip_not_allowed | The key is valid but your IP address is not on your allowlist. |
| 429 | rate_limited | More than 120 requests in the current minute. |
| 429 | too_many_failures | Your IP address sent 20 bad or missing keys in a row. It is blocked for 15 minutes after the last failure. |
| 400 | key_in_url | The API key was sent in the URL. Send it in a header, and ask us to rotate it. |
| 401 | key_expired | The key is more than a year old. Ask RPh Labs for a new one. |
| 413 | payload_too_large | The request body is over 32 KB. |
| 500 | order_failed | The order could not be created. Retrying with the same external_id is safe. |
Order lifecycle
Each order has one partner-facing status. A typical order moves through these in order:
- awaiting_paymentCreated. Waiting for the customer to use the payment link.
- paidPaid. Physician review starts once the patient finishes the intake.
- kit_shippedThe collection kit has a USPS label and tracking number.
- sample_receivedThe returned sample arrived and is being tested.
- results_readyLab results are uploaded and visible to the patient.
| Other statuses | Meaning |
|---|---|
payment_failed | A payment attempt failed. The same payment link can be used again. |
on_hold | Paused by RPh Labs staff, for example while a question is resolved. |
completed | Closed by RPh Labs without lab results attached to it. |
cancelled | Cancelled by you, by RPh Labs, or automatically after 30 days unpaid. |
refunded | Refunded by RPh Labs. |
Physician review
Every test requires review by a licensed physician through our medical partner. After paying, the patient completes a short medical intake. Until they do, physician_review.status is awaiting_patient_intake and physician_review.intake_url holds the link to send them. Answers you pass in intake when creating the order are pre-filled for the patient.
| physician_review.status | Meaning |
|---|---|
not_started | The order is not paid yet. |
awaiting_patient_intake | Paid, and waiting for the patient to finish the intake at intake_url. |
in_review | The intake is in and a physician is reviewing it. |
approved | The physician approved the order. |
cancelled | The review case was cancelled. |
Check your key
GET/ping
Confirms the key works and shows what your profile is enabled for. Use it as a health check.
{
"ok": true,
"partner": "Acme Wellness",
"results_pdfs": false,
"key_issued_at": "2026-10-16T14:05:00Z",
"key_expires_at": "2027-10-16T14:05:00Z",
"time": "2026-10-18T09:30:00Z"
}
| Field | Description |
|---|---|
partner | Your partner name as RPh Labs has it on file. |
results_pdfs | true when a BAA is on file and result PDFs are available to you. |
key_issued_at, key_expires_at | When your current key was issued and when it stops working (one year later). Alert yourselves a month ahead and ask for a new key. The same expiry is in the X-RPH-Key-Expires header on every response. |
time | Server time. Use it to set updated_after from our clock instead of yours. |
List tests
GET/tests
Returns every test you can order, live from our catalog, in the same order as our shop. Prices are the current customer prices. Cache the list and refresh it with updated_after.
Query parameters
| Parameter | Description |
|---|---|
updated_after optional | ISO 8601 date. Only tests changed after this time are returned. |
{
"tests": [
{
"id": 71,
"sku": "RPH-PGX-299",
"name": "CLIA-Accredited Pharmacogenetic Testing Kit",
"slug": "pharmacogenetic-testing-kit",
"url": "https://rphlabs.com/pharmacogenetic-testing-kit/",
"price": "299.00",
"regular_price": "299.00",
"sale_price": null,
"currency": "USD",
"in_stock": true,
"short_description": "At-home pharmacogenetic testing kit: one cheek swab shows how your genes are likely to affect 240+ medications.",
"description": "…",
"images": ["https://rphlabs.com/wp-content/uploads/2026/09/kit-box-1.webp"],
"categories": ["CGx & PGX"],
"requires_physician_review": true,
"limit_per_person": { "quantity": 1, "days": 365 },
"updated_at": "2026-10-12T09:30:00Z"
}
],
"count": 1
}
Test fields
| Field | Description |
|---|---|
id | Pass this as test_id when creating an order. |
sku | Our product code, for your records. Orders use id. |
price | What the customer pays now. sale_price is null when nothing is on sale. |
short_description, description | Plain text with HTML removed, ready to show in your own UI. |
images | Full-size image URLs, main image first. |
requires_physician_review | Always true: every test is reviewed by a physician. |
limit_per_person | For test kits, one per person every 365 days. null when there is no limit. |
Get a test
GET/tests/{id}
Returns one test in the same shape as an item in tests. Returns 404 test_not_found if the test is unpublished, hidden or has no price.
Create an order
POST/orders
Creates an order for one test for one patient. It starts in awaiting_payment; the response's payment.url is the link your customer uses to pay. Returns 201 Created.
Safe to retry. external_id is your own unique reference. If you send an external_id you have already used, no new order is created: you get the existing order back with 200 OK and the header X-RPH-Idempotent-Replay: true. The rest of the body is ignored on a replay, so use a new external_id for a genuinely new order.
Body
| Field | Rules |
|---|---|
external_id required | Your reference for this order. Up to 64 characters, unique within your account. |
test_id required | A test id from GET /tests. |
patient.first_name required | Up to 60 characters. |
patient.last_name required | Up to 60 characters. |
patient.email required | A valid address. The payment link, account invite and kit updates go here. |
patient.phone required | A 10-digit US number. Punctuation and a leading 1 are removed. |
patient.dob required | YYYY-MM-DD. The patient must be 18 or older. |
patient.sex required | female or male (sex assigned at birth). Used by the physician intake. |
shipping_address required | Where the kit goes. All six fields are required: address_1, address_2 (send "" if there is no apartment or suite), city, state (2-letter code), postcode (5-digit ZIP or ZIP+4) and country ("US"). Not CA, MD, NY, PA or RI. |
billing_address required | The patient's billing address, with the same six fields. Send the shipping address again if they are the same. |
intake.current_medications required | Medications the patient takes now. An array of strings, or one newline-separated string, with at least one item and up to 50. Send ["None"] if there are none. Pre-fills the physician intake. |
intake.considering_medications required | Medications the patient is considering. Same format, ["None"] if none. |
intake.allergies required | Known allergies. Same format, ["None"] if none. |
intake.conditions required | Medical conditions. Same format, ["None"] if none. |
notes required | A string up to 1,000 characters, saved as a note on the order for RPh Labs staff. Send "" if you have nothing to add. |
send_payment_email required | A JSON boolean. true to have RPh Labs email the payment link to the patient; false if you will send payment.url yourself. Strings such as "yes" are rejected. |
curl -X POST https://rphlabs.com/wp-json/rph-labs/partner/v1/orders \
-H "X-RPH-Api-Key: $RPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "ACME-100245",
"test_id": 71,
"patient": {
"first_name": "Dana",
"last_name": "Rivera",
"email": "dana.rivera@example.com",
"phone": "(305) 555-0142",
"dob": "1984-03-09",
"sex": "female"
},
"shipping_address": {
"address_1": "1200 Brickell Ave",
"address_2": "Apt 1504",
"city": "Miami",
"state": "FL",
"postcode": "33131",
"country": "US"
},
"billing_address": {
"address_1": "1200 Brickell Ave",
"address_2": "Apt 1504",
"city": "Miami",
"state": "FL",
"postcode": "33131",
"country": "US"
},
"intake": {
"current_medications": ["Sertraline 50 mg", "Atorvastatin 20 mg"],
"considering_medications": ["Bupropion"],
"allergies": ["Penicillin"],
"conditions": ["None"]
},
"notes": "",
"send_payment_email": true
}'
const address = {
address_1: "1200 Brickell Ave", address_2: "Apt 1504",
city: "Miami", state: "FL", postcode: "33131", country: "US",
};
const res = await fetch("https://rphlabs.com/wp-json/rph-labs/partner/v1/orders", {
method: "POST",
headers: {
"X-RPH-Api-Key": process.env.RPH_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
external_id: "ACME-100245",
notes: "",
test_id: 71,
patient: {
first_name: "Dana", last_name: "Rivera",
email: "dana.rivera@example.com", phone: "3055550142",
dob: "1984-03-09", sex: "female",
},
shipping_address: address,
billing_address: address,
intake: {
current_medications: ["Sertraline 50 mg"],
considering_medications: ["Bupropion"],
allergies: ["Penicillin"],
conditions: ["None"],
},
send_payment_email: true,
}),
});
const order = await res.json();
if (!res.ok) throw new Error(order.message);
console.log(order.payment.url);
import os, requests
address = {
"address_1": "1200 Brickell Ave", "address_2": "",
"city": "Miami", "state": "FL", "postcode": "33131", "country": "US",
}
r = requests.post(
"https://rphlabs.com/wp-json/rph-labs/partner/v1/orders",
headers={"X-RPH-Api-Key": os.environ["RPH_API_KEY"]},
json={
"external_id": "ACME-100245",
"test_id": 71,
"patient": {
"first_name": "Dana", "last_name": "Rivera",
"email": "dana.rivera@example.com", "phone": "3055550142",
"dob": "1984-03-09", "sex": "female",
},
"shipping_address": address,
"billing_address": address,
"notes": "",
"intake": {
"current_medications": ["Sertraline 50 mg"],
"considering_medications": ["Bupropion"],
"allergies": ["Penicillin"],
"conditions": ["None"],
},
"send_payment_email": True,
},
timeout=30,
)
r.raise_for_status()
print(r.json()["payment"]["url"])
$address = [
'address_1' => '1200 Brickell Ave', 'address_2' => '',
'city' => 'Miami', 'state' => 'FL', 'postcode' => '33131', 'country' => 'US',
];
$body = [
'external_id' => 'ACME-100245',
'test_id' => 71,
'patient' => [
'first_name' => 'Dana', 'last_name' => 'Rivera',
'email' => 'dana.rivera@example.com', 'phone' => '3055550142',
'dob' => '1984-03-09', 'sex' => 'female',
],
'shipping_address' => $address,
'billing_address' => $address,
'notes' => '',
'intake' => [
'current_medications' => ['Sertraline 50 mg'],
'considering_medications' => ['Bupropion'],
'allergies' => ['Penicillin'],
'conditions' => ['None'],
],
'send_payment_email' => true,
];
$ch = curl_init('https://rphlabs.com/wp-json/rph-labs/partner/v1/orders');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [
'X-RPH-Api-Key: ' . getenv('RPH_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
]);
$order = json_decode(curl_exec($ch), true);
The response is the full order object.
Order-specific errors
422 invalid_order: a field failed validation;data.fieldslists each one.409 not_eligible: this patient email already ordered this kit within the last year.400 invalid_json: the body is missing or is not a JSON object.
List orders
GET/orders
Your orders, newest first. To pick up changes, pass updated_after set to the time of your last check. See Tracking order changes.
Query parameters
| Parameter | Description |
|---|---|
page optional | Page number, from 1. Default 1. |
per_page optional | 1 to 100. Default 20. |
external_id optional | Return only the order with this reference of yours. |
updated_after optional | ISO 8601 date. Only orders changed after this time. |
{
"orders": [ /* order objects */ ],
"page": 1,
"per_page": 20,
"total": 57,
"total_pages": 3
}
Get an order
GET/orders/{id}
Returns one order object. Use it to refresh a single order, for example right after a customer tells you they have paid.
Cancel an order
POST/orders/{id}/cancel
Cancels an order that is still awaiting_payment or payment_failed, so its payment link stops working. No body is needed. Returns the updated order. A paid order returns 409 cannot_cancel; contact RPh Labs to cancel or refund it.
Download a result PDF
GET/orders/{id}/results/{rid}
BAA partners only. Results are protected health information. This endpoint, and the results list on the order, are only available once a signed Business Associate Agreement is on file. Without one it returns 403 baa_required.
Returns the PDF itself (Content-Type: application/pdf) as an attachment. Take {rid} from results[].id on the order. You can also follow results[].download_url directly with your key. Every download is recorded on the order.
curl https://rphlabs.com/wp-json/rph-labs/partner/v1/orders/10452/results/11873 \
-H "X-RPH-Api-Key: $RPH_API_KEY" \
-o RPH-10452-results.pdf
The order object
Returned by every order endpoint. It contains the patient's personal details, so store and log it with the same care as any patient record.
{
"id": 10452,
"number": "10452",
"external_id": "ACME-100245",
"status": "kit_shipped",
"created_at": "2026-10-16T14:05:00Z",
"updated_at": "2026-10-17T16:40:12Z",
"paid_at": "2026-10-16T15:22:48Z",
"payment": {
"required": false,
"url": null,
"total": "299.00",
"currency": "USD"
},
"items": [
{ "test_id": 71, "sku": "RPH-PGX-299", "name": "CLIA-Accredited Pharmacogenetic Testing Kit", "quantity": 1, "total": "299.00" }
],
"patient": {
"first_name": "Dana", "last_name": "Rivera",
"email": "dana.rivera@example.com", "phone": "3055550142", "dob": "1984-03-09"
},
"shipping_address": {
"address_1": "1200 Brickell Ave", "address_2": "Apt 1504",
"city": "Miami", "state": "FL", "postcode": "33131", "country": "US"
},
"physician_review": {
"status": "approved",
"intake_completed": true,
"intake_url": null
},
"shipments": [
{
"kind": "outbound", "carrier": "USPS", "service": "Ground Advantage",
"tracking_number": "9400111899223197428490",
"tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490",
"created_at": "2026-10-17T16:40:00Z"
}
],
"sample": { "received": false, "received_at": null, "return_tracking_status": null },
"results_available": 0,
"results": null
}
| Field | Description |
|---|---|
id | RPh Labs order ID, used in every order URL. |
number | The order number shown to the patient and to RPh Labs support. |
status | One of the lifecycle statuses. |
paid_at | null until the customer pays. |
payment.required | true while the order is unpaid; payment.url is then the link to pay, otherwise null. |
physician_review | Review state, whether the intake is done, and intake_url while the paid patient still needs to complete it. |
shipments[] | USPS labels. kind is outbound (kit to the patient) or return (prepaid return of the sample). |
sample | Whether the lab has the sample, when it arrived, and the latest return-tracking status. |
results_available | Number of lab result reports ready. Shown to every partner. |
results | null unless a BAA is on file. Then a list of { id, type, name, uploaded_at, download_url }, where type is lab_results or prescription. |
Tracking order changes
The Partner API has no webhooks. You follow your orders by asking for the ones that changed since you last checked: call GET /orders with updated_after set to the time of your previous check. Every order you get back is complete, so there is nothing else to fetch.
What moves an order's updated_at
Any change you can see in the order object:
| Change | Where it shows |
|---|---|
| Payment, cancellation, refund or hold | status, paid_at, payment |
| Physician review started, approved or cancelled | physician_review.status |
| Kit or return label created | shipments[], and status becomes kit_shipped |
| Sample arrived at the lab | sample.received, and status becomes sample_received |
| Lab results uploaded | results_available, results[] (BAA partners), and status becomes results_ready |
A reliable sync loop
- Every 5 to 15 minutes, call
GET /orders?updated_after=<last_sync>&per_page=100. - Read every page: keep going while
pageis less thantotal_pages. - For each order, compare it with your copy by
id(or yourexternal_id) and act on what changed. - Save the time you started this run as the next
last_sync, minus one minute of overlap so nothing saved during the run is missed. Overlap only means you see a few orders twice.
Use the time value from GET /ping if your server clock may drift. A poll every five minutes is about 0.01% of your rate limit.
curl -G https://rphlabs.com/wp-json/rph-labs/partner/v1/orders \
-H "X-RPH-Api-Key: $RPH_API_KEY" \
--data-urlencode "updated_after=2026-10-16T14:00:00Z" \
--data-urlencode "per_page=100" \
--data-urlencode "page=1"
const BASE = "https://rphlabs.com/wp-json/rph-labs/partner/v1";
// Returns every order changed since `since` (an ISO 8601 string).
export async function changedOrders(since) {
const orders = [];
for (let page = 1; ; page++) {
const url = `${BASE}/orders?updated_after=${encodeURIComponent(since)}&per_page=100&page=${page}`;
const res = await fetch(url, { headers: { "X-RPH-Api-Key": process.env.RPH_API_KEY } });
if (!res.ok) throw new Error((await res.json()).message);
const body = await res.json();
orders.push(...body.orders);
if (page >= body.total_pages) return orders;
}
}
import os, requests
BASE = "https://rphlabs.com/wp-json/rph-labs/partner/v1"
def changed_orders(since: str) -> list:
"""Every order changed since `since` (an ISO 8601 string)."""
orders, page = [], 1
while True:
r = requests.get(
f"{BASE}/orders",
headers={"X-RPH-Api-Key": os.environ["RPH_API_KEY"]},
params={"updated_after": since, "per_page": 100, "page": page},
timeout=30,
)
r.raise_for_status()
body = r.json()
orders += body["orders"]
if page >= body["total_pages"]:
return orders
page += 1
function changed_orders(string $since): array {
$orders = [];
for ($page = 1; ; $page++) {
$url = 'https://rphlabs.com/wp-json/rph-labs/partner/v1/orders?'
. http_build_query(['updated_after' => $since, 'per_page' => 100, 'page' => $page]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['X-RPH-Api-Key: ' . getenv('RPH_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
]);
$body = json_decode(curl_exec($ch), true);
$orders = array_merge($orders, $body['orders']);
if ($page >= $body['total_pages']) {
return $orders;
}
}
}
Security and HIPAA
- Treat order responses as patient records. They include names, contact details, dates of birth and addresses. Encrypt them at rest, limit who can read them, and keep them out of general-purpose logs.
- Results need a BAA. Result PDFs and the
resultslist are only available once a signed Business Associate Agreement is on file. Every download is recorded on the order. - Nothing is pushed to you. RPh Labs never sends patient data to your systems. You request it, over an authenticated connection, from servers you have allowlisted.
- Get consent. Before you send a customer's details to RPh Labs, make sure they have agreed to share them with us for testing.
- Protect the key. Store it in a secrets manager or environment variable, call the API only from your servers, and ask us to rotate it if anyone who should not have it may have seen it.
- HTTPS only. The API only works over TLS, and every response sends
Strict-Transport-Security. - Nothing is cached. Every response sends
Cache-Control: no-store, so browsers, proxies and CDNs do not keep copies of patient data. Don't cache order responses on your side longer than you need them. - Every access is logged. RPh Labs keeps an append-only audit log of API requests and of every change to your partner profile (key issued or rotated, BAA status, allowlist), for HIPAA access review.
- Restrict by IP. Ask us to put your server addresses on your allowlist before you go live, so a leaked key is useless anywhere else.
- Rotate yearly. Keys expire 365 days after they are issued. Watch
X-RPH-Key-Expires(orkey_expires_atfromGET /ping) and ask for a replacement before then; the old key stops the moment the new one is issued. - Keys stay out of URLs and logs. The API refuses a key sent in the query string. On your side, keep the key out of request logs, error reports and crash dumps.
- Nothing to discover. The site's REST index and route listings are closed to anonymous callers, request bodies are capped at 32 KB, and every API response is marked
noindexandnosniff.
Changelog
| Date | Change |
|---|---|
| 2026-10-18 | Lockdown: keys expire one year after issue (401 key_expired; X-RPH-Key-Expires header and key_issued_at / key_expires_at on GET /ping); a key in the URL is refused (400 key_in_url); request bodies capped at 32 KB (413 payload_too_large); the site's REST index and route listings closed to anonymous callers. |
| 2026-10-16 | Security hardening: IP allowlists, failed-key lockout, browser requests refused, no-store and HSTS headers, audit log. On POST /orders, every body field is now required: patient.sex, all four intake lists, billing_address, address_2 and country on both addresses, notes and send_payment_email. address_2 and notes may be "". sku is no longer accepted in place of test_id. |
| 2026-10-16 | Version 1: catalog, orders, cancellation, result PDFs and change tracking through updated_after. |
Questions, or ready to integrate? Contact RPh Labs to request your API key.