Skip to content
API console (try a request with your own key)Get early access

Submitting drawings

Send the drawing as multipart/form-data, with the file in a part named file, to POST /v1/extractions:

submit.sh
#!/usr/bin/env bash
set -euo pipefail
API=https://api.speccapture.com
AUTH="Authorization: Bearer $SPECCAPTURE_API_KEY"
DRAWING=bracket.pdf # the drawing to send: a PDF, TIFF, PNG, JPEG, BMP or WebP
job=$(curl -sS --fail-with-body "$API/v1/extractions" \
-H "$AUTH" \
-H "Idempotency-Key: $(openssl rand -hex 16)" \
-F "file=@$DRAWING") || { echo "$job" >&2; exit 1; }
jq -r '"Job \(.jobId) is \(.status): \(.sheetCount) sheets, \(.credits.reserved) credits reserved"' <<<"$job"

Save as submit.sh, set SPECCAPTURE_API_KEY, and run: bash submit.sh. Needs curl; scripts also use bash and jq.

A request takes one drawing. A form with a second part named file is refused whole with 400 invalid_request, and nothing in it is stored or charged - the first drawing included - so a drawing is never silently left unread. To send several, send a request for each (a loop over your files, each with its own Idempotency-Key), or upload them together in the SpecCapture dashboard, which takes several at once.

The answer is 202 Accepted with the job, and a Location header saying where to follow it (/v1/extractions/{jobId}). Nothing has been read yet: the job is queued, and its sheets already hold one entry for every page of the file, each waiting to be read. Send an Idempotency-Key header with every submission, so a retry after a dropped connection answers with the first job instead of making a second (Idempotency).

  • Formats: PDF, TIFF, PNG, JPEG, BMP or WebP, recognised from the file’s bytes, never from its name or extension.
  • Sheets: every page of a PDF and TIFF is a sheet of its own; a file in any other format is one sheet. One file may hold up to 200 sheets.
  • Size: up to 50 MB a file, and up to 150 megapixels a page: room for a D-size sheet scanned at 400 dpi, or an E-size or A0 sheet at 300 dpi. GET /v1/account states the limits your key works within as numbers: limits.maximumFileSizeMegabytes, limits.maximumSheetsPerFile and limits.maximumPageMegapixels (see Credits).

Each sheet costs 4 credits. A submission reserves them for every sheet of the file at once, before anything is read; each sheet’s credits are spent when it has been read, and a sheet that cannot be read is refunded. A file needing more credits than the workspace has available is refused whole, with 402 not_enough_credits saying how many it needs and how many there are, and nothing is stored or reserved. See Credits.

A template decides which of the values SpecCapture reads a job keeps, what each is called and in what order. Name one with the optional form fields templateId - drawing for every field under its standard names, or an id from GET /v1/templates - and templateVersion, a published version (its newest when left out):

Terminal window
curl https://api.speccapture.com/v1/extractions \
-H "Authorization: Bearer $SPECCAPTURE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@bracket.pdf" \
-F "templateId=6b0d5d2e-3c8a-4f6e-9a7b-2f1e0c9d8b7a"

Left out, the job uses the workspace’s default template. Every template reads the drawing the same way, so the choice never changes a value; the job’s templateId, templateVersion and templateName say which one it keeps its values with, for good. A template id the workspace does not have, or a version it never published, is 422 template_not_found, and nothing is stored or reserved.

Reading a sheet takes tens of seconds, so a submission never waits for it: the answer comes at once, and the job says when its sheets are ready. Ask for the job, or let a webhook tell you.

SpecCapture is not built for export-controlled technical data. Never send anything controlled under ITAR or the EAR (other than EAR99), anything classified or marked CUI, or anything carrying distribution statement B, C, D, E or F; the terms of service say it in full. Every drawing a key sends falls under its workspace’s confirmation of this: before the workspace’s first upload, or with its first API key, its owner or an admin ticks this sentence once in the dashboard, and SpecCapture records who and when:

I confirm that we will not upload export-controlled drawings (ITAR or EAR) to this workspace, and I understand that SpecCapture is decision support, not a substitute for checking the drawing.

The API itself never asks it and refuses nothing over it, so an integration that works keeps working, including one whose key was made before the dashboard asked (14 October 2026): its workspace is asked at its next upload or new key.

A drawing that says it is controlled is flagged, never refused. SpecCapture reads each sheet’s own export-control marking, as printed on the sheet and as read into titleBlock.exportControlMarking, and when it names Distribution Statement B, C, D, E or F, ITAR (or a USML category, or 22 CFR 120-130), the EAR (or an export classification number such as 9A610, or 15 CFR 730-774), CUI (or FOUO), NOFORN or a warning that the drawing’s export is restricted, the sheet carries markedControlled: true and controlledMarking naming which, on the job and on the sheet’s values, and the dashboard warns the workspace to delete it unless it is sure the drawing is not controlled. A proprietary notice, distribution statement A, EAR99 and a notice saying the drawing is not controlled are not flagged - ITAR CONTROLLED: NO, ITAR: N/A ECCN: EAR99, EXPORT CONTROLLED: [ ] YES [X] NO, THE ITAR DOES NOT APPLY - and a “not” denies only the control it governs: NOT ITAR CONTROLLED - SUBJECT TO THE EAR, ECCN 9A991 is flagged EAR.

A sheet the drawing itself says is export-controlled is not shared outside the workspace until a person kept it. Until the workspace’s owner or an admin keeps its drawing in the dashboard (controlledKept), a view link to a marked sheet is refused with 409 not_shareable, whose detail names the marking, and a link made before the sheet was marked shows only that it does not work; the dashboard’s Share does not offer the sheet and says why, and the MCP tools never suggest create_view_link for it. Once kept, the sheet is shared like any other, and its link’s page says nothing of the marking. And what the API returns is decision support: check values against the drawing before they reach a quote, an ERP or production, starting with those whose reviewState is needsReview.

A file is checked before anything is stored or charged, and the first check it fails answers: file_missing (the form has no part named file), invalid_request (the form has more than one part named file), unsupported_media_type (the body is not multipart/form-data), file_empty, not_a_drawing, unreadable_drawing, file_too_large, too_many_sheets or page_too_large. Then the template it names: invalid_request (a templateId or templateVersion that is not one) or template_not_found. A refusal for a limit names the limit in its detail and carries it as a number: maximumFileSizeMegabytes, maximumSheetsPerFile or maximumPageMegapixels. Then the workspace: workspace_deleting (its deletion has been asked for), trial_ended (its free trial has ended, and its credits with it), or not_enough_credits. Each is a problem with a stable code, and none of them stores the file or spends a credit. If the file cannot be stored once its job exists, the answer is 503 storage_unavailable: that job fails and is refunded in full within minutes, and the file can simply be sent again.