Reading results
GET /v1/extractions/{jobId}/sheets/{sheetNumber} answers one sheet’s values; sheetNumber 1 is the file’s
first page. A sheet not read yet answers with its status and no values.
#!/usr/bin/env bashset -euo pipefailAPI=https://api.speccapture.comAUTH="Authorization: Bearer $SPECCAPTURE_API_KEY"
JOB_ID=7c9e6679-7425-40de-944b-e07fc1f90ae7 # the jobId the submission answered withSHEET_NUMBER=1 # which sheet: 1 is the first page
sheet=$(curl -sS --fail-with-body "$API/v1/extractions/$JOB_ID/sheets/$SHEET_NUMBER" \ -H "$AUTH") || { echo "$sheet" >&2; exit 1; }jq -r '"Sheet \(.sheetNumber) is \(.reviewState)"' <<<"$sheet"
jq -r '.fields[] as $field | (" \($field.path) = \($field.value // "(not on the sheet)") (\($field.confidence))", if $field.reviewState == "needsReview" then " needs a look" else empty end)' <<<"$sheet"Save as read_fields.sh, set SPECCAPTURE_API_KEY, and run: bash read_fields.sh. Needs curl; scripts also use bash and jq.
using System.Text.Json;
using HttpClient http = new() { BaseAddress = new Uri("https://api.speccapture.com") };http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("SPECCAPTURE_API_KEY"));
string jobId = args[0]; // the jobId the submission answered withstring sheetNumber = args[1]; // which sheet: 1 is the first page
JsonElement sheet = await Json(await Call(HttpMethod.Get, $"/v1/extractions/{jobId}/sheets/{sheetNumber}"));Console.WriteLine($"Sheet {sheet.GetProperty("sheetNumber")} is {sheet.GetProperty("reviewState")}");
foreach (JsonElement field in sheet.GetProperty("fields").EnumerateArray()) { Console.WriteLine($" {field.GetProperty("path")} = {field.GetProperty("value").GetString() ?? "(not on the sheet)"} ({field.GetProperty("confidence")})"); if (field.GetProperty("reviewState").GetString() == "needsReview") { Console.WriteLine(" needs a look"); }}
// Sends a request; stops with the problem's code and detail if the API refuses it.async Task<HttpResponseMessage> Call(HttpMethod method, string path, HttpContent? body = null, string? idempotencyKey = null) { using HttpRequestMessage request = new(method, path) { Content = body }; if (idempotencyKey is not null) request.Headers.Add("Idempotency-Key", idempotencyKey); HttpResponseMessage response = await http.SendAsync(request); if (!response.IsSuccessStatusCode) { JsonElement problem = await Json(response); string? detail = (problem.TryGetProperty("detail", out JsonElement text) ? text : problem.GetProperty("title")).GetString(); Console.Error.WriteLine($"{(int)response.StatusCode} {problem.GetProperty("code")}: {detail}"); Environment.Exit(1); } return response;}
// Reads an answer as JSON.static async Task<JsonElement> Json(HttpResponseMessage response) => JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;Save as read_fields.cs, set SPECCAPTURE_API_KEY, and run: dotnet run read_fields.cs -- 7c9e6679-7425-40de-944b-e07fc1f90ae7 1. Needs .NET 10 or later.
import osimport sys
import requests
API = "https://api.speccapture.com"session = requests.Session()session.headers["Authorization"] = f"Bearer {os.environ['SPECCAPTURE_API_KEY']}"
def call(method, path, **kwargs): """Send a request; stop with the problem's code and detail if the API refuses it.""" response = session.request(method, API + path, **kwargs) if not response.ok: problem = response.json() sys.exit(f"{response.status_code} {problem['code']}: {problem.get('detail', problem['title'])}") return response
job_id = sys.argv[1] # the jobId the submission answered withsheet_number = sys.argv[2] # which sheet: 1 is the first page
sheet = call("GET", f"/v1/extractions/{job_id}/sheets/{sheet_number}").json()print(f"Sheet {sheet['sheetNumber']} is {sheet['reviewState']}")
for field in sheet["fields"]: print(f" {field['path']} = {field['value'] or '(not on the sheet)'} ({field['confidence']})") if field["reviewState"] == "needsReview": print(" needs a look")Save as read_fields.py, set SPECCAPTURE_API_KEY, and run: python read_fields.py 7c9e6679-7425-40de-944b-e07fc1f90ae7 1. Needs Python 3.9+ and requests (pip install requests).
const API = "https://api.speccapture.com";const headers = { Authorization: `Bearer ${process.env.SPECCAPTURE_API_KEY}` };
const [jobId, sheetNumber] = process.argv.slice(2); // the jobId the submission answered with; which sheet: 1 is the first page
const sheet = await call("GET", `/v1/extractions/${jobId}/sheets/${sheetNumber}`).then((response) => response.json());console.log(`Sheet ${sheet.sheetNumber} is ${sheet.reviewState}`);
for (const field of sheet.fields) { console.log(` ${field.path} = ${field.value ?? "(not on the sheet)"} (${field.confidence})`); if (field.reviewState === "needsReview") { console.log(" needs a look"); }}
// Sends a request; stops with the problem's code and detail if the API refuses it.async function call(method, path, init = {}) { const response = await fetch(API + path, { ...init, method, headers: { ...headers, ...init.headers } }); if (!response.ok) { const problem = await response.json(); console.error(`${response.status} ${problem.code}: ${problem.detail ?? problem.title}`); process.exit(1); } return response;}Save as read_fields.mjs, set SPECCAPTURE_API_KEY, and run: node read_fields.mjs 7c9e6679-7425-40de-944b-e07fc1f90ae7 1. Needs Node.js 20 or later.
A sheet
Section titled “A sheet”A sheet’s values, shortened to one field, one table cell and one general note:
{ "jobId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "sheetNumber": 1, "status": "completed", "reviewState": "needsReview", "engineVersion": "ev1-3684d4286e2783c2", "completedAt": "2026-10-12T09:01:00+00:00", "fields": [ { "path": "titleBlock.partNumber", "value": "SG-2001", "extractedValue": "SG-2001", "confidence": 0.97, "reviewState": "autoAccepted", "reviewReasons": [], "box": { "points": [ { "x": 0.6367, "y": 0.8016 }, { "x": 0.6698, "y": 0.8016 }, { "x": 0.6698, "y": 0.8248 }, { "x": 0.6367, "y": 0.8248 } ] } } ], "tables": [ { "path": "revisionHistory", "rows": [ { "rowIndex": 0, "cells": { "rev": { "path": "revisionHistory[0].rev", "value": "B", "extractedValue": "B", "confidence": 0.62, "reviewState": "needsReview", "reviewReasons": ["HandwrittenInk"], "box": null } } } ] }, { "path": "generalNotes", "rows": [ { "rowIndex": 0, "cells": { "value": { "path": "generalNotes[0]", "value": "1. REMOVE ALL BURRS AND SHARP EDGES.", "extractedValue": "1. REMOVE ALL BURRS AND SHARP EDGES.", "confidence": null, "reviewState": "needsReview", "reviewReasons": [], "box": null } } } ] } ], "testMode": false, "markedControlled": false, "controlledMarking": null, "drawingPurgedAt": null, "controlledKept": null}reviewStateis the sheet’s:autoAccepted(every value cleared the confidence threshold and nothing asked for a look),needsReview(at least one value, or the sheet as a whole, waits for a person), orreviewed(a person has accepted or corrected every value that needed it).engineVersionnames the version of the engine that read the sheet, so two readings can be told apart.markedControlledistrue, andcontrolledMarkingsays which control, when the sheet’s own export-control marking names one:Distribution Statement BtoDistribution Statement F(or the oldDistribution Statement X),ITAR,EAR,CUI,NOFORN,FOUO, orexport-restrictedfor a warning that the drawing’s export is restricted. Otherwisefalseandnull. The job’ssheetscarry the same two. See Not for export-controlled drawings.controlledKeptis the workspace’s decision to keep a marked drawing -{ "at", "by", "reason" }: when, who (as the workspace’s people list names them,nullonce they have left it) and why, in their words - once its owner or an admin made it in the dashboard. It staysnullwhile the warning stands, and on a sheet that is not marked;markedControlledstaystrue, since the sheet says what it says. The decision covers the whole document, and an integration quarantining marked sheets can release the kept ones. The job’ssheetsand the webhook payload carry it too.fieldsholds the values that occur once on a sheet, such astitleBlock.partNumber;tablesholds the rows of each table.drawingPurgedAtis when the workspace’s retention period removed the drawing, and every value’sboxwith it;nullwhile the drawing is kept. The job carries it too. See How long the drawing is kept.
A value
Section titled “A value”| Property | What it is |
|---|---|
path |
Which value this is: titleBlock.partNumber. Every path is listed under Field paths. Use it as the key: it never changes. |
name |
What the job’s template calls it: Part Number, or the workspace’s own name for it. A table cell’s is its column’s; a note’s is Note. |
value |
The value as it stands: a reviewer’s correction when there is one, otherwise what was read. null means it is not on the sheet. |
extractedValue |
What SpecCapture read, before any correction. |
confidence |
How sure the reading is, from 0 to 1. null for a general note: a note is copied whole, as printed, and not scored. |
reviewState |
autoAccepted (delivered with nothing asking for a look), needsReview (delivered, and waiting for a person), accepted (a person confirmed it as it stands) or corrected (a person replaced it; value is the correction). |
reviewReasons |
Why the value asks for a person, such as HandwrittenInk, or RequiredButNotFound when the job’s template requires the field and the drawing does not give it. |
box |
Where on the sheet it was read: { "points": [{ "x", "y" }, ...] }, in page coordinates from 0 to 1 with the origin at the top left, so it fits the page at any size. null when the value has no single place on the sheet, and for every value once the drawing is no longer kept (the sheet’s drawingPurgedAt says when). |
Values are given as the drawing prints them: a dimension printed .375 +.002 -.000 comes back as exactly that,
not as a number worked out from it. A value waiting for review (needsReview) is delivered as read so your
software can show it, but nobody has confirmed it yet: treat it as a draft until it is accepted or
corrected. A field the sheet does not show comes back with value null; treat a path missing from fields
the same way. A job’s values are the ones its template keeps, in its order: a field the template leaves out is never
in fields.
Tables
Section titled “Tables”Each entry of tables is one table on the sheet - { "path": "bom", "name": "Bill of Materials", "rows": [...] } - and each row is
{ "rowIndex", "cells" }, numbered from 0 in the order the drawing prints them, with one value per column under
the column’s name. Every cell is a value like any other, with its own confidence, review state and box.
The general notes are the generalNotes table, a list with no columns: one row per note, in the order the
drawing prints them, each note whole under value (generalNotes[0] is the first). A note is reviewed like any
other value - accepted, corrected, or waiting for a person - but has no confidence and no box (both null):
it is copied as printed rather than scored, and drawn on no single place on the sheet.
#!/usr/bin/env bashset -euo pipefailAPI=https://api.speccapture.comAUTH="Authorization: Bearer $SPECCAPTURE_API_KEY"
JOB_ID=7c9e6679-7425-40de-944b-e07fc1f90ae7 # the jobId the submission answered withSHEET_NUMBER=1 # which sheet: 1 is the first page
sheet=$(curl -sS --fail-with-body "$API/v1/extractions/$JOB_ID/sheets/$SHEET_NUMBER" \ -H "$AUTH") || { echo "$sheet" >&2; exit 1; }
jq -r '.tables[] as $table | if $table.path == "bom" then $table.rows[] as $row | ($row.cells as $cells | "Item \($cells.item.value): \($cells.qty.value // "?") x \($cells.partNumber.value // "(no part number)"), \($cells.description.value // "(no description)")") else empty end' <<<"$sheet"Save as read_bom.sh, set SPECCAPTURE_API_KEY, and run: bash read_bom.sh. Needs curl; scripts also use bash and jq.
using System.Text.Json;
using HttpClient http = new() { BaseAddress = new Uri("https://api.speccapture.com") };http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("SPECCAPTURE_API_KEY"));
string jobId = args[0]; // the jobId the submission answered withstring sheetNumber = args[1]; // which sheet: 1 is the first page
JsonElement sheet = await Json(await Call(HttpMethod.Get, $"/v1/extractions/{jobId}/sheets/{sheetNumber}"));
foreach (JsonElement table in sheet.GetProperty("tables").EnumerateArray()) { if (table.GetProperty("path").GetString() == "bom") { foreach (JsonElement row in table.GetProperty("rows").EnumerateArray()) { JsonElement cells = row.GetProperty("cells"); Console.WriteLine($"Item {cells.GetProperty("item").GetProperty("value")}: {cells.GetProperty("qty").GetProperty("value").GetString() ?? "?"} x {cells.GetProperty("partNumber").GetProperty("value").GetString() ?? "(no part number)"}, {cells.GetProperty("description").GetProperty("value").GetString() ?? "(no description)"}"); } }}
// Sends a request; stops with the problem's code and detail if the API refuses it.async Task<HttpResponseMessage> Call(HttpMethod method, string path, HttpContent? body = null, string? idempotencyKey = null) { using HttpRequestMessage request = new(method, path) { Content = body }; if (idempotencyKey is not null) request.Headers.Add("Idempotency-Key", idempotencyKey); HttpResponseMessage response = await http.SendAsync(request); if (!response.IsSuccessStatusCode) { JsonElement problem = await Json(response); string? detail = (problem.TryGetProperty("detail", out JsonElement text) ? text : problem.GetProperty("title")).GetString(); Console.Error.WriteLine($"{(int)response.StatusCode} {problem.GetProperty("code")}: {detail}"); Environment.Exit(1); } return response;}
// Reads an answer as JSON.static async Task<JsonElement> Json(HttpResponseMessage response) => JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;Save as read_bom.cs, set SPECCAPTURE_API_KEY, and run: dotnet run read_bom.cs -- 7c9e6679-7425-40de-944b-e07fc1f90ae7 1. Needs .NET 10 or later.
import osimport sys
import requests
API = "https://api.speccapture.com"session = requests.Session()session.headers["Authorization"] = f"Bearer {os.environ['SPECCAPTURE_API_KEY']}"
def call(method, path, **kwargs): """Send a request; stop with the problem's code and detail if the API refuses it.""" response = session.request(method, API + path, **kwargs) if not response.ok: problem = response.json() sys.exit(f"{response.status_code} {problem['code']}: {problem.get('detail', problem['title'])}") return response
job_id = sys.argv[1] # the jobId the submission answered withsheet_number = sys.argv[2] # which sheet: 1 is the first page
sheet = call("GET", f"/v1/extractions/{job_id}/sheets/{sheet_number}").json()
for table in sheet["tables"]: if table["path"] == "bom": for row in table["rows"]: cells = row["cells"] print(f"Item {cells['item']['value']}: {cells['qty']['value'] or '?'} x {cells['partNumber']['value'] or '(no part number)'}, {cells['description']['value'] or '(no description)'}")Save as read_bom.py, set SPECCAPTURE_API_KEY, and run: python read_bom.py 7c9e6679-7425-40de-944b-e07fc1f90ae7 1. Needs Python 3.9+ and requests (pip install requests).
const API = "https://api.speccapture.com";const headers = { Authorization: `Bearer ${process.env.SPECCAPTURE_API_KEY}` };
const [jobId, sheetNumber] = process.argv.slice(2); // the jobId the submission answered with; which sheet: 1 is the first page
const sheet = await call("GET", `/v1/extractions/${jobId}/sheets/${sheetNumber}`).then((response) => response.json());
for (const table of sheet.tables) { if (table.path === "bom") { for (const row of table.rows) { const cells = row.cells; console.log(`Item ${cells.item.value}: ${cells.qty.value ?? "?"} x ${cells.partNumber.value ?? "(no part number)"}, ${cells.description.value ?? "(no description)"}`); } }}
// Sends a request; stops with the problem's code and detail if the API refuses it.async function call(method, path, init = {}) { const response = await fetch(API + path, { ...init, method, headers: { ...headers, ...init.headers } }); if (!response.ok) { const problem = await response.json(); console.error(`${response.status} ${problem.code}: ${problem.detail ?? problem.title}`); process.exit(1); } return response;}Save as read_bom.mjs, set SPECCAPTURE_API_KEY, and run: node read_bom.mjs 7c9e6679-7425-40de-944b-e07fc1f90ae7 1. Needs Node.js 20 or later.
How long the drawing is kept
Section titled “How long the drawing is kept”A workspace keeps each drawing for its retention period, limits.retentionDays in GET /v1/account, counted from
the job’s completedAt: 30 days unless the workspace’s owner chose
otherwise (0, 7, 30 or 90 days, on the dashboard’s workspace settings page).
Within the hour after it ends, SpecCapture deletes the drawing: the uploaded file, its page images, and the box
showing where each value was read.
- Every
boxbecomesnull. The job and the sheet carrydrawingPurgedAt, the time the drawing was deleted, so your software can tell a box that went with the drawing from a value that has no place on the sheet. Nothing else about the job changes: itsstatusstayscompleted. - View links stop. A link made earlier stops working, and a new one is refused with
409 not_shareablefrom the last hour of the retention period on. A link’sexpiresAtis never later than the drawing is kept. - The values stay: every value, its confidence, its review state and any correction, until the workspace is deleted, and so does the export, which no longer carries boxes.
With 0 days, a drawing is deleted as soon as its job has been read, its boxes within the hour, and no view link can be made. To keep the boxes longer than the drawing, store them when you read the sheet.
Field paths
Section titled “Field paths”The values a sheet can carry, from the drawing template every sheet is read with today (drawing, version 1).
Title block
Section titled “Title block”| Path | What it is |
|---|---|
titleBlock.partNumber | Part Number |
titleBlock.drawingNumber | Drawing Number |
titleBlock.revision | Revision |
titleBlock.title | Title |
titleBlock.materialSpec | Material Specification |
titleBlock.materialForm | Material Form |
titleBlock.stockSize | Stock Size |
titleBlock.finish | Finish / Coating |
titleBlock.platingSpec | Plating Specification |
titleBlock.surfaceFinish | Surface Finish (roughness) |
titleBlock.heatTreat | Heat Treatment |
titleBlock.scale | Scale |
titleBlock.units | Units · one of a set |
titleBlock.sheetSize | Sheet Size |
titleBlock.sheetNumber | Sheet Number · whole number |
titleBlock.sheetCount | Sheet Count · whole number |
titleBlock.drawnBy | Drawn By |
titleBlock.drawnDate | Drawn Date · date |
titleBlock.checkedBy | Checked By |
titleBlock.checkedDate | Checked Date · date |
titleBlock.approvedBy | Approved By |
titleBlock.approvedDate | Approved Date · date |
titleBlock.weight | Weight |
titleBlock.toleranceFractions | Default Fraction Tolerance |
titleBlock.toleranceDecimals | Default Decimal Tolerance |
titleBlock.toleranceOnePlaceDecimals | Default One-Place Decimal Tolerance |
titleBlock.toleranceTwoPlaceDecimals | Default Two-Place Decimal Tolerance |
titleBlock.toleranceThreePlaceDecimals | Default Three-Place Decimal Tolerance |
titleBlock.toleranceAngles | Default Angle Tolerance |
titleBlock.projectionAngle | Projection Angle · one of a set |
titleBlock.drawingStandard | Drawing Standard |
titleBlock.cageCode | CAGE Code |
titleBlock.exportControlMarking | Export Control Marking |
Worked out from the drawing
Section titled “Worked out from the drawing”Measurements and counts SpecCapture works out from the drawing’s geometry and notes, rather than reads from one place on it.
| Path | What it is |
|---|---|
assist.envelopeLength | Envelope Length · dimension |
assist.envelopeWidth | Envelope Width · dimension |
assist.envelopeHeight | Envelope Height · dimension |
assist.envelopeForm | Envelope Form · one of a set |
assist.envelopeUnits | Envelope Units · one of a set |
assist.tightestTolerance | Tightest Tolerance on Print · dimension |
assist.gdtCalloutCount | GD&T Callout Count · whole number |
assist.holeCount | Hole Count · whole number |
assist.threadCalloutCount | Thread Callout Count · whole number |
Tables and notes
Section titled “Tables and notes”| Table | Path | Columns (keys of cells) |
|---|---|---|
| Revision History | revisionHistory | rev, description, date, approvedBy |
| Bill of Materials | bom | item, qty, partNumber, description, material |
| Hole Table | holeTable | tag, size, qty, depth |
| Application (next assembly / used on) | application | nextAssembly, usedOn |
| Dash Numbers (tabulated variants) | dashNumbers | dashNumber, description |
| General notes | generalNotes | value (one note a row, in order) |