View links
A sheet’s result never carries a link to view it: a link exists only when you ask for one.
POST /v1/extractions/{jobId}/sheets/{sheetNumber}/view-links answers 201 with the link:
#!/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
link=$(curl -sS --fail-with-body "$API/v1/extractions/$JOB_ID/sheets/$SHEET_NUMBER/view-links" \ -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{"expiresInDays":30}') || { echo "$link" >&2; exit 1; }jq -r '"Anyone with this link sees the sheet until \(.expiresAt): \(.viewUrl)"' <<<"$link"jq -r '"To withdraw it: \(.viewLinkId)"' <<<"$link"Save as view_link.sh, set SPECCAPTURE_API_KEY, and run: bash view_link.sh. Needs curl; scripts also use bash and jq.
using System.Text;using System.Text.Json;using System.Text.Json.Nodes;
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
StringContent body = new(new JsonObject { ["expiresInDays"] = 30 }.ToJsonString(), Encoding.UTF8, "application/json");JsonElement link = await Json(await Call(HttpMethod.Post, $"/v1/extractions/{jobId}/sheets/{sheetNumber}/view-links", body));Console.WriteLine($"Anyone with this link sees the sheet until {link.GetProperty("expiresAt")}: {link.GetProperty("viewUrl")}");Console.WriteLine($"To withdraw it: {link.GetProperty("viewLinkId")}");
// 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 view_link.cs, set SPECCAPTURE_API_KEY, and run: dotnet run view_link.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
link = call( "POST", f"/v1/extractions/{job_id}/sheets/{sheet_number}/view-links", json={"expiresInDays": 30},).json()print(f"Anyone with this link sees the sheet until {link['expiresAt']}: {link['viewUrl']}")print(f"To withdraw it: {link['viewLinkId']}")Save as view_link.py, set SPECCAPTURE_API_KEY, and run: python view_link.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 link = await call("POST", `/v1/extractions/${jobId}/sheets/${sheetNumber}/view-links`, { body: JSON.stringify({ expiresInDays: 30 }), headers: { "Content-Type": "application/json" },}).then((response) => response.json());console.log(`Anyone with this link sees the sheet until ${link.expiresAt}: ${link.viewUrl}`);console.log(`To withdraw it: ${link.viewLinkId}`);
// 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 view_link.mjs, set SPECCAPTURE_API_KEY, and run: node view_link.mjs 7c9e6679-7425-40de-944b-e07fc1f90ae7 1. Needs Node.js 20 or later.
viewUrl opens a read-only page showing that one sheet’s drawing with every value’s box, its confidence band,
whether it waits for a person, and where it came from - to anyone holding the link, with no sign-in, until
expiresAt or until it is withdrawn. It is what your ERP or quoting tool can show as “view on drawing” beside
each value.
- Opening it on one value: add
?field=and the value’s path:{viewUrl}?field=titleBlock.partNumberworks as it stands. - Lifetime:
expiresInDaysfrom 1 to 90, or 30 when left out, and never later than the workspace keeps the drawing:expiresAtmay be sooner than asked. A link stops working when the drawing is no longer kept (How long the drawing is kept). - Who may make one: a live key acting as the workspace’s owner or an admin. A test key is refused with
403 live_key_required, a key acting as a member with403 role_not_allowed. - Refusals: another workspace’s job, or a sheet number past the job’s
sheetCount, answers404 not_found; a sheet not read yet - from the moment the job is submitted, as the job lists it - a drawing past the workspace’s retention period (the job’sdrawingPurgedAt), a workspace that keeps no drawings, or a sheet marked export-controlled (markedControlled) that its workspace has not kept (controlledKeptisnull), answers409 not_shareable, whosedetailsays which. A409for a sheet not read yet is worth asking again once the sheet iscompleted, and one for a marked sheet once the workspace has kept its drawing; the others, and a404, never change. - Withdrawing it:
DELETE /v1/view-links/{viewLinkId}stops it at once; the page then says only that the link has expired or was withdrawn. The workspace’s owner and admins also see every link in the dashboard - who made it, when it expires, how often and when it was last opened - and can withdraw any of them there. Asking for the workspace to be deleted withdraws every link. - Listing them:
GET /v1/view-linkslists every link the workspace has made, through the API or the dashboard, newest first and a page at a time (Lists and paging);GET /v1/view-links/{viewLinkId}reads one. Each says which sheet it shows (jobId,sheetNumber,fileName), itsstatus-active,expired(pastexpiresAt, or the drawing is no longer kept) orwithdrawn-createdAt,expiresAt,withdrawnAt, how often it was opened (openCount,lastOpenedAt) and who made it (createdBy: apersonor anapiKey, by name). Never the link itself: storeviewUrlwhen you make it.statusnarrows the list; a deleted job’s links are not listed. The same key as for making one: a live key acting as the owner or an admin. - What the page records: how many times it was opened and when it was last opened. No address, browser or person.
Withdrawing a link from your own code:
VIEW_LINK_ID=3b241101-e2bb-4255-8caf-4136c566a962 # the viewLinkId the link was made with
curl -sS -X DELETE "https://api.speccapture.com/v1/view-links/$VIEW_LINK_ID" \ -H "Authorization: Bearer $SPECCAPTURE_API_KEY"Paste into a terminal with SPECCAPTURE_API_KEY set. 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 viewLinkId = args[0]; // the viewLinkId the link was made with
await Call(HttpMethod.Delete, $"/v1/view-links/{viewLinkId}");
// 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 withdraw_view_link.cs, set SPECCAPTURE_API_KEY, and run: dotnet run withdraw_view_link.cs -- 3b241101-e2bb-4255-8caf-4136c566a962. 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
view_link_id = sys.argv[1] # the viewLinkId the link was made with
call("DELETE", f"/v1/view-links/{view_link_id}")Save as withdraw_view_link.py, set SPECCAPTURE_API_KEY, and run: python withdraw_view_link.py 3b241101-e2bb-4255-8caf-4136c566a962. 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 [viewLinkId] = process.argv.slice(2); // the viewLinkId the link was made with
await call("DELETE", `/v1/view-links/${viewLinkId}`);
// 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 withdraw_view_link.mjs, set SPECCAPTURE_API_KEY, and run: node withdraw_view_link.mjs 3b241101-e2bb-4255-8caf-4136c566a962. Needs Node.js 20 or later.