API v1 · Stable

DNAChain REST API

Integrate your LIMS or lab automation platform with DNAChain's tamper-evident chain-of-custody. Register samples, initiate transfers, and pull consent and audit records over HTTPS with scoped API keys.

Base URL

All endpoints are served from a single stable HTTPS origin:

https://api.dnachain.bio/v1

All requests must use HTTPS. All responses are JSON.

Authentication

Every request must include an Authorization header with a Bearer API key. Admins can create keys in Settings → API keys.

curl "https://api.dnachain.bio/v1/samples" \
  -H "Authorization: Bearer dna_live_YOUR_KEY_HERE"

Keys carry one or more scopes. A request missing the required scope returns 403 INSUFFICIENT_SCOPE.

samples:read
GET /samples, /samples/:id, /integrity/:id
samples:write
POST /samples
transfers:read
GET /transfers
transfers:write
POST /transfers
consent:read
GET /consent
audit:read
GET /audit

Endpoints

GET/samplessamples:read

List samples for your organization. Supports pagination and filters.

Request
curl "https://api.dnachain.bio/v1/samples?limit=50&status=Anchored" \
  -H "Authorization: Bearer dna_live_YOUR_KEY"
Response
{
  "data": [{
    "id": "…",
    "sample_code": "SMP-001",
    "donor_ref": "D-123",
    "sample_type": "Whole Blood",
    "collected_at": "2026-06-01",
    "custodian": "Dr. Smith",
    "location": "Cambridge, MA",
    "chain_status": "Anchored",
    "consent_status": "Active",
    "current_hash": "a3f2…",
    "created_at": "2026-06-01T10:22:11Z"
  }],
  "total": 128, "limit": 50, "offset": 0
}
POST/samplessamples:write

Register a new sample. Returns the genesis hash and initial chain status.

Request
curl -X POST "https://api.dnachain.bio/v1/samples" \
  -H "Authorization: Bearer dna_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sample_code": "SMP-001",
    "donor_ref": "D-123",
    "sample_type": "Whole Blood",
    "collected_at": "2026-06-01",
    "custodian": "Dr. Smith",
    "location": "Cambridge, MA",
    "irb_protocol": "IRB-2026-01",
    "consent_type": "Broad research",
    "consented_at": "2026-06-01"
  }'
Response
{
  "id": "…",
  "sample_code": "SMP-001",
  "hash": "a3f2…",
  "chain_status": "Pending",
  "created_at": "2026-06-01T10:22:11Z"
}
GET/samples/:idsamples:read

Fetch a single sample with its full custody event history and consent records.

Response
{
  "sample": { "id": "…", "sample_code": "SMP-001", "…": "…" },
  "custody_events": [
    { "event_type": "registered", "to_custodian": "Dr. Smith",
      "event_hash": "a3f2…", "created_at": "…" }
  ],
  "consent_records": [
    { "consent_type": "Broad research", "status": "Active", "expires_at": null }
  ]
}
GET/transferstransfers:read

List transfers for your organization.

Response
{
  "data": [{
    "id": "…", "sample_id": "…",
    "from_custodian": "Dr. Smith", "to_custodian": "Dr. Chen",
    "status": "In Transit", "initiated_at": "…", "current_hash": "…"
  }],
  "total": 12, "limit": 50, "offset": 0
}
POST/transferstransfers:write

Initiate a transfer. The sample's custody chain is extended atomically.

Request
curl -X POST "https://api.dnachain.bio/v1/transfers" \
  -H "Authorization: Bearer dna_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sample_id": "…",
    "to_custodian": "Dr. Chen",
    "to_location": "Boston, MA"
  }'
Response
{
  "id": "…",
  "sample_id": "…",
  "status": "In Transit",
  "hash": "b7e8…",
  "initiated_at": "…"
}
GET/consentconsent:read

List consent records with expiry status. Filter by sample_id or status.

Response
{
  "data": [{
    "id": "…", "sample_id": "…", "donor_ref": "D-123",
    "consent_type": "Broad research", "irb_protocol": "IRB-2026-01",
    "status": "Active", "expires_at": "2027-06-01", "days_until_expiry": 322
  }],
  "total": 128, "limit": 50, "offset": 0
}
GET/auditaudit:read

Pull audit log entries. Supports action, from_date, and to_date filters. Max limit 500.

Response
{
  "data": [{
    "id": "…", "action": "sample.registered",
    "table_name": "samples", "record_id": "…",
    "actor_id": "…", "created_at": "…"
  }],
  "total": 1284, "limit": 100, "offset": 0
}
GET/integrity/:sample_idsamples:read

Recompute and verify the full custody hash chain for a sample.

Response
{
  "sample_id": "…",
  "integrity_ok": true,
  "chain_status": "Anchored",
  "checked_at": "2026-07-14T12:00:00Z"
}

Rate limits

Rate limits are enforced per API key, per minute:

  • Read endpoints: 100 requests / minute
  • Write endpoints: 20 requests / minute

Every response includes:

X-Request-ID: <uuid>
X-RateLimit-Remaining: <n>
X-RateLimit-Reset: <unix-epoch-seconds>

When exceeded, the API returns 429 RATE_LIMITED with a Retry-After header indicating seconds until reset.

Error format

All errors share a uniform envelope:

{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "Invalid or missing API key"
  }
}
INVALID_API_KEY401Key missing, malformed, revoked, or expired
INSUFFICIENT_SCOPE403Key does not carry the required scope
NOT_FOUND404Resource does not exist or not in your org
VALIDATION_ERROR400Request body or query parameter is invalid
RATE_LIMITED429Per-minute rate limit exceeded
INTERNAL_ERROR500Unexpected server-side error

Code examples

curl

curl "https://api.dnachain.bio/v1/samples?limit=10" \
  -H "Authorization: Bearer dna_live_YOUR_KEY"

Python

import requests

API_KEY = "dna_live_YOUR_KEY"
BASE = "https://api.dnachain.bio/v1"

r = requests.get(f"{BASE}/samples",
                 headers={"Authorization": f"Bearer {API_KEY}"})
r.raise_for_status()
print(r.json())

# Register a sample
r = requests.post(f"{BASE}/samples",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "sample_code": "SMP-001",
        "donor_ref": "D-123",
        "sample_type": "Whole Blood",
        "collected_at": "2026-06-01",
        "custodian": "Dr. Smith",
        "location": "Cambridge, MA",
        "irb_protocol": "IRB-2026-01",
        "consent_type": "Broad research",
        "consented_at": "2026-06-01",
    })
print(r.json())

JavaScript

const API_KEY = "dna_live_YOUR_KEY";
const BASE = "https://api.dnachain.bio/v1";

// List samples
const res = await fetch(`${BASE}/samples?limit=10`, {
  headers: { Authorization: `Bearer ${API_KEY}` },
});
const { data, total } = await res.json();
console.log(`${total} samples`, data);

// Register a sample
const created = await fetch(`${BASE}/samples`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    sample_code: "SMP-001",
    donor_ref: "D-123",
    sample_type: "Whole Blood",
    collected_at: "2026-06-01",
    custodian: "Dr. Smith",
    location: "Cambridge, MA",
    irb_protocol: "IRB-2026-01",
    consent_type: "Broad research",
    consented_at: "2026-06-01",
  }),
}).then((r) => r.json());
console.log(created);
Need help? Contact support@ordex-systems.com.