Let a third-party system create test cases for your drivers and clients, and subscribe to webhook events when those test cases change. Generate a key and a webhook endpoint from the API Keys & Webhooks settings page in DTS.
Every request must include your API key as a Bearer token. Keys are shown once at creation — if lost, revoke and generate a new one.
Authorization: Bearer dts_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
| Scope | Grants |
|---|---|
| test-cases:create | POST /v1/test-cases |
| test-cases:read | GET /v1/test-cases, GET /v1/test-cases/:id |
| test-cases:result | GET /v1/test-cases/:id/result |
POST /v1/test-cases — requires the test-cases:create scope.
Third parties don't have your internal driver/client IDs, so identify them by license number + state (driver) and USDOT number (client company) — DTS matches an existing record on these fields or creates a new one automatically. The response only ever contains test case fields, not driver/client details.
curl -X POST https://api.drugtestingsites.app/v1/test-cases \
-H "Authorization: Bearer dts_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"driver": {
"externalId": "partner-driver-4821",
"firstName": "Jane",
"lastName": "Doe",
"licenseNumber": "D1234567",
"licenseState": "TX",
"phoneNumber": "+15125550123",
"email": "jane.doe@example.com"
},
"client": {
"externalId": "partner-company-102",
"companyName": "Acme Trucking LLC",
"usdot": "1234567",
"email": "compliance@acmetrucking.com",
"phoneNumber": "+15125550199",
"contactPerson": "John Smith"
},
"type": "urine",
"reason": "pre-employment",
"orderDate": "2026-09-18",
"zipCode": "90210"
}'
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "created",
"type": "urine",
"reason": "pre-employment",
"orderDate": "2026-09-18"
}
driver.uuid / client.uuid are also accepted and
skip the license/USDOT lookup entirely — but note that no response from
this API ever returns a DTS uuid to you, so this only applies if you
obtained one some other way (e.g. a DTS staff member shared it with you
directly). For normal integration use, identify drivers/clients by
license/state and USDOT as shown above.
| Field | Required | Notes |
|---|---|---|
| driver.uuid | No | If given, links to this existing driver directly and every other driver.* field is ignored/optional. |
| driver.firstName / lastName | Yes | — |
| driver.licenseNumber / licenseState | Yes | Used to match/create the driver record. |
| driver.phoneNumber | Yes | Always required on every call that identifies a driver by license/state, even if that driver already exists. If the existing record has no phone on file yet, this value backfills it. |
| driver.email | No | — |
| driver.externalId | No |
Your own identifier for this driver. Stored as-is and returned as
driverExternalId on GET /v1/test-cases and
GET /v1/test-cases/:id — not used for lookup.
|
| client.uuid | No | If given, links to this existing client company directly and every other client.* field is ignored/optional. |
| client.companyName | Yes | — |
| client.usdot | Yes | Used to match/create the client company. |
| client.email | Yes | — |
| client.phoneNumber | Yes | — |
| client.contactPerson | Yes | Name of the client company's point of contact. |
| client.externalId | No |
Your own identifier for this client company. Stored as-is and
returned as clientExternalId on
GET /v1/test-cases and
GET /v1/test-cases/:id.
|
| type | Yes | urine | hair | saliva | breath | dotPhysical | nonDotPhysical |
| reason | Yes | pre-employment | reasonable-suspicion | post-accident | follow-up | rapid-urine | self-drug | dot-drug | non-dot-drug | driver-query | random | non-dot-breath-alcohol | dot-breath-alcohol | return-to-duty | renewal | dmv |
| orderDate | Yes | YYYY-MM-DD — the date the test will be held on. |
| zipCode | Yes | — |
Requires the test-cases:read scope.
Responses contain test case fields plus driverExternalId and
clientExternalId — whatever you sent as
driver.externalId / client.externalId at
creation, or null if you didn't send one. No other
driver/client details (name, license, company, email) are ever returned.
curl https://api.drugtestingsites.app/v1/test-cases?page=1&limit=20 \
-H "Authorization: Bearer dts_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
curl https://api.drugtestingsites.app/v1/test-cases/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer dts_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"driverExternalId": "partner-driver-4821",
"clientExternalId": "partner-company-102",
"type": "urine",
"reason": "pre-employment",
"orderDate": "2026-09-18",
"createdAt": "2026-09-20T14:03:11.000Z",
"updatedAt": "2026-09-20T14:03:11.000Z"
}
Result fields aren't included here — fetch them from
GET /v1/test-cases/:id/result instead.
GET /v1/test-cases/:id/result — requires the test-cases:result scope.
curl https://api.drugtestingsites.app/v1/test-cases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/result \
-H "Authorization: Bearer dts_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"result": "negative",
"resultDate": "2026-09-22",
"resultAddedAt": "2026-09-22T16:40:02.000Z",
"resultPdf": "JVBERi0xLjQKJc...(base64-encoded PDF bytes, truncated)",
"ccfNumber": "ABC123",
"ccfAddedAt": "2026-09-20T14:03:11.000Z",
"ccfForm": "JVBERi0xLjQKJc...(base64-encoded PDF bytes, truncated)"
}
Fields are null until a result has been added to the test
case. resultPdf and ccfForm are both the
base64-encoded bytes of the respective PDF (not a URL) — decode and write
to a file, e.g. Buffer.from(resultPdf, "base64") in Node.
Errors return a non-2xx status with a JSON body containing an
error message.
{
"error": "Insufficient scope. Required: test-cases:create"
}
DTS POSTs an event payload to each active, subscribed endpoint. Receivers
should fetch full details via the API using resourceId — the
payload itself is intentionally thin.
| Event | Fires when |
|---|---|
| test_case.created | A new test case is created. |
| test_case.updated | A test case's fields are updated. |
| test_case.result_added | A result is added to a test case. |
{
"event": "test_case.created",
"tenantId": "8f14e45f-ceea-467e-b7f1-9f0a3d2c1b45",
"resourceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"resourceType": "test_case",
"timestamp": "2026-09-20T14:03:11.000Z"
}
X-DTS-Signature: t=1758375791,v1=5c1a3f9b2e...
X-DTS-Event: test_case.created
X-DTS-Delivery: 3f6a1b2c-...
X-DTS-Event-ID: evt_9d8c7b6a-...
X-DTS-Signature is
t=<unix_seconds>,v1=<hmac_sha256_hex>, where the
HMAC key is your endpoint's signing secret and the signed message is
${t}.${rawRequestBody}. Reject signatures older than 5
seconds to prevent replay.
const crypto = require("crypto");
function verifyDtsSignature(signatureHeader, rawBody, signingSecret, toleranceSec = 5) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
const receivedSig = parts.v1;
if (!timestamp || !receivedSig) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSec) return false;
const expectedSig = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(receivedSig, "hex"), Buffer.from(expectedSig, "hex"));
}
Failed deliveries retry with backoff (1 min, 5 min, 15 min, 1 hour, 6 hours) up to 6 attempts, then the delivery is marked abandoned. Use "Send test event" on an endpoint to verify your receiver before going live.