Parser API

Parse electronic-component datasheets into verified, page-cited facts, and read the published Coppr catalogue.

Keys come from the Coppr console. The examples use curl and Python.

At a glancev0.1.0
Base URL
https://api.copprlab.com
Key
Authorization: Bearer or X-API-Key
Scopes
jobs:write parses datasheets. parts:read reads the catalogue.
Built from
openapi.json 0.1.0, fetched 2026-11-04

01Quick start

Parse a datasheet in four calls. You need a key with the jobs:write scope. Create one in the Coppr console under API keys. The console shows a key once, when you create it.

  1. Set your key

    shell
    export COPPR_API_KEY=cpr_live_XXXXXXXX

    A placeholder. A real key is cpr_ followed by 43 characters. Keep it out of source control.

  2. Submit the datasheet

    curl
    curl -sS -X POST https://api.copprlab.com/v1/jobs \
      -H "Authorization: Bearer $COPPR_API_KEY" \
      -F file=@RK73H.pdf \
      -F manufacturer="KOA Speer" \
      -F mpn=RK73H

    The API answers at once with 202 and the job id. Keep job_id; the job is already queued.

    response
    {
      "job_id": "9e0a176b5ae74ab0b51d69580b5a3b66",
      "status": "queued",
      "status_url": "/v1/jobs/9e0a176b5ae74ab0b51d69580b5a3b66"
    }
  3. Poll the status

    curl
    export JOB_ID=9e0a176b5ae74ab0b51d69580b5a3b66   # the job_id from the last step
    
    curl -sS https://api.copprlab.com/v1/jobs/$JOB_ID \
      -H "Authorization: Bearer $COPPR_API_KEY"

    status is queued, running, succeeded or failed. Polling every few seconds is fine. A two-page datasheet takes about 25 minutes when the OCR service starts cold; longer sheets take longer, and jobs queue behind each other.

  4. Read the result, then download the package

    curl
    curl -sS https://api.copprlab.com/v1/jobs/$JOB_ID/result \
      -H "Authorization: Bearer $COPPR_API_KEY"
    
    curl -sS -o RK73H-coppr-export.zip https://api.copprlab.com/v1/jobs/$JOB_ID/export \
      -H "Authorization: Bearer $COPPR_API_KEY"

    Every value in the result is labelled verified or needs_review, with the page it was read from (Results). The zip is the Coppr part package (The export zip).

The same in Python

The whole flow, with the API's own error body raised as an exception. It needs requests.

python
import os
import time

import requests

API = "https://api.copprlab.com"
HEADERS = {"Authorization": "Bearer " + os.environ["COPPR_API_KEY"]}


def check(response):
    """Return the response, or raise with the API's own detail and reason."""
    if response.ok:
        return response
    try:
        body = response.json()
    except ValueError:
        body = {"reason": "not_json", "detail": response.text[:200]}
    retry = response.headers.get("Retry-After")
    wait = f" (retry after {retry} s)" if retry else ""
    raise RuntimeError(f"{response.status_code} {body['reason']}: {body['detail']}{wait}")


with open("RK73H.pdf", "rb") as pdf:
    job = check(
        requests.post(
            f"{API}/v1/jobs",
            headers=HEADERS,
            files={"file": pdf},
            data={"manufacturer": "KOA Speer", "mpn": "RK73H"},
            timeout=120,
        )
    ).json()

while True:
    status = check(requests.get(API + job["status_url"], headers=HEADERS, timeout=30)).json()
    if status["status"] in ("succeeded", "failed"):
        break
    fraction = (status.get("progress") or {}).get("fraction")
    print(status["status"], status.get("step"), fraction)
    time.sleep(15)

if status["status"] == "failed":
    raise SystemExit(status["error"])

result = check(requests.get(f"{API}/v1/jobs/{job['job_id']}/result", headers=HEADERS, timeout=30)).json()
for fact in result["spec_values"]:
    print(fact["status"], fact["parameter"], fact["min"], fact["typ"], fact["max"], fact["unit"], "page", fact["page"])

package = check(requests.get(f"{API}/v1/jobs/{job['job_id']}/export", headers=HEADERS, timeout=120))
with open("RK73H-coppr-export.zip", "wb") as out:
    out.write(package.content)

02Authentication and scopes

Every /v1 route takes an API key from the developer console (copprlab.com), as Authorization: Bearer cpr_... or X-API-Key: cpr_.... A key is cpr_ followed by 43 characters; the service stores only its SHA-256. A key carries scopes:

scopeallows
parts:readGET /v1/parts, POST /v1/search, POST /v1/requirements/validate
jobs:writePOST /v1/jobs, GET /v1/jobs/{job_id}, GET /v1/jobs/{job_id}/result, GET /v1/jobs/{job_id}/export, GET /v1/jobs/{job_id}/source

A job is visible only to the key that submitted it. A revoked key may still be honoured for up to 30 seconds (an allow is remembered that long).

Create, name and revoke keys in the Coppr console.

03Limits

  • Per organization, per day (UTC): 2,000 requests and 10 parse jobs; across the service, 50 parse jobs. A submission the API refuses (a bad PDF, a missing field) does not count against the job quota.
  • Per key: 60 requests a minute. Per client address: 120 requests a minute.
  • Bodies: a PDF up to 40 MB, JSON up to 256 KB, each with a declared Content-Length.

04Errors

Every error is JSON: {"detail": "a sentence for a person", "reason": "a code for a program"}. A 429 and a 503 carry Retry-After (seconds); a daily quota's is the time to the next UTC midnight.

error body
{
  "detail": "a sentence for a person",
  "reason": "a code for a program"
}

Branch on reason; show detail to a person. Each route lists the reasons it can return in the reference.

ReasonStatusMeaning
invalid401No key, a malformed key, or a key the console never issued. Send Authorization: Bearer cpr_... with a key from the developer console.
revoked401The key was revoked in the developer console. Create a new key in the console.
insufficient_scope403The key lacks the scope the route needs (parts:read or jobs:write). Use a key with that scope, or add the scope in the console.
quota_requests429The organization's requests for the day (UTC) are used up. Wait for Retry-After seconds (the next UTC midnight).
quota_jobs429The key's or the organization's parse jobs for the day (UTC) are used up. Wait for Retry-After seconds (the next UTC midnight).
service_cap429The service's parse jobs for the day (UTC), across every organization, are used up. Wait for Retry-After seconds (the next UTC midnight).
rate_limited429More than 60 requests in a minute with one key. Wait for Retry-After seconds.
address_rate_limited429More than 120 requests in a minute from one client address. Wait for Retry-After seconds.
too_many_bad_keys429Too many invalid or revoked keys from one client address; it is turned away for a minute. Check the key, then wait for Retry-After seconds.
key_check_unavailable503The key could not be checked (the database is unreachable). Every key is refused until it can be: the API fails closed. Retry after Retry-After seconds.
bad_request400A required field is missing or malformed. Fix the request; detail says what.
bad_pdf400The upload or the URL is not a usable PDF (not a PDF, too large, or not fetchable). A refused submission does not count against the job quota. Send a PDF datasheet.
invalid_parameters422A query parameter or body field has the wrong type or is missing. detail lists each problem.
not_found404No published part with that MPN, or no job with that id for this key.
job_not_finished409The job has no result yet (queued or running). Poll the job's status.
job_failed409The job failed; detail carries the error. Submit it again, or another datasheet.
length_required411A request with a body must declare its Content-Length.
body_too_large413The body is larger than allowed (40 MB for a PDF, 256 KB for JSON).
catalogue_error502The catalogue read failed. Retry; report it if it persists.
catalogue_unavailable503The catalogue cannot be read right now. Retry shortly.

05The job lifecycle

POST /v1/jobs returns at once with a job id and a status_url. The job waits in a queue, then runs four steps in order, and ends succeeded or failed.

  1. queuedqueue_position says where: 1 is next.
  2. ingestPages, text, OCR, layout and the document index.
  3. extractTables, pins and ratings.
  4. figuresGraphs and drawings.
  5. verifyEvery value checked against its page.
  6. succeededresult_url and export_url are set.
  7. failederror says why.
How long it takes

A two-page datasheet takes about 25 minutes when the OCR service starts cold; longer sheets take longer, and jobs queue behind each other. One measured example: a 24-page datasheet took 74.5 minutes.

  • Two workers run one job each; other jobs wait in the queue.
  • A service restart fails any running job. Submit it again; nothing is half-kept.
  • A job never publishes to the catalogue. Publishing stays behind Coppr's release gates.
  • Only the key that submitted a job can read it. Any other key gets not_found.
  • A failed job answers job_failed from /result and /export. Submit it again, or another datasheet.

Reading the status

GET /v1/jobs/{job_id} is served from the job's own record. It gives the step running now and for how long, the steps already done with their durations, a progress estimate, and the latest events.

Example status of a running job
{
  "id": "9e0a176b5ae74ab0b51d69580b5a3b66",
  "status": "running",
  "step": "figures",
  "steps": [
    "ingest",
    "extract",
    "figures",
    "verify"
  ],
  "queue_position": null,
  "current_step": {
    "step": "figures",
    "started_at": "2026-11-02T18:13:07+00:00",
    "elapsed_seconds": 95
  },
  "completed_steps": [
    {
      "step": "ingest",
      "seconds": 577.5,
      "started_at": "2026-11-02T17:58:31+00:00",
      "finished_at": "2026-11-02T18:08:09+00:00"
    },
    {
      "step": "extract",
      "seconds": 296.6,
      "started_at": "2026-11-02T18:08:09+00:00",
      "finished_at": "2026-11-02T18:13:06+00:00"
    }
  ],
  "progress": {
    "fraction": 0.67,
    "estimated_seconds_remaining": 483,
    "basis": "each step's typical time on a two-page datasheet with the OCR service starting cold, scaled by this datasheet's pages once they are counted"
  },
  "events": [
    {
      "at": "2026-11-02T18:08:09+00:00",
      "step": "ingest",
      "text": "read 2 pages"
    },
    {
      "at": "2026-11-02T18:13:06+00:00",
      "step": "extract",
      "text": "3 tables on 2 pages"
    },
    {
      "at": "2026-11-02T18:13:06+00:00",
      "step": "extract",
      "text": "found 2 tables on p.2"
    }
  ],
  "manufacturer": "KOA Speer",
  "mpn": "RK73H",
  "category": "passive.resistor",
  "source_url": null,
  "source_filename": "RK73H.pdf",
  "pages": 2,
  "pdf_sha256": "4c1d0e3a9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d",
  "created_at": "2026-11-02T17:58:30+00:00",
  "started_at": "2026-11-02T17:58:31+00:00",
  "finished_at": null,
  "error": null
}
FieldTypeDescription
id requiredstring
status requiredstringqueued, running, succeeded or failed.
stepstring or nullThe step running now (ingest, extract, figures, verify), or done.
steps requiredarray of stringEvery step, in order.
queue_positioninteger or nullWhile queued: 1 is next.
manufacturer requiredstring
mpn requiredstring
categorystring or null
source_urlstring or null
source_filenamestring or nullThe uploaded file's name, or the URL's last segment.
pagesinteger or nullThe datasheet's page count, once ingest has counted it.
pdf_sha256string or nullSHA-256 of the datasheet parsed.
created_at requiredstring
started_atstring or null
finished_atstring or null
errorstring or nullWhy a failed job failed.
current_stepCurrentStep or nullWhile running: the step and its elapsed time.
completed_stepsarray of StepTimingThe steps finished, with durations.
progress requiredProgress
eventsarray of JobEventThe most recent events (at most 10).
result_urlstring or nullOnce succeeded.
export_urlstring or nullOnce succeeded: the export zip, a Coppr part package (with KiCad interop files where a land pattern was read).

current_step

FieldTypeDescription
step requiredstringingest, extract, figures or verify.
started_atstring or null
elapsed_seconds requiredinteger

completed_steps

FieldTypeDescription
step requiredstring
secondsnumber or nullHow long the step took.
started_atstring or null
finished_atstring or null

progress

An estimate, not a promise. basis says how it was made.

FieldTypeDescription
fraction requirednumber0 to 1: an estimate from each step's typical time, scaled by the datasheet's pages; 1 once succeeded.
estimated_seconds_remaininginteger or nullAn estimate; none once failed.
basis requiredstringHow the estimate is made.

events

FieldTypeDescription
at requiredstring
step requiredstring
text requiredstringWhat a finished step found, e.g. "found 3 tables on p.13".

06Results

GET /v1/jobs/{job_id}/result answers once the job has succeeded. Before that it returns job_not_finished; after a failure, job_failed with the error in detail. A result is yours alone and is never published to the catalogue.

It holds the parsed values, the pins, the orderable codes, each package's body outline and land-pattern pads in millimetres, and a manifest of what the export zip holds. Every value carries a status, verified or needs_review, and the page it was read from. Concepts explains what the statuses mean.

FieldTypeDescription
schema requiredstringcopper-parser-api-result/v1.
mpn requiredstring
manufacturer requiredstring
categorystring or null
labels requiredobjectHow many items carry each status.
by_kind requiredobjectPer kind of item (spec_value, table, graph ...), how Layer 4 settled them.
spec_values requiredarray of SpecValue
pins requiredarray of objectpackage, number, name, status.
orderables requiredarray of objectcode, package, lifecycle, status.
packagesarray of PackageGeometryEach package read: body outline and land-pattern pads in mm, with status.
exportExportManifest or nullWhat GET /v1/jobs/{job_id}/export holds.
other_items requiredobjectCounts of the other items (graphs, tables, drawings) by status.
note requiredstring

spec_values

FieldTypeDescription
sectionstring or nullabsolute_maximum_ratings, recommended_operating_conditions, electrical_characteristics ...
parameterstring or nullAs printed.
symbolstring or null
conditionstring or nullThe printed test condition.
minstring or nullAs printed, in unit.
typstring or null
maxstring or null
ratingstring or nullA printed value that is not a min/typ/max.
unitstring or null
pageinteger or nullThe datasheet page it was read from (1-based).
status requiredstringverified or needs_review.

Packages and land patterns

Each entry of packages is one package the parse read: its body outline, and the manufacturer's recommended land pattern as pads. All lengths are in millimetres. Pad positions are measured from the package centre with y pointing down, so they can be drawn as SVG without a flip.

When a land pattern is verified

A land pattern is verified only when two independent readings of the drawing agree with the datasheet page. Otherwise it stays needs_review, and open_checks names the dimension that did not settle.

Today the rule covers two-terminal chip packages: resistors, non-polarized capacitors, and inductors with a series land-pattern table. Every other package kind keeps its land pattern at needs_review for now. Terminal positions are left unknown when the datasheet does not give them; they are never invented.

Example package
{
  "kind": "two_terminal_package",
  "status": "verified",
  "package": "NRS4018T2R2MDGJ",
  "drawing_identifier": "NR 4018 NRS4018",
  "page": 3,
  "body": {
    "length_mm": 4.0,
    "width_mm": 4.0,
    "height_mm": 1.8,
    "shape": "rect",
    "centre": [
      0.0,
      0.0
    ]
  },
  "pads": [
    {
      "number": "1",
      "x_mm": -1.4,
      "y_mm": 0.0,
      "width_mm": 1.2,
      "height_mm": 3.7,
      "shape": "rect",
      "drill_mm": null,
      "status": "verified"
    },
    {
      "number": "2",
      "x_mm": 1.4,
      "y_mm": 0.0,
      "width_mm": 1.2,
      "height_mm": 3.7,
      "shape": "rect",
      "drill_mm": null,
      "status": "verified"
    }
  ],
  "open_checks": []
}
FieldTypeDescription
kind requiredstringtwo_terminal_package (body and land pattern) or package_topology (terminals only).
status requiredstringverified or needs_review.
packagestring or null
drawing_identifierstring or nullThe size row or package name as printed.
pageinteger or nullThe page the package table or drawing is on.
bodyBody or null
padsarray of PadThe manufacturer's recommended land pattern; empty when the datasheet gives none the parser read.
open_checksarray of stringThe checks that did not settle, named.

body

FieldTypeDescription
length_mm requirednumberAlong the terminal axis (x).
width_mm requirednumber
height_mm requirednumberThe printed maximum.
shape requiredstringrect, centred on the origin.
centre requiredarray of number

pads

FieldTypeDescription
number requiredstringThe terminal the pad lands (1, 2 for a two-terminal part: local identities).
x_mm requirednumberPad centre from the package centre, along the terminal axis.
y_mm requirednumberPad centre from the package centre, y down.
width_mm requirednumberSize along x.
height_mm requirednumberSize along y.
shape requiredstringrect.
drill_mmnumber or nullA through-hole pad's drill; none for a surface pad.
status requiredstring

07The export zip

GET /v1/jobs/{job_id}/export returns a finished job as one zip: the Coppr part package. The file is named <mpn>-coppr-export.zip. Like the result, it is yours alone.

What is in it

A zip of what the parse found:

  • part.json: every value as printed (with its unit), pins, orderable codes, graphs (with their curves) and packages, each with its status and page anchor (page, table, printed line, bounding box in PDF points);
  • facts.csv: the values, one per row (section, parameter, symbol, condition, min, typ, max, rating, unit, status, page, table, printed);
  • README.md: the source citation (datasheet URL and SHA-256, pages, job), the Coppr part and how to bring it into Coppr, then the KiCad files, and what is missing or open;
  • coppr/ (the part, where the parse read a land pattern):
    • <mpn>.packet.json: the evidence-backed part packet Coppr ingests (copper library ingest-local), each value cited, with the parser's own verdict (producer_verification);
    • <mpn>.component.json: the Coppr component the pinned Coppr CLI built from it (library inspect): identity, the semantic symbol generated from pins, electrical types and functional groups (never drawn), the footprint with Coppr's verification state, and provenance. A land pattern the parse verified is a verified footprint; one with a value still open stays needs_review;
    • <mpn>.symbol.svg: the symbol as Coppr's own pipeline renders it (library render --glyph). The pinned CLI renders no footprints, so there is no footprint drawing;
  • coppr/ (the part as a symbol alone, where the parse read pins but no land pattern):
    • <mpn>.component-draft.json: a Coppr component draft (copper library new-component): identity and the pins, each with the electrical type and signal role (its functional group) the catalogue publishes for it. A pin nothing settled a type for is Coppr's unspecified, never a guess, and export.coppr.reason counts them;
    • <mpn>.component.json and <mpn>.symbol.svg, as above, with no footprint (export.coppr.footprint is none);
  • kicad/ (interop): a KiCad symbol (.kicad_sym) and footprint (.kicad_mod) Coppr's KiCad exporter made from the same component. A footprint still open is a draft stamped NEEDS REVIEW, and the README names what did not settle.

Without a land pattern there are no KiCad files, and without pins either no Coppr component; the README says why. No footprint and no pin type is ever invented.

GET /v1/jobs/{job_id}/source returns the exact PDF the job parsed. Its SHA-256 is the job's pdf_sha256, and exports of an uploaded datasheet cite it.

The manifest in the result

The result's export field says what the zip holds before you download it: the file names; coppr, whether the Coppr component is included and the state of its footprint (verified, needs_review, or none for a symbol-only component), with a reason for what is missing; and kicad, the same for the interop files.

Example export manifest
{
  "files": [
    "part.json",
    "facts.csv",
    "README.md",
    "coppr/NRS4018T2R2MDGJ.packet.json",
    "coppr/NRS4018T2R2MDGJ.component.json",
    "coppr/NRS4018T2R2MDGJ.symbol.svg",
    "kicad/NRS4018T2R2MDGJ.kicad_sym",
    "kicad/NR_4018_NRS4018_manufacturer_land_pattern_NRS4018T2R2MDGJ.kicad_mod"
  ],
  "coppr": {
    "included": true,
    "footprint": "verified",
    "reason": null
  },
  "kicad": {
    "included": true,
    "footprint": "verified",
    "reason": null
  }
}
FieldTypeDescription
files requiredarray of stringThe file names in the export zip.
coppr requiredobjectThe Coppr component: included, footprint (Coppr's own state of it: verified, needs_review, or none for a symbol-only component), and the reason for what is missing: the component, its footprint, its symbol drawing, or pins' electrical types.
kicad requiredobjectThe KiCad files (interop): included, footprint (verified or draft), and the reason when there are none.

KiCad library files, as interop

The Coppr part package is the primary output. Where the datasheet prints a land pattern the parse read, the zip also holds KiCad library files under kicad/: a symbol (.kicad_sym) and a footprint (.kicad_mod), with the packet they were made from. They are there for interop with KiCad.

The manifest's kicad object says whether they are included, whether the footprint is verified or a draft, and why not when they are missing. A draft footprint is stamped NEEDS REVIEW inside the file, and the README says which footprints are verified and which are drafts. Without a land pattern there are no KiCad files, and the README says why.

08Concepts

Statuses

Each fact, pin and parsed value carries a status:

  • verified: confirmed on the datasheet page it cites (by code against the page's text layer, or by an independent second reading of the page image).
  • needs_review: extracted and cited, but not confirmed; check it against the cited page before relying on it.
  • generic_verified (catalogue only): confirmed against a family- or series-level source rather than a datasheet for this exact part number.

Every catalogue fact carries the literal it was read from (printed) and its conditions; a published part revision carries the release it came from.

The catalogue's search returns only parts with nothing left to review, unless include_review is true. Today most parts still carry at least one review row, so most appear only with include_review.

Provenance

Every catalogue fact keeps the literal exactly as printed, and its conditions: the spec type (absolute maximum, recommended operating condition or electrical characteristic), the test condition, the printed parameter name, and the variant or device it applies to. Values are converted to SI units (V, A, H, F, ohm, Hz), and the printed literal is kept alongside.

In the catalogue each value is anchored to its page: the page number, a table id, and cell bounding boxes in PDF points. An export's part.json carries that anchor for each value. GET /v1/parts does not return page anchors yet. Parse-job results give each value's page.

Parse jobs and the catalogue

A parse job reads one datasheet for you and never publishes to the catalogue. The catalogue holds the parts Coppr has published, and the catalogue routes (parts:read) read it. A published part contains:

  • facts, with canonical units, conditions and status;
  • performance curves: digitized graphs, with their axes;
  • pins, with names and electrical types;
  • packages: body dimensions, terminals, and the manufacturer's land pattern where the datasheet prints one;
  • orderable part numbers, with package codes and lifecycle.

GET /v1/parts returns facts, pins, categories and orderables. Curves, packages and land patterns are in the catalogue but are not served by that route yet.

Not covered yet

  • Drawing-only datasheets, where ratings or dimensions appear only in drawing notes. Some connectors (XT60PW-M, USB4105) are refused with "no fact".
  • Chinese-only datasheets, where no specification rows are read. The manufacturer's English edition works.
  • Packages for two-terminal passives with no dimension table. They get pins 1 and 2, but no package.
  • Reading a multilayer ceramic capacitor's part number from the numbering page of a catalogue, and scanned series catalogues.

09Reference

Every route, built from the API's own openapi.json. Each route lists its scope, its parameters, and the error reasons it can return; each reason links to the error table.

Parse jobs

Submit a datasheet and read back what the parser found, each value labelled verified or needs_review. Scope jobs:write. A job runs its steps in order: ingest (pages, text, OCR), extract (tables, pins, ratings), figures (graphs and drawings), verify (every value checked against its page). A two-page datasheet takes about 25 minutes when the OCR service starts cold; longer sheets take longer, and jobs queue behind each other.

POST /v1/jobs jobs:write

Submit a datasheet for parsing

A PDF upload (multipart: file, manufacturer, mpn, optional category) or a URL (JSON: datasheet_url, manufacturer, mpn, optional category). Returns at once with the job id; poll status_url. Each accepted job counts against the key's and the organization's daily job quota and the service's; a refused submission does not.

Request body multipart/form-data · JobSubmitForm

FieldTypeDescription
file requiredfileThe PDF datasheet (up to 40 MB).
manufacturer requiredstring
mpn requiredstring
categorystring or null

Request body application/json · JobSubmitJSON

FieldTypeDescription
datasheet_url requiredstringAn https URL of the manufacturer's PDF datasheet.
manufacturer requiredstring
mpn requiredstringThe part number the datasheet is parsed for.
categorystring or nullA category code, if known (it guides the parse).

Response 202 · application/json · JobAccepted

Accepted and queued.

Example response
{
  "job_id": "9e0a176b5ae74ab0b51d69580b5a3b66",
  "status": "queued",
  "status_url": "/v1/jobs/9e0a176b5ae74ab0b51d69580b5a3b66"
}

Errors

GET /v1/jobs/{job_id} jobs:write

A job's status

Its status, the step running and for how long, the steps done with their durations, a progress estimate and the latest events; result_url and export_url once it has succeeded. Served from the job's own record, so polling every few seconds is fine. Only the key that submitted a job can read it.

Parameters

NameInTypeDescription
job_id requiredpathstringThe job_id the submission returned.

Response 200 · application/json · JobStatus

The job.

Example response
{
  "id": "9e0a176b5ae74ab0b51d69580b5a3b66",
  "status": "running",
  "step": "figures",
  "steps": [
    "ingest",
    "extract",
    "figures",
    "verify"
  ],
  "queue_position": null,
  "current_step": {
    "step": "figures",
    "started_at": "2026-11-02T18:13:07+00:00",
    "elapsed_seconds": 95
  },
  "completed_steps": [
    {
      "step": "ingest",
      "seconds": 577.5,
      "started_at": "2026-11-02T17:58:31+00:00",
      "finished_at": "2026-11-02T18:08:09+00:00"
    },
    {
      "step": "extract",
      "seconds": 296.6,
      "started_at": "2026-11-02T18:08:09+00:00",
      "finished_at": "2026-11-02T18:13:06+00:00"
    }
  ],
  "progress": {
    "fraction": 0.67,
    "estimated_seconds_remaining": 483,
    "basis": "each step's typical time on a two-page datasheet with the OCR service starting cold, scaled by this datasheet's pages once they are counted"
  },
  "events": [
    {
      "at": "2026-11-02T18:08:09+00:00",
      "step": "ingest",
      "text": "read 2 pages"
    },
    {
      "at": "2026-11-02T18:13:06+00:00",
      "step": "extract",
      "text": "3 tables on 2 pages"
    },
    {
      "at": "2026-11-02T18:13:06+00:00",
      "step": "extract",
      "text": "found 2 tables on p.2"
    }
  ],
  "manufacturer": "KOA Speer",
  "mpn": "RK73H",
  "category": "passive.resistor",
  "source_url": null,
  "source_filename": "RK73H.pdf",
  "pages": 2,
  "pdf_sha256": "4c1d0e3a9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d",
  "created_at": "2026-11-02T17:58:30+00:00",
  "started_at": "2026-11-02T17:58:31+00:00",
  "finished_at": null,
  "error": null
}

Errors

GET /v1/jobs/{job_id}/result jobs:write

A finished job's verified values

What the parse found, each value labelled verified or needs_review, with the page it was read from: values, pins, orderable codes, each package's body outline and land-pattern pads in mm, and what the export zip holds. Not published to the catalogue. (The example mixes two jobs' results to show every field.)

Parameters

NameInTypeDescription
job_id requiredpathstringThe job_id the submission returned.

Response 200 · application/json · JobResult

The result (trimmed in this example).

Example response
{
  "schema": "copper-parser-api-result/v1",
  "mpn": "RK73H",
  "manufacturer": "KOA Speer",
  "category": "passive.resistor",
  "labels": {
    "verified": 5,
    "needs_review": 3
  },
  "by_kind": {
    "spec_value": {
      "verified": 2,
      "corrected": 0,
      "model_confirmed": 0,
      "contradicted": 0,
      "unresolved": 0
    },
    "graph": {
      "verified": 3,
      "corrected": 0,
      "model_confirmed": 0,
      "contradicted": 0,
      "unresolved": 2
    }
  },
  "spec_values": [
    {
      "section": "electrical_characteristics",
      "parameter": "T.C.R.",
      "symbol": null,
      "condition": "+25°C/-55°C and +25°C/+125°C",
      "min": null,
      "typ": null,
      "max": null,
      "rating": null,
      "unit": null,
      "page": 2,
      "status": "verified"
    }
  ],
  "pins": [
    {
      "package": "NRS4018T2R2MDGJ",
      "number": "1",
      "name": "1",
      "page": 3,
      "status": "verified"
    },
    {
      "package": "NRS4018T2R2MDGJ",
      "number": "2",
      "name": "2",
      "page": 3,
      "status": "verified"
    }
  ],
  "orderables": [],
  "packages": [
    {
      "kind": "two_terminal_package",
      "status": "verified",
      "package": "NRS4018T2R2MDGJ",
      "drawing_identifier": "NR 4018 NRS4018",
      "page": 3,
      "body": {
        "length_mm": 4.0,
        "width_mm": 4.0,
        "height_mm": 1.8,
        "shape": "rect",
        "centre": [
          0.0,
          0.0
        ]
      },
      "pads": [
        {
          "number": "1",
          "x_mm": -1.4,
          "y_mm": 0.0,
          "width_mm": 1.2,
          "height_mm": 3.7,
          "shape": "rect",
          "drill_mm": null,
          "status": "verified"
        },
        {
          "number": "2",
          "x_mm": 1.4,
          "y_mm": 0.0,
          "width_mm": 1.2,
          "height_mm": 3.7,
          "shape": "rect",
          "drill_mm": null,
          "status": "verified"
        }
      ],
      "open_checks": []
    }
  ],
  "export": {
    "files": [
      "part.json",
      "facts.csv",
      "README.md",
      "coppr/NRS4018T2R2MDGJ.packet.json",
      "coppr/NRS4018T2R2MDGJ.component.json",
      "coppr/NRS4018T2R2MDGJ.symbol.svg",
      "kicad/NRS4018T2R2MDGJ.kicad_sym",
      "kicad/NR_4018_NRS4018_manufacturer_land_pattern_NRS4018T2R2MDGJ.kicad_mod"
    ],
    "coppr": {
      "included": true,
      "footprint": "verified",
      "reason": null
    },
    "kicad": {
      "included": true,
      "footprint": "verified",
      "reason": null
    }
  },
  "other_items": {
    "graph": {
      "verified": 3,
      "needs_review": 2
    },
    "table": {
      "needs_review": 1
    }
  },
  "note": "Parsed and verified against the datasheet's pages; not published to the Coppr catalogue. 'verified' values were confirmed on the page; 'needs_review' ones were not."
}

Errors

GET /v1/jobs/{job_id}/source jobs:write

The datasheet a job parsed

The exact PDF the job parsed (its SHA-256 is the job's pdf_sha256); exports cite it for an uploaded datasheet. Only the key that submitted the job can read it.

Parameters

NameInTypeDescription
job_id requiredpathstringThe job_id the submission returned.

Response 200 · application/pdf

The PDF.

Errors

GET /v1/jobs/{job_id}/export jobs:write

A finished job as a zip (JSON, CSV, README, the Coppr part, KiCad)

The files in the zip are described under The export zip.

Parameters

NameInTypeDescription
job_id requiredpathstringThe job_id the submission returned.

Response 200 · application/zip

The zip (Content-Disposition: attachment; filename=<mpn>-coppr-export.zip).

Errors

Catalogue

The published Coppr catalogue: parts, search, and requirement checks. Scope parts:read.

GET /v1/parts parts:read

Look up a published part by MPN

The published part's facts (with canonical units, conditions, the printed literal and status), pins, categories and orderable part numbers. The MPN is matched as published (the series or base part number, e.g. LMR16030, NRS4018T2R2MDGJ).

Parameters

NameInTypeDescription
mpn requiredquerystringThe manufacturer part number, e.g. LMR16030.

Response 200 · application/json · Part

The published part (trimmed in this example).

Example response
{
  "mpn": "LMR16030",
  "manufacturer": "Texas Instruments",
  "family": "LMR16030",
  "description": "LMR16030 SIMPLE SWITCHER® 60-V, 3-A Step-Down Converter With 40-µA IQ",
  "lifecycle_status": "unknown",
  "revision": {
    "id": 311,
    "label": null,
    "published_at": "2026-11-02T07:07:04.967699+00:00",
    "verification_status": "verified"
  },
  "categories": [
    {
      "category_code": "integrated_circuit.power_management.converter.buck_regulator",
      "assignment_role": "primary",
      "verification_status": "needs_review"
    }
  ],
  "facts": [
    {
      "key": "electrical.input_voltage.operating",
      "kind": "quantity",
      "min": 4.3,
      "typ": null,
      "max": 60,
      "unit": "V",
      "value": null,
      "status": "verified",
      "printed": "This allows self start-up when the input voltage is in the operation range: 4.3 V to 60 V.",
      "conditions": {
        "spec_type": "recommended_operating_condition",
        "test_condition": "when the EN pin is connected to the VIN pin",
        "printed_parameter": "Input voltage range"
      }
    }
  ],
  "pins": [
    {
      "pin_number": "2",
      "pin_name": "VIN",
      "electrical_type": "power_in",
      "signal_roles": [
        "power"
      ]
    }
  ],
  "orderables": [
    {
      "code": "LMR16030PDDA",
      "package_code": "DDA",
      "lifecycle": "active",
      "temperature_grade": "-40 to 125"
    }
  ]
}

Errors

POST /v1/search parts:read

Search published parts by category and requirements

Parts in the category whose published facts meet every requirement. Each result carries its per-requirement evaluation: matched, not matched, or not evaluable (only a typical value is printed). Check requirements with POST /v1/requirements/validate first: an unknown fact key excludes every part.

Request body application/json · SearchRequest

FieldTypeDescription
categorystring or nullA category code; parts in it are searched.
requirementsarray of Requirement
include_reviewbooleanAlso return parts whose facts, categories or pins include needs_review rows (most parts today). Without it, only parts with nothing left to review are returned. Default False.
limitinteger1 to 200. Default 50.

Response 200 · application/json · SearchResponse

The matching parts.

Example response
{
  "count": 1,
  "results": [
    {
      "mpn": "SRP4020TA-100M",
      "manufacturer": "Bourns",
      "description": "SRP4020TA Series - Shielded Power Inductors",
      "lifecycle_status": "unknown",
      "revision_id": 355,
      "categories": [
        {
          "code": "passive.inductor.power",
          "role": "primary"
        }
      ],
      "evaluation": [
        {
          "requirement_index": 1,
          "fact_key": "passive.inductance.nominal",
          "operator": "gte",
          "matched": true,
          "reason": "matched verified canonical fact",
          "observed": {
            "min": null,
            "typ": null,
            "max": 1e-05,
            "unit": "H",
            "original_literal": "SRP4020TA-100M | 10.0 | ±20 | 10 | 19 | 215 | 243 | 1.8 | 2.3"
          }
        }
      ]
    }
  ]
}

Errors

POST /v1/requirements/validate parts:read

Check requirements against the catalogue's vocabulary

Whether each requirement names a defined fact key, an operator its kind allows and a unit of its dimension, with the allowed operators and the fact's canonical unit.

Request body application/json · ValidateRequest

FieldTypeDescription
requirements requiredarray of Requirement
categorystring or nullCheck the keys against this category's vocabulary.

Response 200 · application/json · ValidateResponse

Per-requirement verdicts (here an operator the key does not allow).

Example response
{
  "valid": false,
  "requirement_count": 1,
  "invalid_count": 1,
  "schema_version": "copper-requirement-validation/v1",
  "requirements": [
    {
      "requirement_index": 0,
      "ok": false,
      "status": "unsupported_operator",
      "message": "Operator '>=' does not apply to a quantity fact.",
      "fact_key": "passive.inductance.nominal",
      "value_kind": "quantity",
      "unit": "H",
      "allowed_operators": [
        "eq",
        "gt",
        "gte",
        "lt",
        "lte",
        "range_contains"
      ]
    }
  ]
}

Errors

Service

Health, without a key.

GET /health no key

Service health

Whether the service is up. No key, no rate limit.

Response 200 · application/json · Health

The service is up.

Example response
{
  "status": "ok",
  "version": "0.1.0",
  "workers": 2
}