Exports
GET /v1/extractions/{jobId}/export?format=json|csv|xlsx downloads a finished job’s values as one file.
format is json when left out.
#!/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 with
curl -sS --fail-with-body "$API/v1/extractions/$JOB_ID/export?format=xlsx" \ -H "$AUTH" \ -o extraction.xlsxcurl -sS --fail-with-body "$API/v1/extractions/$JOB_ID/export?format=csv" \ -H "$AUTH" \ -o extraction-csv.zipcurl -sS --fail-with-body "$API/v1/extractions/$JOB_ID/export?format=json" \ -H "$AUTH" \ -o extraction.jsonecho "Saved extraction.xlsx, extraction-csv.zip and extraction.json"Save as export.sh, set SPECCAPTURE_API_KEY, and run: bash export.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 with
HttpResponseMessage xlsxDownload = await Call(HttpMethod.Get, $"/v1/extractions/{jobId}/export?format=xlsx");await File.WriteAllBytesAsync("extraction.xlsx", await xlsxDownload.Content.ReadAsByteArrayAsync());HttpResponseMessage csvDownload = await Call(HttpMethod.Get, $"/v1/extractions/{jobId}/export?format=csv");await File.WriteAllBytesAsync("extraction-csv.zip", await csvDownload.Content.ReadAsByteArrayAsync());HttpResponseMessage jsonDownload = await Call(HttpMethod.Get, $"/v1/extractions/{jobId}/export?format=json");await File.WriteAllBytesAsync("extraction.json", await jsonDownload.Content.ReadAsByteArrayAsync());Console.WriteLine("Saved extraction.xlsx, extraction-csv.zip and extraction.json");
// 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 export.cs, set SPECCAPTURE_API_KEY, and run: dotnet run export.cs -- 7c9e6679-7425-40de-944b-e07fc1f90ae7. 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 with
with open("extraction.xlsx", "wb") as download: download.write(call("GET", f"/v1/extractions/{job_id}/export?format=xlsx").content)with open("extraction-csv.zip", "wb") as download: download.write(call("GET", f"/v1/extractions/{job_id}/export?format=csv").content)with open("extraction.json", "wb") as download: download.write(call("GET", f"/v1/extractions/{job_id}/export?format=json").content)print("Saved extraction.xlsx, extraction-csv.zip and extraction.json")Save as export.py, set SPECCAPTURE_API_KEY, and run: python export.py 7c9e6679-7425-40de-944b-e07fc1f90ae7. Needs Python 3.9+ and requests (pip install requests).
import { writeFile } from "node:fs/promises";
const API = "https://api.speccapture.com";const headers = { Authorization: `Bearer ${process.env.SPECCAPTURE_API_KEY}` };
const [jobId] = process.argv.slice(2); // the jobId the submission answered with
const xlsxDownload = await call("GET", `/v1/extractions/${jobId}/export?format=xlsx`);await writeFile("extraction.xlsx", Buffer.from(await xlsxDownload.arrayBuffer()));const csvDownload = await call("GET", `/v1/extractions/${jobId}/export?format=csv`);await writeFile("extraction-csv.zip", Buffer.from(await csvDownload.arrayBuffer()));const jsonDownload = await call("GET", `/v1/extractions/${jobId}/export?format=json`);await writeFile("extraction.json", Buffer.from(await jsonDownload.arrayBuffer()));console.log("Saved extraction.xlsx, extraction-csv.zip and extraction.json");
// 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 export.mjs, set SPECCAPTURE_API_KEY, and run: node export.mjs 7c9e6679-7425-40de-944b-e07fc1f90ae7. Needs Node.js 20 or later.
format |
Content type | Saved as | What it holds |
|---|---|---|---|
json |
application/json |
{upload name}-extraction.json |
formatVersion (1), workspace, exportedAt, job - exactly what GET /v1/extractions/{jobId} answers - and sheets, each exactly what GET .../sheets/{n} answers. Indented UTF-8; ± and ° are written as themselves. |
csv |
application/zip |
{upload name}-extraction-csv.zip |
sheets.csv (one row per sheet), one file per table type named by its path (revisionHistory.csv, bom.csv, holeTable.csv, application.csv, dashNumbers.csv) and generalNotes.csv. |
xlsx |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
{upload name}-extraction.xlsx |
A Cover tab (workspace, job, file, template, status, engine version, export time, how to read it), Sheets, a tab per table type (named by the template: Revision History, Bill of Materials, …) and Notes. |
The CSV is always a zip, even for a job whose drawings print no table: a program reading it finds the
same files every time, and a table no sheet printed is a file with its heading row only. Each file is
RFC 4180 (comma-separated, CRLF, a field quoted when it holds a comma, a quote or a line break) in UTF-8
with a byte-order mark, so Excel reads it as UTF-8. Times are ISO 8601 in UTC (2026-10-12T09:01:00Z).
Columns. The Sheets file and tab have Sheet, Sheet status, Sheet review, Engine version,
Completed at, then every single-valued field the job’s template keeps, in its order, headed by its
names (Part Number, Drawing Number, …, or the workspace’s own). A table or the notes the template leaves out
has no file or tab. A table’s file
and tab have Sheet, Sheet review, Row (1 for the table’s first row: the API’s rowIndex + 1), then
the template’s columns. Every column is there whether or not the job has a value for it, so the files’
shape depends on the template, never on what one drawing printed; a value at a path the template does not
name gets a column of its own after the template’s, headed by its path, so nothing read is ever dropped.
Every value is four columns side by side: the value as it stands (a reviewer’s correction when there is
one); - as read, what the engine read, filled only beside a correction; - review (autoAccepted,
needsReview, accepted, corrected); and - confidence (0 to 1; empty for a general note, which is not
scored). The notes are generalNotes.csv and the Notes tab: one row per note, sheet by sheet in printed order,
under Note. A value waiting for review is never
exported as if it were final. In the workbook, values are always text (1:2 stays a scale, 0012 keeps
its zeros, nothing is run as a formula), confidences are numbers, every tab has a frozen, filterable
heading row, a value waiting for review is shaded amber and a correction blue. In the CSV a value is
written exactly as the API returns it, except one a spreadsheet would run as a formula - it starts with
=, @, a tab, or a sign followed by more than a number - which gets a leading apostrophe; an amount such
as -, -.005 or +.002/-.000 is left alone.
Values only. A template whose exportLayout is valuesOnly - for an ERP import - has one column per field
instead of four: Sheet, Sheet review, then each field under its name, then Waiting for review. A value still
waiting for a person is left blank (shaded amber in the workbook) and its heading is named in Waiting for review, so
nothing unchecked is imported as final; a corrected value is the correction. A table’s file is the same, row by row.
No sheet is ever left out. The workbook’s cover names the template and its version.
Always current. The file says what the API says at the moment it is asked: a value a reviewer corrected after
the job finished is exported corrected. Two downloads of the same values differ only in exportedAt (and the
cover’s export time). Answers carry Cache-Control: no-store.
From the dashboard. A document’s Export menu offers the same three files, named for what arrives: Excel,
CSV files (zip) and JSON. One difference: a JSON file downloaded there names each sheet’s resultUrl as a full
address on the API (https://api.speccapture.com/v1/extractions/{jobId}/sheets/1), because a file kept away from
the API has nothing to resolve a path against; the API’s own download keeps the paths its answers give.
| Answer | When |
|---|---|
200 |
The job is completed or failed (a failed job exports its sheets with no values) |
400 invalid_request |
format is not json, csv or xlsx |
404 not_found |
No such job in the workspace - another workspace’s id answers the same |
409 not_finished |
The job is queued, splitting or extracting; the problem’s jobStatus says which |
501 test_mode_not_available |
An sc_test_ key, until test keys replay sample results |