Quick start
Every endpoint is read-only, versioned under /api/v1, and returns JSON. The base URL is https://drafttally.app.
curl "https://drafttally.app/api/v1/surveys?status=approved&limit=2" \
-H "Authorization: Bearer dt_live_YOUR_KEY"{
"surveys": [
{
"surveyId": "srv_8f21c0a94e6b4d17",
"surveyName": "ACL 4021 — Cargill Westwego",
"bargeNumber": "ACL 4021",
"vessel": "MV Delta Runner",
"surveyor": "Sam Rivers",
"location": "Westwego, LA",
"client": "Cargill",
"companyName": "Cargill Grain",
"status": "approved",
"lightLocation": "Westwego, LA",
"lightSalinity": "1.000",
"loadedLocation": "Westwego, LA",
"loadedSalinity": "1.002",
"averageDraft": "9-4-2",
"cargoWeightTons": 1487.25,
"cargoWeightMetric": 1349.03,
"weather": "Clear",
"river": "Mississippi",
"createdAt": "2026-08-14T13:22:41.118Z",
"updatedAt": "2026-08-15T18:04:09.660Z"
}
],
"nextCursor": "eyJQSyI6Ik9SRyNvcmdfMTIzIiwiU0siOiJTVVJWRVkjc3J2Xzhm"
}Getting a key
API access is an Enterprise plan feature. Organizations on other plans get 403 forbidden from every /api/v1 endpoint, and the key screen shows an upgrade link instead of a create button.
- 1Sign in as an organization admin and open Admin → API Keys. The same screen is in the mobile app under More → Admin → API Keys.
- 2Create a key, naming it after the system that will use it, and optionally give it an expiry of 30 days, 90 days, or a year.
- 3Copy the key immediately and store it in your own secret manager. Revoking and re-issuing is the only recovery.
The secret is shown exactly once, at creation. DraftTally stores only a SHA-256 hash of it and a short prefix such as dt_live_9fQ2ax so you can tell keys apart in the list. Nobody — including DraftTally support — can read a key back.
Authentication
Send the key as a bearer token. Keys look like dt_live_ followed by 40 base62 characters.
Authorization: Bearer dt_live_9fQ2axPmVt7Kd3Ns1RbYwZ4LcHu8Ej6TgXoQaWvB- A key belongs to exactly one organization. Every response contains that organization's data and nothing else — an id from another organization reads as
404, never as someone else's record. - A revoked, expired, or unknown key returns
401with the same message either way, so a probe cannot learn whether a key ever existed. - Browser and mobile callers that already hold a DraftTally session can call the same endpoints with their session instead of a key; the Enterprise gate still applies.
- Each key records the time it was last used, refreshed at most once a minute, so you can spot a credential nothing is calling with any more.
List surveys
The organization's surveys. Records come back in a stable store order rather than by date — every item carries createdAt and updatedAt, and from and to select the period you care about.
| Parameter | Type | Description |
|---|---|---|
| status | string | One of draft, submitted, broker_review, pending_approval, approved, rejected, archived. |
| from | YYYY-MM-DD | Only surveys created on or after this UTC day. |
| to | YYYY-MM-DD | Only surveys created on or before this UTC day. Must not be earlier than from. |
| bargeNumber | string | Exact match on the barge number, for example ACL 4021. |
| client | string | Exact match on the client name recorded on the survey. |
| limit | integer | Records read per page, 1 to 200. Defaults to 50. |
| cursor | string | The nextCursor from the previous page. |
| id | string | Original single-survey lookup, kept working for existing callers. Returns the same object as GET /api/v1/surveys/{surveyId}. |
curl "https://drafttally.app/api/v1/surveys?from=2026-08-01&to=2026-08-31&bargeNumber=ACL%204021" \
-H "Authorization: Bearer dt_live_YOUR_KEY"Every element of surveys has the fields shown in the quick start. cargoWeightTons, cargoWeightMetric and nextCursor are omitted when they have no value.
Get a survey
One survey with every measurement, condition, and workflow timestamp. Photo links are presigned and expire an hour after the response; fetch the survey again to renew them. Signature blocks carry who signed and when — the drawn signature images are not part of the API. Platform-internal identifiers are not returned.
curl "https://drafttally.app/api/v1/surveys/srv_8f21c0a94e6b4d17" \
-H "Authorization: Bearer dt_live_YOUR_KEY"{
"survey": {
"surveyId": "srv_8f21c0a94e6b4d17",
"orgId": "org_4b7d2e",
"surveyName": "ACL 4021 — Cargill Westwego",
"description": "Loaded corn, north dock",
"status": "approved",
"bargeNumber": "ACL 4021",
"vessel": "MV Delta Runner",
"surveyor": "Sam Rivers",
"location": "Westwego, LA",
"client": "Cargill",
"companyName": "Cargill Grain",
"contactName": "Dana Okafor",
"covers": "Rolling",
"liftTop": "No",
"depthType": "Standard",
"depthA": { "ft": "12", "in": "0" },
"depthB": { "ft": "12", "in": "0" },
"depthC": { "ft": "12", "in": "1" },
"depthD": { "ft": "12", "in": "0" },
"rakeE": { "ft": "3", "in": "6" },
"rakeF": { "ft": "3", "in": "6" },
"rakeG": { "ft": "3", "in": "6" },
"mhToCoaming": "2.5",
"coverLength": "175",
"deckToCoaming": "1.5",
"doubleBottomDepth": "0",
"lightLocation": "Westwego, LA",
"lightGpsLat": "29.9048",
"lightGpsLng": "-90.1526",
"lightSalinity": "1.000",
"lightFreeboards": {
"date": "2026-08-14",
"portFwd": { "feet": "7", "inches": "10", "subInches": "1/4" },
"portMid": { "feet": "7", "inches": "9", "subInches": "1/2" },
"portAft": { "feet": "7", "inches": "9", "subInches": "0" },
"stbdFwd": { "feet": "7", "inches": "10", "subInches": "0" },
"stbdMid": { "feet": "7", "inches": "9", "subInches": "1/2" },
"stbdAft": { "feet": "7", "inches": "9", "subInches": "1/4" }
},
"lightVoids": {
"bowPort": ["0", "0"],
"bowStbd": ["0", "0"],
"sternPort": ["0", "0"],
"sternStbd": ["0", "0"]
},
"loadedLocation": "Westwego, LA",
"loadedGpsLat": "29.9048",
"loadedGpsLng": "-90.1526",
"loadedSalinity": "1.002",
"loadedFreeboards": {
"date": "2026-08-15",
"portFwd": { "feet": "2", "inches": "8", "subInches": "0" },
"portMid": { "feet": "2", "inches": "7", "subInches": "1/2" },
"portAft": { "feet": "2", "inches": "7", "subInches": "1/4" },
"stbdFwd": { "feet": "2", "inches": "8", "subInches": "1/4" },
"stbdMid": { "feet": "2", "inches": "7", "subInches": "1/2" },
"stbdAft": { "feet": "2", "inches": "7", "subInches": "0" }
},
"loadedVoids": {
"bowPort": ["0", "0"],
"bowStbd": ["0", "0"],
"sternPort": ["0", "0"],
"sternStbd": ["0", "0"]
},
"averageDraft": "9-4-2",
"gpsLat": "29.9048",
"gpsLng": "-90.1526",
"gpsAddress": "Westwego, LA 70094",
"weather": "Clear",
"river": "Mississippi",
"hopperCondition": "Dry",
"hopperMoisture": "None",
"rainfallAmount": "0",
"notes": "Hatch covers dry, no standing water.",
"bargeProfileId": "brg_5c1a83f0d24e7b96",
"cargoWeightTons": 1487.25,
"cargoWeightMetric": 1349.03,
"cargoCalc": {
"method": "register_table",
"profileId": "brg_5c1a83f0d24e7b96",
"profileName": "ACL 200ft Covered",
"tableSource": "Conti register rev 2024-03",
"tableFingerprint": "3f9a1c7d5b02",
"tableRows": 145,
"lightDensityLbFt3": 62.4,
"loadedDensityLbFt3": 62.5,
"computedAt": "2026-08-15T17:58:12.004Z"
},
"photos": [
{
"id": "pho_1d94",
"name": "hatch-1.jpg",
"url": "https://drafttally-uploads.s3.us-east-2.amazonaws.com/...",
"uploadedAt": "2026-08-15T17:41:02.887Z"
}
],
"signatures": {
"surveyor": { "signerName": "Sam Rivers", "signedAt": "2026-08-15T17:59:40.221Z" },
"broker": { "signerName": "Ida Kelley", "signedAt": "2026-08-15T18:01:15.900Z" },
"client": { "signerName": "Dana Okafor", "signedAt": "2026-08-15T18:04:09.660Z" }
},
"submittedAt": "2026-08-15T18:00:02.512Z",
"brokerApprovedAt": "2026-08-15T18:01:15.900Z",
"clientApprovedAt": "2026-08-15T18:04:09.660Z",
"approvalNotes": "Figures agree with our scale ticket.",
"createdAt": "2026-08-14T13:22:41.118Z",
"updatedAt": "2026-08-15T18:04:09.660Z"
}
}List barge profiles
The hull profiles your organization maintains, with the provenance of each register table: where it came from, when it last changed, its fingerprint, and how many rows it holds. The rows themselves are not served — the fingerprint is what ties a survey's cargo figure to the exact table that produced it.
| Parameter | Type | Description |
|---|---|---|
| status | string | active or disabled. Omit for both. |
| limit | integer | Records read per page, 1 to 200. Defaults to 50. |
| cursor | string | The nextCursor from the previous page. |
curl "https://drafttally.app/api/v1/barges?status=active" \
-H "Authorization: Bearer dt_live_YOUR_KEY"{
"barges": [
{
"profileId": "brg_5c1a83f0d24e7b96",
"name": "ACL 200ft Covered",
"bargeNumber": "ACL 4021",
"loa": 200,
"beam": 35,
"depth": 12,
"blockCoefficient": 0.9,
"tpi": 11.67,
"lightShipWeight": 450,
"status": "active",
"hasRegisterTable": true,
"tableSource": "Conti register rev 2024-03",
"tableUpdatedAt": "2026-03-04T15:12:44.301Z",
"tableFingerprint": "3f9a1c7d5b02",
"tableRowCount": 145,
"createdAt": "2025-11-02T19:40:11.244Z",
"updatedAt": "2026-03-04T15:12:44.301Z"
}
]
}Get an issued document
Every PDF DraftTally issues carries a document ID printed on it and a SHA-256 of the exact bytes handed out. This endpoint returns your organization's record of one of them, so you can file the hash alongside your own copy and prove later that the PDF was not edited. Document IDs are accepted in any case, with or without the DT- prefix.
curl "https://drafttally.app/api/v1/documents/DT-4KP9RJ2XQ7" \
-H "Authorization: Bearer dt_live_YOUR_KEY"{
"document": {
"documentId": "DT-4KP9RJ2XQ7",
"kind": "survey_worksheet",
"surveyId": "srv_8f21c0a94e6b4d17",
"surveyName": "ACL 4021 — Cargill Westwego",
"bargeNumber": "ACL 4021",
"surveyStatus": "approved",
"surveyDate": "2026-08-15",
"signatures": [
{ "slot": "surveyor", "signerName": "Sam Rivers", "signedAt": "2026-08-15T17:59:40.221Z" },
{ "slot": "broker", "signerName": "Ida Kelley", "signedAt": "2026-08-15T18:01:15.900Z" }
],
"sha256": "9d2f1b0c7a45e8836bb1f0a2c4d59e7318ba6cf05d2e4917ab83c60d15fe74a2",
"byteLength": 284119,
"issuedAt": "2026-08-15T18:05:22.470Z",
"issuedBy": { "name": "Ida Kelley", "email": "ida@example.com" },
"verifyUrl": "https://drafttally.app/verify?id=DT-4KP9RJ2XQ7"
}
}Anyone holding the PDF, with no DraftTally account, can check the same document at https://drafttally.app/verify. That public page shows the issuing organization and the hash, and nothing from inside the survey.
Pagination
List endpoints return an opaque nextCursor when more records remain. Pass it back unchanged as the cursor parameter, keeping the other parameters the same, until the response has no nextCursor.
curl "https://drafttally.app/api/v1/surveys?limit=100&cursor=eyJQSyI6Ik9SRyNvcmdfMTIzIiwiU0si..." \
-H "Authorization: Bearer dt_live_YOUR_KEY"limitcaps how many records a page reads, from 1 to 200, defaulting to 50.- Filters are applied while reading, so a filtered page can hold fewer items than the limit — even zero — and still return a
nextCursor. An empty page is not the end of the results; a missingnextCursoris. - A cursor encodes a position, not a snapshot. Records created after you started paging may not appear until the next pass.
Errors
Errors use the matching HTTP status and a body with a human-readable error and a stable machine-readable code.
{
"error": "Invalid API key",
"code": "unauthorized"
}| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A query parameter is missing, malformed, or out of range — a limit above 200, a date that is not YYYY-MM-DD, a from later than to. |
| 401 | unauthorized | No credential, or a key that is unknown, revoked, or expired. The message is the same in every case. |
| 403 | forbidden | The organization's plan does not include API access. |
| 404 | not_found | No such record in this organization. Ids belonging to another organization answer the same way. |
| 429 | rate_limited | The per-key rate limit was exceeded. Retry after the seconds given in the Retry-After header. |
| 500 | internal_error | Something failed on our side. The response never carries internal detail; retry, then contact support. |
Rate limits
Each key may make 120 requests per minute, counted over a rolling window. Session-authenticated callers are counted per user against the same budget. Over the limit, the API answers 429 with a Retry-After header in seconds.
HTTP/1.1 429 Too Many Requests
Retry-After: 24
{
"error": "Rate limit exceeded. The limit is 120 requests per minute.",
"code": "rate_limited"
}Issue a separate key per integration so one busy job cannot starve another, and so revoking one does not take the rest down with it.
Ready to connect your systems?
API access is included with the Enterprise plan. Create a key from the admin screen and make your first call in a minute.