NexaScreen

DTS Public API

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.

Authentication

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

Scopes

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

Create a test case

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.

Request

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"
  }'

Response — 201 Created

{
  "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 reference

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 —

List and fetch test cases

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"

Response — GET /v1/test-cases/:id

{
  "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.

Fetch a test result

GET /v1/test-cases/:id/result — requires the test-cases:result scope.

Request

curl https://api.drugtestingsites.app/v1/test-cases/a1b2c3d4-e5f6-7890-abcd-ef1234567890/result \
  -H "Authorization: Bearer dts_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Response

{
  "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

Errors return a non-2xx status with a JSON body containing an error message.

{
  "error": "Insufficient scope. Required: test-cases:create"
}

Webhooks

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 types

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.

Payload

{
  "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"
}

Headers

X-DTS-Signature: t=1758375791,v1=5c1a3f9b2e...
X-DTS-Event: test_case.created
X-DTS-Delivery: 3f6a1b2c-...
X-DTS-Event-ID: evt_9d8c7b6a-...

Verifying the signature

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.