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-Key header, 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_progress with a Retry-After header;
  • after a first request with save_to_library=true finished, answers 200 with the saved document, the header Idempotent-Replayed: true and meta.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 answers 500 internal_error; retry with the same key. If the saved document has since been deleted, it answers 409 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_progress until 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.

EndpointPilot / ProfessionalEnterprise / Charter
POST /documents10 / min20 / min
GET /documents/*300 / min300 / min
DELETE /documents/*60 / min60 / 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 document payload uses camelCase: sourceLanguage, confidenceReason, etc. This mirrors the internal extraction schema.
  • document.metadata uses 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

POST/api/v1/documents

Upload 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
  }
}
GET/api/v1/documents

List 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). team answers not_on_team for an account without a team.
  • cursor: the next_cursor of the previous page. Treat it as an opaque string: pass it back unchanged, URL-encoded, with the same scope.
  • before: kept for existing clients. Takes a next_cursor value, 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 either cursor or before, 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.

GET/api/v1/documents/:id

Fetch 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 of compliance and/or dpp. 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 under compliance.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.

GET/api/v1/documents/:id/translations

List 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_••••"
DELETE/api/v1/documents/:id

Delete 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>" }.

400invalid_requestMissing or malformed parameters.
400invalid_cursorThe list’s cursor or before is neither a next_cursor value nor an ISO 8601 date or timestamp.
400not_on_teamThe list was asked for scope=team by an account that is not on a team.
400invalid_fileFile failed magic-byte validation.
400file_too_largeFile exceeds 4.5 MB.
400unsupported_file_typeOnly PDF, DOCX, and XLSX are supported.
400too_many_pagesThe PDF has more pages than one extraction can read (600). Refused before processing; split it into smaller files.
400request_too_largeThe PDF and its text are too large to read in one pass. Refused before processing; split it or export it with compressed images.
400archive_too_largeThe 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.
400archive_unsafeThe 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.
400invalid_idempotency_keyThe Idempotency-Key header is empty, longer than 255 characters or not printable ASCII.
400document_too_denseThe 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.
401unauthorizedMissing, malformed, or revoked API key.
403plan_requiredAPI access is included from the Pilot plan.
403insufficient_permissionsTeam role doesn’t allow this action (viewers cannot upload).
403catalog_limit_reachedYour SKUs under management are over the catalog size your plan allows. Response includes used and limit. Contact us to add capacity.
403processing_pausedProcessing is paused on this account by an automated safety check. Our team has been notified; contact support to resume.
403quota_exceededMonthly document cap reached. Response includes used, limit, and resets_on.
404not_foundDocument doesn’t exist or isn’t visible to you.
409idempotency_in_progressA request with this Idempotency-Key is still running. Check Retry-After.
409idempotency_result_not_retainedThe 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.
422idempotency_key_mismatchThe key was used for a different file or different options. Use a new key.
422document_too_longThe 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.
429rate_limitedSlow down. Check Retry-After.
429upstream_rate_limitedUpstream provider is rate-limiting us. Try again shortly.
500internal_errorSomething failed on our side. Retry; it never means the document is gone (that is 404).
500processing_failedExtraction or audit failed unexpectedly. Retry.
503service_unavailableTemporarily unavailable. We’ve been alerted; try again shortly.
503upstream_overloadedUpstream 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.failed events 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.