Following a job
A job’s status says where the whole file is:
status |
What it means |
|---|---|
queued |
Accepted and paid for; waiting to be split into its sheets. |
splitting |
Being split into its sheets. |
extracting |
Its sheets are being read, each on its own. |
completed |
Every sheet has finished: read, or failed and refunded. |
failed |
The file could not be split into sheets, and everything it held was refunded. failureReason says why, in words for a person. |
The job’s templateId and templateVersion name the template its values are kept with - drawing and 1 for the
standard template - and templateName what the workspace calls it (null for drawing); see
Templates.
Each entry of the job’s sheets has a status of its own - queued, running, completed, or failed (the
sheet could not be read, and its credits were refunded) - and, once read, its reviewState and the resultUrl
its values are at. progress counts the sheets completed, failed and needing review, and credits says what the
job has reserved, spent, refunded and still holds.
Ask for the job
Section titled “Ask for the job”GET /v1/extractions/{jobId} until its status is completed or failed:
#!/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
while true; do job=$(curl -sS --fail-with-body "$API/v1/extractions/$JOB_ID" \ -H "$AUTH") || { echo "$job" >&2; exit 1; } jq -r '"\(.status): \(.progress.sheetsCompleted) of \(.sheetCount) sheets read"' <<<"$job" if [[ "$(jq -r '.status' <<<"$job")" =~ ^(completed|failed)$ ]]; then break; fi sleep 5done
jq -r '"\(.progress.sheetsNeedingReview) sheets need a person to look; \(.credits.refunded) credits refunded"' <<<"$job"Save as wait.sh, set SPECCAPTURE_API_KEY, and run: bash wait.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
JsonElement job;while (true) { job = await Json(await Call(HttpMethod.Get, $"/v1/extractions/{jobId}")); Console.WriteLine($"{job.GetProperty("status")}: {job.GetProperty("progress").GetProperty("sheetsCompleted")} of {job.GetProperty("sheetCount")} sheets read"); if (job.GetProperty("status").GetString() is "completed" or "failed") break; await Task.Delay(TimeSpan.FromSeconds(5));}
Console.WriteLine($"{job.GetProperty("progress").GetProperty("sheetsNeedingReview")} sheets need a person to look; {job.GetProperty("credits").GetProperty("refunded")} credits refunded");
// 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 wait.cs, set SPECCAPTURE_API_KEY, and run: dotnet run wait.cs -- 7c9e6679-7425-40de-944b-e07fc1f90ae7. Needs .NET 10 or later.
import osimport sysimport time
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
while True: job = call("GET", f"/v1/extractions/{job_id}").json() print(f"{job['status']}: {job['progress']['sheetsCompleted']} of {job['sheetCount']} sheets read") if job["status"] in ("completed", "failed"): break time.sleep(5)
print(f"{job['progress']['sheetsNeedingReview']} sheets need a person to look; {job['credits']['refunded']} credits refunded")Save as wait.py, set SPECCAPTURE_API_KEY, and run: python wait.py 7c9e6679-7425-40de-944b-e07fc1f90ae7. Needs Python 3.9+ and requests (pip install requests).
import { setTimeout as sleep } from "node:timers/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
let job;while (true) { job = await call("GET", `/v1/extractions/${jobId}`).then((response) => response.json()); console.log(`${job.status}: ${job.progress.sheetsCompleted} of ${job.sheetCount} sheets read`); if (["completed", "failed"].includes(job.status)) break; await sleep(5000);}
console.log(`${job.progress.sheetsNeedingReview} sheets need a person to look; ${job.credits.refunded} credits refunded`);
// 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 wait.mjs, set SPECCAPTURE_API_KEY, and run: node wait.mjs 7c9e6679-7425-40de-944b-e07fc1f90ae7. Needs Node.js 20 or later.
Every 5 seconds is plenty: a sheet takes tens of seconds, and every request counts against the key’s
rate limit. There is no need to wait for the whole file: a sheet whose own status is
completed can be read at its resultUrl straight away, while the rest are still being read.
To find jobs you did not keep the id of - a drawing uploaded in the dashboard, or everything finished since you last
looked - list them: GET /v1/extractions answers the workspace’s jobs newest first, a page at a time
(Lists and paging), each as GET /v1/extractions/{jobId} answers it but without its sheets
(progress says how they stand, and url is where the job is). status keeps only jobs in one status, and
fileName only those whose file name contains it, in any case: ?status=completed&createdAfter=2026-10-14T00:00:00Z
is every job finished since that day began.
Or let a webhook tell you
Section titled “Or let a webhook tell you”Instead of asking, add a webhook endpoint (POST /v1/webhooks, or Webhooks in the dashboard), and
SpecCapture posts a signed event to it when each job finishes: extraction.completed once every sheet has
finished, or extraction.failed when the file could not be split. The event carries the job exactly as
GET /v1/extractions/{jobId} returns it, and links to its exports. See Webhooks.
While reading is paused
Section titled “While reading is paused”Now and then reading pauses for a while: for every workspace, or for one workspace that has reached its daily
limit (a safety cap on how much each workspace can have read in one day), until 00:00 UTC. Nothing is lost and
nothing needs doing. Submissions are still accepted and their credits reserved as usual; a job waits unfinished
(queued, or extracting with its sheets queued), its credits stay held and none is spent on a sheet not yet
read; when reading resumes every sheet is read automatically, and the job finishes and its webhook is sent as
usual. Do not submit the file again: a new submission is a new job, and reserves its own credits. The dashboard
shows a notice on every page while a pause lasts, with the time it ends when that is known.