v1 · Stable
SpecMake API
Upload technical documents, fetch structured product data, run compliance checks, all from your own code. REST + JSON, Bearer-token auth. Integrate SpecMake into your existing PIM, ERP, or DPP workflow.
Included from the Pilot plan, on Professional, Enterprise and Charter Customer plans too. Generate keys in your account settings.
Authentication
Every request to the API must include your API key in the Authorization header as a Bearer token:
curl https://www.specmake.com/api/v1/documents \
-H "Authorization: Bearer sk_live_••••••••••"Keys are shown once at creation and never displayed again. Lost keys must be revoked and replaced. Treat keys like passwords: never commit them to source control, never share them with third parties.
Timeouts & processing time
POST is synchronous. Plan for about 70s, and up to 300s.
The POST /documents call runs extraction, structuring, and (when enabled) audit in a single request: no polling, no callback. Typical responses land in about 70s; larger or denser PDFs can take up to 300s, the ceiling for one request. A document that cannot be read in one pass within that ceiling is stopped shortly before it, at about 290s, and answered 422 document_too_long (see Errors).
Default HTTP timeouts in popular integration tools are too short. Before your first call, raise your client’s read timeout to at least 310 seconds:
- Low-code automation platforms typically default to 30–40s; raise to 310s in the HTTP module settings.
curl: pass--max-time 310.- Node
fetch: no default timeout, but reverse proxies and serverless platforms may impose one; verify yours. - Pass
run_audit=falseto skip the audit stage for a ~30% latency reduction when you don’t need the coverage signal. - Send an
Idempotency-Keyheader, so a retry after a timeout is recognised instead of processing the file twice (below).
Retries and Idempotency-Key
Add Idempotency-Key: <your id> to a POST /documents call: 1 to 255 printable ASCII characters, one per document you mean to process (a UUID works). Keys are scoped to your API key and remembered for 24 hours. A second request with the same key, the same file and the same options:
- while the first is still running, answers
409 idempotency_in_progresswith aRetry-Afterheader; - after a first request with
save_to_library=truefinished, answers200with the saved document, the headerIdempotent-Replayed: trueandmeta.replayed: true. The replay is read from your library: it carries no token or cost fields, because nothing was processed, and the audit is the saved one. If that read fails, it answers500 internal_error; retry with the same key. If the saved document has since been deleted, it answers409 idempotency_result_not_retained: send a new key to process the file again; - after a first stateless request finished, answers
409 idempotency_result_not_retained: stateless results are not stored, so send a new key to process the file again; - after a first request that failed, processes the file: a failure releases the key. If the release itself fails, the key answers
409 idempotency_in_progressuntil about 330s after the first request started, then processes the file.
The same key with a different file or different options answers 422 idempotency_key_mismatch. Without the header nothing changes.
Async upload + webhook callback is on the v1.1 roadmap. Until then, a synchronous integration is the simplest, most reliable shape.
Rate limits
Limits are enforced per API key, per minute. Over-limit requests return 429 with a Retry-After header. Plan accordingly.
| Endpoint | Pilot / Professional | Enterprise / Charter |
|---|---|---|
| POST /documents | 10 / min | 20 / min |
| GET /documents/* | 300 / min | 300 / min |
| DELETE /documents/* | 60 / min | 60 / min |
Documents saved through the API count against your plan exactly like uploads in the app (SKUs under management on paid plans; the Trial’s document allowance). The rate limit is independent of that accounting.
Conventions
Versioning
Every response includes an X-API-Version: v1 header. Check this header in your client to detect breaking changes; we will bump it (and announce in advance) when v2 ships. The path prefix /api/v1 also stays for the entire v1 lifecycle.
Naming: snake_case vs camelCase
For historical reasons (our internal schema predates v1), the API uses two conventions in the same response:
- Top-level summary fields use snake_case:
file_type,source_language,detected_domain,created_at. - The nested
documentpayload uses camelCase:sourceLanguage,confidenceReason, etc. This mirrors the internal extraction schema. document.metadatauses snake_case: the keys come from source-document fields (page_count,domain_detected) and follow source convention.
The same data sometimes appears under both keys (e.g. source_language at the top level and document.sourceLanguage inside the payload). Either is canonical; pick whichever is convenient for your client.
Diagrams
When a saved document has extracted diagrams, each entry in document.diagrams[] carries an imageUrl (1-hour signed URL) plus storagePath, width, height, description, and type. The URL is short- lived; re-fetch the document if you need to ingest images after an hour.
Diagrams are extracted only for documents saved via the web UI (or with save_to_library=true via the API today; planned). A stateless API call with save_to_library=false returns an empty diagrams[].
Cost transparency
Every POST response includes the per-call AI cost in meta.tokens (input/output counts) and meta.estimatedCostCents (USD cents). This is the same value we log to our internal usage table; no markup. Use it to forecast your monthly spend or to budget per-document.
Endpoints
Documents
/api/v1/documentsUpload a PDF, DOCX, or XLSX. The document is extracted, structured into canonical JSON, and (optionally) audited. Stateless by default; pass save_to_library=true to persist.
Request
curl https://www.specmake.com/api/v1/documents \
-H "Authorization: Bearer sk_live_••••" \
-H "Idempotency-Key: 5f0c1f2e-spec-sheet-001" \
-F "file=@spec-sheet.pdf" \
-F "save_to_library=false" \
-F "run_audit=true"Form fields
file: PDF, DOCX, or XLSX, max 4.5 MB (required)save_to_library:true|false. When true, the document persists to your library at/products. A file byte-for-byte identical to one already saved in the same library (your team’s when you are on a team, otherwise your own) joins that product as a new version rather than adding a product, so pushing an unchanged file again adds no SKU. Default:false.run_audit:true|false. When false, skips the audit stage (~30% latency reduction). Default:true.
Response
{
"id": null, // only set when save_to_library=true
"document": {
"title": "...",
"sourceLanguage": "de", // camelCase inside document
"metadata": { "domain_detected": "hydraulics_pneumatics", ... },
"sections": [
{ "heading": "...", "fields": [
{ "key": "...", "value": "...", "unit": "bar",
"confidence": 0.95, "source": { "page": 2, "quote": "..." } }
]}
],
"diagrams": [] // empty unless save_to_library=true
},
"audit": {
"coverage_score": 87, // 0-100
"data_points_extracted": 47,
"total_data_points_in_source": 54,
"severity_counts": { "low": 6, "medium": 2 },
"findings": [ ... ]
} | null,
"meta": {
"fileType": "pdf",
"fileSize": 230423,
"fileName": "spec-sheet.pdf",
"detectedDomain": "hydraulics_pneumatics",
"processingMs": 31420,
"tokens": { "input": 10601, "output": 5936 },
"estimatedCostCents": 15.82, // USD cents, no markup
"auditSkipped": true // only when the audit was skipped or stopped
// because time ran out (not when run_audit=false
// or the audit failed); audit is then null
}
}/api/v1/documentsList saved documents, newest first. By default the documents you uploaded, like the product library; ask for your team's with scope.
curl "https://www.specmake.com/api/v1/documents?scope=all&limit=50" \
-H "Authorization: Bearer sk_live_••••"
# the next page: pass next_cursor back as cursor, with the same scope
curl "https://www.specmake.com/api/v1/documents?scope=all&limit=50&cursor=eyJjIjoiMjAyNi0wNS0x..." \
-H "Authorization: Bearer sk_live_••••"Query params
limit: 1 to 100. Default 25.scope:mine(default, the documents uploaded by the key’s user) |team(every document of your team, a teammate’s included) |all(both).teamanswersnot_on_teamfor an account without a team.cursor: thenext_cursorof the previous page. Treat it as an opaque string: pass it back unchanged, URL-encoded, with the samescope.before: kept for existing clients. Takes anext_cursorvalue, or an ISO 8601 timestamp or date (documents created strictly before it; a date means midnight UTC, a time without a zone is read as UTC). Pass eithercursororbefore, not both.
Response
{
"documents": [
{ "id": "...", "filename": "spec.pdf", "file_type": "pdf",
"file_size": 230423, "source_language": "de",
"detected_domain": "hydraulics_pneumatics",
"status": "completed", "created_at": "2026-05-16T...",
"owned_by_caller": true }, // false for a teammate's document
...
],
"has_more": true,
"next_cursor": "eyJjIjoiMjAyNi0wNS0x..." // opaque; null on the last page
}Pages follow upload time, then document id, so walking next_cursor until has_more is false returns each document once, including documents uploaded in the same instant. Each version of a product is its own document, so the list counts documents, not products under management. Deleted documents are not listed; to mirror deletions, compare the ids you hold with a full walk.
/api/v1/documents/:idFetch one document with full structured_data + audit_data. Diagrams come back with fresh 1h signed URLs; audit includes derived severity_counts.
curl https://www.specmake.com/api/v1/documents/abc-123 \
-H "Authorization: Bearer sk_live_••••"
# with the compliance cross-check and the DPP readiness score
curl "https://www.specmake.com/api/v1/documents/abc-123?include=compliance,dpp" \
-H "Authorization: Bearer sk_live_••••"Query parameters
include: comma list ofcomplianceand/ordpp. Adds the same regulatory cross-check and Digital Product Passport score the product shows, computed for your plan’s compliance modules (a regulation in a module your plan does not include is listed undercompliance.locked, never scored). Omitted keys are absent, so existing clients see an unchanged payload.
Response
{
"id": "abc-123",
"filename": "spec.pdf",
"file_type": "pdf", // snake_case at top level
"file_size": 230423,
"source_language": "de",
"detected_domain": "hydraulics_pneumatics",
"status": "completed",
"created_at": "2026-05-16T12:00:00Z",
"document": { // camelCase inside
"title": "...",
"sourceLanguage": "de",
"metadata": { ... },
"sections": [ ... ],
"diagrams": [
{ "type": "application_photo",
"width": 2000, "height": 2000,
"description": "Figure 1",
"storagePath": "uuid/0.jpg",
"imageUrl": "https://...signed...?expires=..." } // 1h TTL
]
},
"audit": {
"coverage_score": 87,
"data_points_extracted": 47,
"total_data_points_in_source": 54,
"severity_counts": { "low": 8, "medium": 6 },
"findings": [ ... ]
} | null,
"compliance": { // only with include=compliance
"ruleset_version": "2026.09.1",
"overall_score": 79,
"applicable": [
{ "id": "ce", "short_name": "CE", "name": "CE Marking", "score": 83,
"counts": { "pass": 5, "fail": 1, "warning": 0, "not_checked": 0 },
"requirements": [
{ "id": "ce_standards", "label": "...", "status": "fail",
"severity": "mandatory", "detail": "...", "matched_field": "..." }
] }
],
"not_applicable": [ "machinery", "voc_paints", "cpr" ],
"locked": [ { "id": "atex", "short_name": "ATEX", "module": "atex" } ],
"disclaimer": "Documentation readiness ... Not a legal assessment."
},
"dpp": { // only with include=dpp; null when no DPP category applies;
// { "locked": "dpp", "note": "..." } when the plan lacks the DPP module
"category": "batteries", "display_name": "Batteries (EU Battery Regulation)",
"regulation": "Regulation (EU) 2023/1542", "mandatory_from": "18 February 2027",
"coverage_percent": 31, "found_required": 10, "total_required": 32,
"missing_required": [ { "id": "...", "label": "...", "description": "...", "cluster": "..." } ],
"found": [ { "id": "...", "label": "...", "matched_key": "..." } ],
"category_source": "auto", // or "override" when set on the product
"requirement_note": "..." // only when a caveat applies: a field list not yet fixed by an
// adopted act, a battery whose passport scope the sheet does
// not settle, or a battery category chosen by hand for a
// sheet that reads as out of scope
}
}MCP server (agents)
The same API is available to AI agents as a Model Context Protocol server: Claude Desktop, Claude Code or any MCP client can list your documents (and your team’s, page by page), read the extracted fields with their confidence and source page, ask which regulations apply, list the readiness gaps and upload a document. Every value comes from the API untouched; nothing is re-extracted by the agent. The server is a small folder we hand to pilot accounts today (two source files plus their dependencies; a packaged release follows); point your MCP client at it with your API key:
{
"mcpServers": {
"specmake": {
"command": "npx",
"args": ["tsx", "src/mcp/server.ts"],
"cwd": "/absolute/path/to/the/specmake-mcp/folder",
"env": { "SPECMAKE_API_KEY": "sk_live_••••" }
}
}
}Tools: list_documents, get_document, document_fields, readiness_gaps, which_regulations_apply, upload_document. Uploads are stateless unless the agent asks to save (a saved document counts as a product under management, exactly like the API). Same plan, module and rate limits as the REST API: a regulation or the DPP score in a module your plan does not include is reported as locked, never scored.
/api/v1/documents/:id/translationsList translations associated with a document. Translations are created via the web UI; the API exposes them for read.
curl https://www.specmake.com/api/v1/documents/abc-123/translations \
-H "Authorization: Bearer sk_live_••••"/api/v1/documents/:idDelete a document (cascades translations, diagrams, source file). Members can delete their own docs; team admins/owners can delete any team doc.
curl -X DELETE https://www.specmake.com/api/v1/documents/abc-123 \
-H "Authorization: Bearer sk_live_••••"Errors
All errors return JSON of the shape { "error": "<code>", "message"?: "<human>" }.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or malformed parameters. |
| 400 | invalid_cursor | The list’s cursor or before is neither a next_cursor value nor an ISO 8601 date or timestamp. |
| 400 | not_on_team | The list was asked for scope=team by an account that is not on a team. |
| 400 | invalid_file | File failed magic-byte validation. |
| 400 | file_too_large | File exceeds 4.5 MB. |
| 400 | unsupported_file_type | Only PDF, DOCX, and XLSX are supported. |
| 400 | too_many_pages | The PDF has more pages than one extraction can read (600). Refused before processing; split it into smaller files. |
| 400 | request_too_large | The PDF and its text are too large to read in one pass. Refused before processing; split it or export it with compressed images. |
| 400 | archive_too_large | The DOCX or XLSX expands to more than 100 MB once unpacked. Refused before processing and not counted against your plan; split it, or send only the sheets you need. |
| 400 | archive_unsafe | The DOCX or XLSX is packed in a way that cannot be unpacked safely (an extreme compression ratio or thousands of parts). Refused before processing and not counted against your plan; save it again from Word or Excel and send the new copy. |
| 400 | invalid_idempotency_key | The Idempotency-Key header is empty, longer than 255 characters or not printable ASCII. |
| 400 | document_too_dense | The file reads as a selection catalog (many product variants at once) and is refused before processing, so it does not count against your plan. Split it by product family or model range and upload those separately. |
| 401 | unauthorized | Missing, malformed, or revoked API key. |
| 403 | plan_required | API access is included from the Pilot plan. |
| 403 | insufficient_permissions | Team role doesn’t allow this action (viewers cannot upload). |
| 403 | catalog_limit_reached | Your SKUs under management are over the catalog size your plan allows. Response includes used and limit. Contact us to add capacity. |
| 403 | processing_paused | Processing is paused on this account by an automated safety check. Our team has been notified; contact support to resume. |
| 403 | quota_exceeded | Monthly document cap reached. Response includes used, limit, and resets_on. |
| 404 | not_found | Document doesn’t exist or isn’t visible to you. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. Check Retry-After. |
| 409 | idempotency_result_not_retained | The key completed without save_to_library (stateless results are not stored), or the document it saved has since been deleted. Send a new key to process again. |
| 422 | idempotency_key_mismatch | The key was used for a different file or different options. Use a new key. |
| 422 | document_too_long | The document could not be read in one pass: it was stopped shortly before the time limit, at about 290s, or its extraction outgrew what one pass can return, which a dense document can reach well before that. The message says when nothing was counted against your plan. We release the Idempotency-Key so a retry with it runs again; if that release fails, the key answers 409 idempotency_in_progress until about 330s after the first request started. Split it into smaller files (one product family each works best) and send those: retrying the same file can take just as long. |
| 429 | rate_limited | Slow down. Check Retry-After. |
| 429 | upstream_rate_limited | Upstream provider is rate-limiting us. Try again shortly. |
| 500 | internal_error | Something failed on our side. Retry; it never means the document is gone (that is 404). |
| 500 | processing_failed | Extraction or audit failed unexpectedly. Retry. |
| 503 | service_unavailable | Temporarily unavailable. We’ve been alerted; try again shortly. |
| 503 | upstream_overloaded | Upstream provider is overloaded. Try again shortly. |
What’s on the roadmap
v1 covers the core: upload, fetch, list, delete. Coming next:
- Webhooks: subscribe to
document.processed/document.failedevents instead of polling. - Translation endpoint: trigger translation jobs programmatically.
- Compliance evidence packet endpoint: generate the PDF artifact via API.
- SDKs: Python and Node thin wrappers when customer demand justifies.
Need something not listed? Get in touch.