CircuitHub

API Reference

Complete reference for the CircuitHub Platform REST API.

Base URL: https://api.circuithub.com/v1

All endpoints require authentication via Bearer token unless otherwise noted.

curl -H "Authorization: Bearer $CIRCUITHUB_API_KEY" https://api.circuithub.com/v1/...

Health

GET /health

Check API health. No authentication required.

// Response: 200 OK
"ok"

Authentication

GET /auth/cli

Start the CLI login flow. Shows the signed-in user a page asking them to confirm signing in to the CLI. Served only at https://app.circuithub.com/platform/v1/auth/cli, where the browser's CircuitHub session is: a request with an Authorization header is refused.

ParameterInTypeDescription
redirect_uriquerystringRequired. Must be http://localhost:* or http://127.0.0.1:*

Response: 200 OK — the confirmation page. Confirming posts its form back to POST /auth/cli, which responds 303 See Other, redirecting to {redirect_uri}?token={JWT}, or {redirect_uri}?error=access_denied if the user cancels. The form can be posted only from the page, by the same user; otherwise the sign-in is refused. A refusal goes back to a local redirect_uri as {redirect_uri}?error={error}&error_description={reason}, where error is access_denied for a form not confirmed on the page, server_error for a failure at CircuitHub and otherwise invalid_request. Without a local redirect_uri, CircuitHub shows the user an error page. After ten minutes, posting the form shows the page again, saying it expired.

The JWT is valid for 24 hours.

This endpoint is used internally by circuithub auth login. You don't need to call it directly.

GET /auth/mcp

Start connecting an MCP client. Shows the signed-in user a page asking them to confirm connecting the named client and to choose what it may do: Read only, Engineering (preselected) or Full access (see Scopes). Served only at https://app.circuithub.com/platform/v1/auth/mcp, where the browser's CircuitHub session is: a request with an Authorization header is refused.

ParameterInTypeDescription
redirect_uriquerystringRequired. A CircuitHub MCP server callback; any other URL is refused
code_challengequerystringRequired. PKCE challenge
code_challenge_methodquerystringRequired. Must be S256
statequerystringRequired. Returned unchanged in the redirect
client_namequerystringRequired. The MCP client's name, shown to the user and recorded with the connection
scopequerystringThe scopes the MCP server will use at most, space-separated. The page offers only the choices within them, and none at all if they are empty: the server then learns only who the user is. Every choice includes read, so a non-empty limit without read (unknown names aside) is refused with 400 VALIDATION_ERROR. Naming every scope offers Full access, which, as when there is no limit, includes scopes added later. Unknown names are ignored. Every scope if not given
client_kindquerystringHow the MCP server identified the client, for the page's warning: known (an assistant known by its callback), local (a program on the user's computer) or unverified
client_hostquerystringWhere a local or unverified client's connection goes, which the page shows; required for those kinds and refused for others. At most 100 characters

Response: 200 OK — the confirmation page. Confirming posts its form back to POST /auth/mcp, which responds 303 See Other, redirecting to {redirect_uri}?code={code}&state={state}, or {redirect_uri}?error=access_denied&state={state} if the user cancels. As for the CLI, the form can be posted only from the page, by the same user, and shows the page again after ten minutes. A client_name or client_host with control or formatting characters is refused. The page names an MCP server other than CircuitHub's own, such as CircuitHub CRM, by its redirect_uri, from CircuitHub's configuration. A refused sign-in is sent back to {redirect_uri} as an OAuth error (error=invalid_request, access_denied for a confirmation that was not for this sign-in, or server_error for a failure at CircuitHub) with the reason in error_description and the state, whenever redirect_uri is a CircuitHub MCP server callback; otherwise CircuitHub shows the user an error page. A page shown again after ten minutes says so and keeps the user's choice.

The code is valid for two minutes.

POST /auth/mcp/token

Exchange a code from GET /auth/mcp for a connection token. Only the CircuitHub MCP server can call this: it sends the secret it shares with the API in the X-Connection-Client-Secret header, and any other caller is refused with 401 and code INVALID_CLIENT, without spending the code. A code can be presented once, whether or not the exchange then succeeds.

Request body:

{
  "code": "Xq3fH9kLm2pR7vT1wY5zB8nC4dG6jA0sE2uI7oK9lQ3",
  "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
  "redirect_uri": "https://mcp.circuithub.com/oauth/callback"
}

Response: 200 OK

{
  "token": "chc_N4vR8sT2wQ6yU0iO3pA7dF1gH5jK9lZ2xC6vB0nM4qE"
}

The token is a bearer credential (Authorization: Bearer chc_…) that acts as the user on this API, within the scopes they chose, until it is revoked or goes unused for 90 days. A wrong, used or expired code, a verifier that does not match the challenge, or a different redirect_uri is refused with 400 and code INVALID_GRANT.

POST /auth/mcp/revoke

Revoke a connection token. Only the CircuitHub MCP server can call this, with the same X-Connection-Client-Secret header.

Request body:

{
  "token": "chc_N4vR8sT2wQ6yU0iO3pA7dF1gH5jK9lZ2xC6vB0nM4qE"
}

Response: 204 No Content, whether or not the token existed.

POST /auth/mcp/introspect

Whose a connection token is and what it may do, for an MCP server that acts on the token itself or offers only what the token allows. Only the CircuitHub MCP server can call this, with the same X-Connection-Client-Secret header. It counts as using the token, so the 90 days start again.

Request body: as for POST /auth/mcp/revoke.

Response: 200 OK

{
  "active": true,
  "user_id": 1234,
  "scope": "read design issues"
}

scope names every scope the token holds, space-separated.

Once the token is revoked, has gone unused for 90 days, or never existed, the response is {"active": false}.

These endpoints are used by the CircuitHub MCP server. You don't need to call them directly.

Scopes

A connection token made through GET /auth/mcp may use only the endpoints within the scopes its user chose; every other endpoint refuses it with 403 and code INSUFFICIENT_SCOPE, naming the scope in requiredScope. The user can change a connection's scopes in CircuitHub, under Settings → Connected apps. Your own sign-in, such as the CLI's, is not limited by scopes. The OpenAPI document names each operation's scope under the connectionToken security scheme.

ScopeEndpoints
readEverything that only reads: workspaces, projects, revisions, BOMs, board settings, orders, issues, quotes, parts, shipping addresses, billing details, saved cards, invoices, statements, payments and balance
designRequesting quotes, choosing BOM parts, do-not-place, board settings and impedances, and creating projects and revisions, including their export classification
issuesRaising issues on orders, which emails CircuitHub staff and the order's followers
orderingPreviewing and placing orders, shipping options, and adding, changing and removing shipping addresses
billingChanging billing details, and adding, removing and choosing the default saved card
ChoiceScopes
Read onlyread
Engineeringread, design, issues
Full accessEvery scope, including any added later

A connection acts as a member of its user's workspaces only, never with CircuitHub staff powers.


Workspaces

GET /workspaces

List workspaces the authenticated user belongs to.

Response: 200 OK

{
  "workspaces": [
    {
      "id": 1,
      "slug": "acme-electronics",
      "name": "Acme Electronics"
    }
  ]
}
FieldTypeDescription
idintegerWorkspace ID
slugstringURL-safe identifier
namestringDisplay name

Projects

GET /projects

List projects accessible to the authenticated user.

ParameterInTypeDescription
workspacequerystringRequired. Workspace slug

Response: 200 OK

{
  "projects": [
    {
      "id": 12345,
      "name": "power-supply-board",
      "description": "24V to 5V buck converter",
      "createdAt": "2025-01-10T08:00:00Z",
      "updatedAt": "2025-03-01T14:30:00Z",
      "exportClassification": "EAR99",
      "latestRevisionId": 99001,
      "latestRevisionNumber": 3
    }
  ]
}

GET /projects/:projectId

Get a single project by ID.

Response: 200 OK

{
  "id": 12345,
  "name": "power-supply-board",
  "description": "24V to 5V buck converter",
  "createdAt": "2025-01-10T08:00:00Z",
  "updatedAt": "2025-03-01T14:30:00Z",
  "exportClassification": "EAR99",
  "latestRevisionId": 99001,
  "latestRevisionNumber": 3
}

POST /projects/upload-urls

Request presigned S3 upload URLs. Call this before creating a project or revision. You can request URLs for multiple files at once — for example, a design file and a BOM CSV.

Request body:

{
  "workspace": "acme-electronics",
  "files": [
    { "filename": "my-board.cvg", "size": 48210 },
    { "filename": "bom.csv", "size": 1024 }
  ]
}
FieldInTypeDescription
workspacebodystringRequired. Workspace slug
filesbodyarrayRequired. Files to upload
files[].filenamebodystringRequired. Filename
files[].sizebodyintegerRequired. File size in bytes

Response: 200 OK

{
  "uploads": [
    {
      "filename": "my-board.cvg",
      "uploadUrl": "https://circuithub-uploads.s3.amazonaws.com/...",
      "objectKey": "uploads/abc123/my-board.cvg",
      "validTill": "2025-03-15T11:00:00Z",
      "headers": [
        ["Content-Type", "application/octet-stream"],
        ["x-amz-acl", "bucket-owner-full-control"]
      ]
    },
    {
      "filename": "bom.csv",
      "uploadUrl": "https://circuithub-uploads.s3.amazonaws.com/...",
      "objectKey": "uploads/abc123/bom.csv",
      "validTill": "2025-03-15T11:00:00Z",
      "headers": [
        ["Content-Type", "application/octet-stream"],
        ["x-amz-acl", "bucket-owner-full-control"]
      ]
    }
  ]
}

Upload each file by sending a PUT request to its uploadUrl with the headers from the response and the file as the request body.

POST /projects

Create a new project from previously uploaded files. Include all files (design files and optional BOM CSV) in the files array.

Request body:

{
  "name": "power-supply-board",
  "workspace": "acme-electronics",
  "exportClassification": {"tag": "EAR99"},
  "files": [
    { "objectKey": "uploads/abc123/my-board.cvg", "filename": "my-board.cvg" },
    { "objectKey": "uploads/abc123/bom.csv", "filename": "bom.csv" }
  ]
}
FieldInTypeDescription
namebodystringRequired. Project name
workspacebodystringRequired. Workspace slug
exportClassificationbodyobjectRequired. {"tag": "NotExportControlled"}, {"tag": "EAR99"}, or {"tag": "ECCN", "contents": "<ECCN>"}
filesbodyarrayRequired. Uploaded file references
files[].objectKeybodystringRequired. S3 object key from upload-urls response
files[].filenamebodystringRequired. Original filename

Response: 200 OK

{
  "projectId": 12345,
  "revisionId": 98999,
  "statusUrl": "/v1/projects/12345/status"
}

exportClassification is required. Supply {"tag":"NotExportControlled"}, {"tag":"EAR99"}, or {"tag":"ECCN","contents":"3A001"} as appropriate for your design.

GET /projects/:projectId/status

Check the import status of a project.

Response: 200 OK

{
  "projectId": 12345,
  "status": "importing",
  "message": null
}
FieldTypeDescription
projectIdintegerProject ID
statusstring"importing", "complete", or "failed"
messagestring | nullError message when status is "failed"

GET /projects/:projectId/revisions

List all revisions for a project.

Response: 200 OK

{
  "revisions": [
    {
      "id": 99001,
      "number": 3,
      "createdAt": "2025-03-01T14:30:00Z",
      "importedAt": "2025-03-01T14:35:00Z",
      "edaTool": "kicad",
      "edaError": null
    }
  ]
}

POST /projects/:projectId/revisions

Create a new revision on an existing project from previously uploaded files.

Request body:

{
  "files": [
    { "objectKey": "uploads/def456/my-board-v2.kicad_pcb", "filename": "my-board-v2.kicad_pcb" },
    { "objectKey": "uploads/def456/bom.csv", "filename": "bom.csv" }
  ]
}
FieldInTypeDescription
filesbodyarrayRequired. Uploaded file references
files[].objectKeybodystringRequired. S3 object key from upload-urls response
files[].filenamebodystringRequired. Original filename

Response: 200 OK

{
  "projectId": 12345,
  "revisionId": 99002,
  "statusUrl": "/v1/projects/12345/status"
}

GET /projects/:projectId/bom

Get the Bill of Materials for a project's latest revision. This endpoint does not accept a revision selector. Check revisionId before changing BOM lines for the revision you intend to order.

Response: 200 OK

{
  "revisionId": 99001,
  "bomLines": [
    {
      "id": 1001,
      "value": "100nF",
      "footprint": "C_0402",
      "description": "Ceramic capacitor",
      "referenceDesignators": ["C1", "C2", "C3", "C4"],
      "status": "resolved",
      "resolvedPartMpn": "GCM155R71C104KA55D",
      "resolvedPartDescription": "100nF 16V X7R 0402",
      "resolvedManufacturer": "Murata"
    }
  ],
  "summary": {
    "total": 4,
    "resolved": 2,
    "unresolved": 1,
    "errored": 1
  }
}
FieldTypeDescription
revisionIdinteger | nullRevision the BOM belongs to; null while the project has no revision

BOM line fields:

FieldTypeDescription
idintegerBOM line ID
valuestring | nullComponent value
footprintstring | nullComponent footprint
descriptionstring | nullComponent description
referenceDesignatorsstring[]Reference designators
statusstring"resolved" | "unresolved" | "error"
resolvedPartMpnstring | nullMatched MPN
resolvedPartDescriptionstring | nullMatched part description
resolvedManufacturerstring | nullMatched manufacturer

Summary fields:

FieldTypeDescription
totalintegerTotal BOM lines
resolvedintegerMatched to a purchasable part
unresolvedintegerNo match found
erroredintegerResolution failed

PUT /bom/:bomLineId/part

Set the part for a BOM line. Use the BOM line id from GET /projects/:projectId/bom and a part id from GET /parts. The project is derived from the BOM line — no project ID is needed.

Request body:

{ "partId": 5001 }
FieldInTypeDescription
partIdbodyintegerRequired. Part ID from GET /parts

Response: 200 OK

{
  "partId": 5001,
  "mpn": "LM358DR",
  "manufacturer": "Texas Instruments"
}
FieldTypeDescription
partIdintegerPart ID that was set on the BOM line
mpnstringManufacturer part number
manufacturerstringManufacturer name

PUT /bom/:bomLineId/placements/:ref/dnp

Mark or unmark a placement as Do Not Place (DNP). A DNP placement will not be assembled or purchased. The project is derived from the BOM line — no project ID is needed.

Request body:

{ "dnp": true }
FieldInTypeDescription
dnpbodybooleanRequired. true to mark as DNP, false to unmark
ParameterInTypeDescription
refpathstringRequired. Reference designator (e.g. "C3")

Response: 200 OK

{
  "ref": "C3",
  "dnp": true
}
FieldTypeDescription
refstringReference designator
dnpbooleanCurrent DNP state

Board configuration

GET /revisions/:revisionId/board

Read the board for the revision you intend to order. Keep boardId and version for edits. A project contains revisions of a design; quote and order a specific revision, not an implicit latest version.

Response: 200 OK

{
  "boardId": 7001,
  "spec": {
    "blindBuriedHoles": 0,
    "numLayers": 4,
    "widthMM": 50,
    "lengthMM": 40,
    "materialThicknessMm": 1.6,
    "minTraceWidthMM": 0.15,
    "minPitchMM": 0.5,
    "minHoleSizeMM": 0.3,
    "silkScreenBothSides": true,
    "hasSMDBothSides": true,
    "selectedThicknessMM": 1.6,
    "selectedSolderMaskColor": "green",
    "selectedSilkscreenColor": "white",
    "customRequirements": null,
    "numBoards": 1,
    "tgLevel": null,
    "viaInPad": false,
    "customStackup": null,
    "blindBuriedLayerSets": 0,
    "castellatedEdges": false,
    "panelizationMethod": "Any",
    "silkscreenWidthMM": null,
    "fixSilkscreen": true,
    "smViaFillLocations": null,
    "viaInPadFillLocations": null,
    "goldFingers": null,
    "dielectricMaterial": "FR4",
    "surfaceFinish": "ENIG",
    "outerCopperWeightOz": 1,
    "innerCopperWeightOz": 0.5,
    "viaIsConductive": false
  },
  "impedances": [],
  "stackup": null,
  "possibleStackupCombinations": [],
  "messages": [],
  "hasCamReport": false,
  "circuitHubCAMComments": null,
  "updatedAt": "2026-11-03T09:00:00Z",
  "version": 12
}

Response fields

FieldTypeDescription
boardIdintegerIdentity of this revision’s current board
versionintegerExisting board version; changes when board settings or impedance requirements change
updatedAtstringISO 8601 update time
specobjectImported geometry and selected manufacturing settings
impedancesarrayControlled-impedance requirements; empty when none
stackupobject or nullResolved stackup, when available
possibleStackupCombinationsarrayAvailable stackup combinations; may be empty
messagesarrayFabrication messages to review
hasCamReportbooleanWhether a CAM report exists
circuitHubCAMCommentsstring or nullCircuitHub manufacturing review comments

The response follows the Portal board representation. Missing measurements are null, not zero. Stackup availability is not a promise of manufacturability; the quote determines the available services.

Board measurements and stackup

spec.numLayers is the imported copper layer count. widthMM, lengthMM, materialThicknessMm, minTraceWidthMM, minPitchMM, minHoleSizeMM and silkscreenWidthMM are measurements in millimetres. materialThicknessMm describes the imported design; selectedThicknessMM is the manufacturing selection. numBoards describes the design, not the quantity being ordered.

silkScreenBothSides, hasSMDBothSides, blindBuriedHoles and viaInPad describe the imported board. fixSilkscreen is a read-only manufacturing setting on this surface. The editable selections are listed under Update board configuration. goldFingers, when present, contains the Portal’s gold finger options; use the three individual switches when editing.

A resolved stackup contains id, name, description, ownerId, overallThickness, tolerancePlus, toleranceMinus, copperLayerCount and layers. Each layer has id, stackupId, sequence, number, layerType, materialName, description, thickness, minTraceWidth, layerName and note. Unknown layer measurements and descriptive fields can be null. possibleStackupCombinations lists supported combinations of overallThickness, copperWeightOuter and copperWeightInner.

messages contains level and message for each fabrication finding. Review these alongside the quote’s orderability and blockers. A board version does not freeze the standard stackup catalogue.

PATCH /revisions/:revisionId/board

Change only the supplied settings. The API example is also the content of board-change.json. Omitted or null settings remain unchanged, matching the Portal; send an empty string to clear a text requirement. Geometry such as layer count and outline dimensions is read-only. Request a fresh quote after editing.

Request body:

{
  "expectedBoardId": 7001,
  "expectedVersion": 12,
  "boardThickness": 1.2
}

Response: 200 OK

{
  "boardId": 7001,
  "spec": {
    "blindBuriedHoles": 0,
    "numLayers": 4,
    "widthMM": 50,
    "lengthMM": 40,
    "materialThicknessMm": 1.6,
    "minTraceWidthMM": 0.15,
    "minPitchMM": 0.5,
    "minHoleSizeMM": 0.3,
    "silkScreenBothSides": true,
    "hasSMDBothSides": true,
    "selectedThicknessMM": 1.2,
    "selectedSolderMaskColor": "green",
    "selectedSilkscreenColor": "white",
    "customRequirements": null,
    "numBoards": 1,
    "tgLevel": null,
    "viaInPad": false,
    "customStackup": null,
    "blindBuriedLayerSets": 0,
    "castellatedEdges": false,
    "panelizationMethod": "Any",
    "silkscreenWidthMM": null,
    "fixSilkscreen": true,
    "smViaFillLocations": null,
    "viaInPadFillLocations": null,
    "goldFingers": null,
    "dielectricMaterial": "FR4",
    "surfaceFinish": "ENIG",
    "outerCopperWeightOz": 1,
    "innerCopperWeightOz": 0.5,
    "viaIsConductive": false
  },
  "impedances": [],
  "stackup": null,
  "possibleStackupCombinations": [],
  "messages": [],
  "hasCamReport": false,
  "circuitHubCAMComments": null,
  "updatedAt": "2026-11-03T09:05:00Z",
  "version": 13
}

Request body

FieldTypeDescription
expectedBoardIdintegerRequired. boardId from the last read
expectedVersionintegerRequired. version from the last read
boardThicknessnumberSelected thickness in mm; read as spec.selectedThicknessMM
dielectricMaterialstringFor example FR4, Aluminum, Roger, RigidFlex or Flex
tgLevelstringMaterial glass-transition requirement: Low, Medium or High
outerCopperWeightOz / innerCopperWeightOznumberCopper weight in oz
surfaceFinishstringENIG or ENEPIG
soldermaskColor / silkscreenColorstringSolder mask: black, purple, green, green-matte, red, yellow, blue, white, black-matte, orange. Silkscreen: white or black. Read as selectedSolderMaskColor and selectedSilkscreenColor
goldFingers / goldFingersHardGoldPlating / goldFingersChamferbooleanGold finger options; disabling fingers also disables plating and chamfer
viaPlugging / viaFillAndCapstringVia locations; read as smViaFillLocations and viaInPadFillLocations
viaIsConductivebooleanConductive via fill requirement
blindBuriedLayerSetsintegerNumber of blind/buried layer sets
panelizationMethodstringAny, VScore or TabRoute
needsCastellatedEdgesbooleanRead as spec.castellatedEdges
hasCustomRequirements / hasCustomStackupstringRead as customRequirements and customStackup

The Platform calls require both identity and version. The Portal also supports timestamp preconditions; these examples use its existing version counter. Unknown fields are rejected. On a stale board, read it again and review your changes before resubmitting. A locked quote prevents edits.

PUT /revisions/:revisionId/board/impedances

Replace the complete list. The API body is the content of impedances.json. Retain IDs for existing rows, use null for new rows, and omit rows to remove them. An empty rows array clears all requirements. Read the board again before quoting.

Request body:

{
  "expectedBoardId": 7001,
  "expectedVersion": 13,
  "rows": [
    {
      "id": null,
      "layers": "1",
      "width": 0.15,
      "space": null,
      "unit": "mm",
      "mode": "single-ended",
      "transmissionLineType": "microstrip",
      "value": 50,
      "tolerance": 10,
      "coupon": true,
      "comments": null
    }
  ]
}

Response: 200 OK

{
  "boardId": 7001,
  "impedances": [
    {
      "id": 7101,
      "layers": "1",
      "width": 0.15,
      "space": null,
      "unit": "mm",
      "mode": "single-ended",
      "transmissionLineType": "microstrip",
      "value": 50,
      "tolerance": 10,
      "coupon": true,
      "comments": null,
      "createdAt": "2026-11-03T09:10:00Z",
      "boardId": 7001
    }
  ],
  "updatedAt": "2026-11-03T09:10:00Z",
  "version": 14
}

Request body

FieldTypeDescription
expectedBoardId / expectedVersionintegerRequired. Current board identity and version
rowsarrayRequired. Complete list of impedance requirements
rows[].idinteger or nullExisting row ID, or null for a new row
rows[].layersstringApplicable layer or layers
rows[].width / rows[].spacenumber / number or nullTrace width and pair spacing in unit; spacing is null for single-ended traces
rows[].unitstringmm or mil
rows[].modestringsingle-ended or differential
rows[].transmissionLineTypestringTransmission line geometry, for example microstrip
rows[].valueintegerTarget impedance in ohms
rows[].toleranceintegerAllowed percentage deviation
rows[].couponbooleanWhether a test coupon is required
rows[].commentsstring or nullAdditional instructions

Errors

Errors: 400 VALIDATION_ERROR for invalid settings, 409 BOARD_CHANGED for an outdated identity or version, and 409 QUOTE_LOCKED while a quote is locked. Do not silently retry a conflicting edit.

See the board guide for CLI examples and editing rules.


Shipping addresses

GET /shipping-addresses

Select a saved address by its ID.

Response: 200 OK

{
  "shippingAddresses": [
    {
      "id": 6101,
      "name": "Alex Smith",
      "company": "Acme Electronics",
      "street1": "100 Main Street",
      "street2": null,
      "street3": null,
      "city": "Boston",
      "state": "MA",
      "zip": "02110",
      "countryCode": "US",
      "phoneNumber": "+16175550100"
    }
  ]
}

Address fields

FieldTypeDescription
idintegerServer-assigned saved address ID; not a create input
namestringRecipient name
companystring or nullRecipient company
street1stringStreet address
street2 / street3string or nullAdditional address lines
citystringCity
statestring or nullState or region; required where the destination needs one
zipstring or nullPostal code; required where the destination needs one
countryCodestringTwo-letter country code, for example US
phoneNumberstringContact telephone number, including country code

The workspace query parameter is required on every address-book operation. Older saved addresses may have null contact or address fields; complete them before requesting shipping options. Address validation does not guarantee delivery availability.

GET /shipping-addresses/:addressId

Read one address in the workspace’s address book.

Response: 200 OK

{
  "id": 6101,
  "name": "Alex Smith",
  "company": "Acme Electronics",
  "street1": "100 Main Street",
  "street2": null,
  "street3": null,
  "city": "Boston",
  "state": "MA",
  "zip": "02110",
  "countryCode": "US",
  "phoneNumber": "+16175550100"
}

POST /shipping-addresses

Save a delivery address. Use the API body below as address.json. Keep the returned ID for shipping options and order preview.

Request body:

{
  "name": "Alex Smith",
  "company": "Acme Electronics",
  "street1": "100 Main Street",
  "street2": null,
  "street3": null,
  "city": "Boston",
  "state": "MA",
  "zip": "02110",
  "countryCode": "US",
  "phoneNumber": "+16175550100"
}

Response: 201 Created

{
  "id": 6101,
  "name": "Alex Smith",
  "company": "Acme Electronics",
  "street1": "100 Main Street",
  "street2": null,
  "street3": null,
  "city": "Boston",
  "state": "MA",
  "zip": "02110",
  "countryCode": "US",
  "phoneNumber": "+16175550100"
}

PUT /shipping-addresses/:addressId

Send the full address. Replacement creates a new address record and removes the old one from this address book, matching the Portal. Existing orders retain the old address. Use the new ID and request fresh shipping options and a new preview.

Request body:

{
  "name": "Alex Smith",
  "company": "Acme Electronics",
  "street1": "200 Main Street",
  "street2": null,
  "street3": null,
  "city": "Boston",
  "state": "MA",
  "zip": "02110",
  "countryCode": "US",
  "phoneNumber": "+16175550100"
}

Response: 200 OK

{
  "id": 6102,
  "name": "Alex Smith",
  "company": "Acme Electronics",
  "street1": "200 Main Street",
  "street2": null,
  "street3": null,
  "city": "Boston",
  "state": "MA",
  "zip": "02110",
  "countryCode": "US",
  "phoneNumber": "+16175550100"
}

DELETE /shipping-addresses/:addressId

Remove an address from the workspace’s address book. This does not change an existing order’s delivery address.

Response: 204 No Content

No response body.

Errors

Errors: 400 VALIDATION_ERROR for incomplete or invalid fields, and 404 NOT_FOUND when the address is not in the selected workspace.


Quotes

A quote prices one project revision at one quantity. Its offers show the available manufacturing services and ship dates. Manufacturing prices exclude shipping and tax; requesting a quote does not place an order or take payment.

POST /quotes

Supply revisionId and quantity. The revision identifies the project and owning workspace; there is no projectId input or workspace query parameter on this endpoint. The caller must have access to that revision.

The request waits for a quote, or returns a valid cached quote for the same revision and quantity. There is no separate polling resource. A request can take up to five minutes. Use expiresAt rather than assuming a full hour: normal quotes expire after one hour or earlier at the daily order cutoff.

Request body:

{"revisionId": 99001, "quantity": 10}
FieldTypeDescription
revisionIdintegerRequired. The specific revision to quote
quantityintegerRequired. Positive number of boards to manufacture

Response: 200 OK

{
  "id": 50001,
  "projectId": 12345,
  "revisionId": 99001,
  "revisionNumber": 3,
  "quantity": 10,
  "currency": "USD",
  "createdAt": "2026-11-03T10:00:00Z",
  "expiresAt": "2026-11-03T11:00:00Z",
  "locked": false,
  "status": "ready",
  "type": "assembled",
  "orderable": true,
  "blockers": [],
  "offers": [
    {
      "id": 880002,
      "service": "priority",
      "shipDate": {
        "earliest": "2026-11-06",
        "latest": "2026-11-06"
      },
      "fabricationCountry": "US",
      "assemblyLine": "us_grid",
      "price": {
        "lineItems": [
          {
            "type": "boards",
            "label": "Boards",
            "amount": "504.00"
          },
          {
            "type": "parts",
            "label": "Parts",
            "amount": "279.45"
          },
          {
            "type": "assembly",
            "label": "Assembly",
            "amount": "396.55"
          }
        ],
        "manufacturingSubtotal": "1180.00",
        "discounts": [
          {
            "type": "grid_proto",
            "code": null,
            "label": "Grid Proto",
            "amount": "319.50"
          },
          {
            "type": "account",
            "code": null,
            "label": "Your pricing",
            "amount": "86.05"
          }
        ],
        "manufacturingTotal": "774.45",
        "manufacturingUnitPrice": "77.45",
        "excludes": [
          "shipping",
          "tax"
        ]
      },
      "checkoutUrl": "https://app.circuithub.com/projects/acme-electronics/power-supply-board/revisions/99001/order?quantity=10&offer=880002"
    }
  ],
  "designFeedback": []
}

Response fields

FieldTypeDescription
idintegerStable quote ID
projectIdintegerProject containing the quoted revision
revisionIdintegerRevision used for pricing
revisionNumberintegerHuman-readable revision number
quantityintegerRequested board quantity; applies to every offer
currencystringCurrency for all amounts; USD in this version
createdAtstringISO 8601 quote creation time
expiresAtstringISO 8601 expiry time
lockedbooleanWhether the quote is locked. Read-only: the API cannot lock or unlock quotes
statusstringready or manual_quote_required; ready alone does not mean orderable
typestring or nullassembled, board_only, or null when a manufacturing type is not available
orderablebooleanWhether the quote can currently be used to order; not a reservation of stock
blockersarrayReasons ordering is blocked, with affected BOM line IDs
offersarrayAvailable manufacturing offers for the requested quantity; empty when manual review is required
designFeedbackarrayChanges to the design that would make the order cheaper, such as qualifying for Grid Proto; empty when there are none. Each entry has type, summary, reasons, potential and docsUrl; ignore entries whose type you do not know. See design feedback in the quoting guide

Offer fields

FieldTypeDescription
idintegerStable offer ID; select this with the quote ID when ordering
servicestringpriority, standard, flexible, manual_quote, or board_only
shipDateobjectearliest and latest calendar dates in YYYY-MM-DD format; equal for a single ship date
fabricationCountrystring or nullBoard fabrication country code, when known
assemblyLinestring or nullAssembly line identifier, when applicable
priceobjectManufacturing price, detailed below
checkoutUrlstringPortal link to this revision's order page. The Portal does not yet preselect the quantity and offer from the link, so choose them again there

Ship dates describe dispatch from CircuitHub, not delivery. Grid Proto is a discount on a Priority offer, not a separate service. Available offers depend on the design; not every service is returned for every quote.

Price fields

All money amounts are decimal strings in the quote's currency. Use exact decimal arithmetic rather than binary floating-point arithmetic.

FieldTypeDescription
lineItemsarray or nullUndiscounted manufacturing service amounts; null if not recorded
manufacturingSubtotalstring or nullSum of undiscounted line amounts; null if not recorded
discountsarray or nullDiscounts in application order; null if not recorded, [] if known to have none
manufacturingTotalstringManufacturing amount after discounts, excluding shipping and tax
manufacturingUnitPricestringRounded manufacturing total divided by board quantity; for display only
excludesstring[]shipping and tax

Each line item has type, label and amount. Board-only offers have a boards line. Assembled offers have boards, parts and assembly lines; a fully consigned build can have a zero parts amount. Consignment handling appears when applicable. Firmware is included in assembly. Accept new line types and display their supplied labels.

Each discount has type, code, label and amount. Types are grid_proto, account and promo_code; code is null when no code applies. Account discounts use the customer-facing label “Your pricing”. Discounts are positive amounts subtracted from the subtotal. Grid Proto is applied before an account discount; promo codes are not combined with Grid Proto. This endpoint reports applicable discounts and does not redeem codes.

manufacturingSubtotal = sum(lineItems.amount)
manufacturingTotal = manufacturingSubtotal - sum(discounts.amount)

Discounts apply to the manufacturing total, not individual public line items. Do not subtract a discount again from already discounted amounts. Do not reconstruct the total by multiplying the rounded unit price by quantity.

Older offers retain their known total and unit price, but lineItems, manufacturingSubtotal and discounts are null if their breakdown was not recorded. Do not infer missing lines or treat null discounts as zero.

Blockers

Each blocker identifies its code and affected bomLineIds.

CodeMeaningWhat to do
part_not_chosenA BOM line has no approved partChoose a suitable part
no_supplierNo usable supplier for a selected partReview the part and its sourcing
insufficient_stockAvailable stock cannot cover the buildChoose an alternative or change the quantity
parts_require_reviewAn offer cannot be ordered for a reason no BOM line explains; bomLineIds is emptyContact CircuitHub to review the parts

For example, a blocked quote contains orderable: false and a blocker such as:

{"code": "insufficient_stock", "bomLineIds": [1003]}

Use the BOM guide and part search to resolve affected lines, then request a new quote. A quote needing manual review has status: "manual_quote_required", type: null, orderable: false and no offers. Contact CircuitHub for that review rather than repeatedly requesting the same quote.

GET /quotes/:quoteId

Retrieve an existing quote without recalculating it or extending its expiry. Returns the same quote representation as POST /quotes.

ParameterInTypeDescription
quoteIdpathintegerRequired. Quote ID

Response: 200 OK

{
  "id": 50001,
  "projectId": 12345,
  "revisionId": 99001,
  "revisionNumber": 3,
  "quantity": 10,
  "currency": "USD",
  "createdAt": "2026-11-03T10:00:00Z",
  "expiresAt": "2026-11-03T11:00:00Z",
  "locked": false,
  "status": "ready",
  "type": "assembled",
  "orderable": true,
  "blockers": [],
  "offers": [
    {
      "id": 880002,
      "service": "priority",
      "shipDate": {
        "earliest": "2026-11-06",
        "latest": "2026-11-06"
      },
      "fabricationCountry": "US",
      "assemblyLine": "us_grid",
      "price": {
        "lineItems": [
          {
            "type": "boards",
            "label": "Boards",
            "amount": "504.00"
          },
          {
            "type": "parts",
            "label": "Parts",
            "amount": "279.45"
          },
          {
            "type": "assembly",
            "label": "Assembly",
            "amount": "396.55"
          }
        ],
        "manufacturingSubtotal": "1180.00",
        "discounts": [
          {
            "type": "grid_proto",
            "code": null,
            "label": "Grid Proto",
            "amount": "319.50"
          },
          {
            "type": "account",
            "code": null,
            "label": "Your pricing",
            "amount": "86.05"
          }
        ],
        "manufacturingTotal": "774.45",
        "manufacturingUnitPrice": "77.45",
        "excludes": [
          "shipping",
          "tax"
        ]
      },
      "checkoutUrl": "https://app.circuithub.com/projects/acme-electronics/power-supply-board/revisions/99001/order?quantity=10&offer=880002"
    }
  ],
  "designFeedback": []
}

Errors and expiry

These are the quote-specific errors, using the API error envelope.

HTTP statusCodeMeaning
400VALIDATION_ERRORMissing or invalid revision or quantity
409REVISION_NOT_READYThe revision has not finished importing
409QUOTE_LOCKEDA different quantity is currently locked
410QUOTE_EXPIREDThe requested quote has expired
429RATE_LIMITEDRequest limit reached; follow Retry-After
503QUOTING_UNAVAILABLEQuoting is temporarily paused

Request a fresh quote after changing the design, parts or quantity. A quote lock does not fix shipping charges or tax. The API does not manage quote locks or give per-part cost breakdowns.

See the Quoting guide for CLI and curl examples, and the ordering walkthrough for shipping, tax and order placement.


Orders

POST /quotes/:quoteId/offers/:offerId/shipping-options

Request services for the selected offer and delivery address. An empty list means no service is available for this selection. Option IDs identify available services, not saved shipping quotations; prices are recalculated at preview and submission.

Request body:

{
  "shippingAddressId": 6101
}

Response: 200 OK

{
  "quoteId": 50001,
  "offerId": 880002,
  "shippingAddressId": 6101,
  "currency": "USD",
  "shippingOptions": [
    {
      "id": "ups_ground",
      "label": "UPS Ground",
      "amount": "25.00"
    }
  ]
}

Request and response fields

FieldTypeDescription
shippingAddressIdintegerRequired request field. Saved address available to the quote’s workspace
shippingOptions[].idstringOpaque service option ID to use in preview
shippingOptions[].labelstringService display name
shippingOptions[].amountstringShipping charge before tax, as a decimal string
currencystringUSD

Request options again after changing the offer or address. Manufacturing ship dates are dispatch dates, not delivery dates. Shipping charges do not include tax or international import duties.

A quote, offer or address that is not the workspace's is 404 NOT_FOUND; an address missing details needed for shipping is 400 VALIDATION_ERROR. If shipping rates cannot be fetched, the request fails with 503 SHIPPING_UNAVAILABLE; request options again later.

POST /orders/preview

Calculate the complete price without saving a checkout, creating an order or taking payment. Use the API body as order-selection.json. The workspace query parameter is required; every selection must belong to or be usable by that workspace.

Request body:

{
  "quoteId": 50001,
  "offerId": 880002,
  "shippingAddressId": 6101,
  "shippingOptionId": "ups_ground",
  "paymentMethodId": "card_abc123"
}

Response: 200 OK

{
  "quoteId": 50001,
  "offerId": 880002,
  "shippingAddressId": 6101,
  "shippingOptionId": "ups_ground",
  "paymentMethodId": "card_abc123",
  "currency": "USD",
  "orderable": true,
  "blockers": [],
  "price": {
    "manufacturingSubtotal": "1180.00",
    "discounts": [
      {
        "type": "grid_proto",
        "code": null,
        "label": "Grid Proto",
        "amount": "319.50"
      },
      {
        "type": "account",
        "code": null,
        "label": "Your pricing",
        "amount": "86.05"
      }
    ],
    "manufacturingTotal": "774.45",
    "shipping": "25.00",
    "tax": "49.97",
    "total": "849.42"
  },
  "payment": {
    "amountDueNow": "849.42",
    "amountDueLater": "0.00",
    "terms": null
  },
  "taxStatus": "calculated",
  "quoteExpiresAt": "2026-11-03T11:00:00Z"
}

Request body

FieldTypeDescription
quoteId / offerIdintegerRequired. An offer belonging to this quote
shippingAddressIdintegerRequired. Saved delivery address
shippingOptionIdstringRequired. Option returned for this offer and address
paymentMethodIdstringRequired. Available workspace payment method

Preview fields

FieldTypeDescription
currencystringCurrency for all amounts; USD
orderablebooleanWhether this selection can currently be submitted
blockersarrayReasons it cannot be ordered; objects with code and message
price.manufacturingSubtotalstring or nullManufacturing price before discounts; null if historical detail was not recorded
price.discountsarray or nullPositive savings, already reflected in manufacturingTotal
price.manufacturingTotalstringManufacturing price after discounts
price.shippingstringShipping charge before tax
price.taxstring or nullCalculated tax; null when unavailable, never substituted with zero
price.totalstring or nullManufacturing total + shipping + tax; null when tax is unavailable
payment.amountDueNow / payment.amountDueLaterstring or nullAmounts payable now and later under the selected method; null when a complete price cannot be calculated
payment.termsstring or nullApproved payment terms when applicable
taxStatusstringcalculated or unavailable; a calculated zero tax is valid
quoteExpiresAtstringOriginal quote expiry; preview does not extend it

All amounts are decimal strings. Discounts are already deducted from manufacturingTotal; do not deduct them again. For a valid preview, amountDueNow + amountDueLater = total. The example pays in full by card. Approved terms may instead show zero due now and the full total due later; the server decides from the account and selected method. Credit limits are not a discount.

If tax calculation is unavailable, return orderable: false, taxStatus: "unavailable", a TAX_UNAVAILABLE blocker, and null tax, total and payment amounts. Resolve it before ordering. International duties may be payable separately and are not included in this total.

A net terms method without enough available credit for the total gives a PAYMENT_METHOD_UNAVAILABLE blocker. A selection that is not the workspace's, or a payment method it does not list, is 404 NOT_FOUND.

A preview does not reserve stock, fix shipping prices or hold credit. There is no preview ID to save. Review a fresh preview after changing any selection or billing details.

POST /orders

Submit the same selections you previewed, with the reviewed total. Use the API body as order.json. This operation creates a manufacturing order and may charge the selected method. Read the returned order ID and payment status.

Request body:

{
  "quoteId": 50001,
  "offerId": 880002,
  "shippingAddressId": 6101,
  "shippingOptionId": "ups_ground",
  "paymentMethodId": "card_abc123",
  "expectedTotal": "849.42",
  "poNumber": "PO-2026-042"
}

Response: 201 Created

{
  "id": 8001,
  "urn": "acme-electronics/00001f41",
  "name": "ORD-8001",
  "status": "in-progress",
  "quantity": 10,
  "createdAt": "2026-11-03T10:05:00Z",
  "updatedAt": "2026-11-03T10:05:00Z",
  "quotedShippingDate": "2026-11-06T00:00:00Z",
  "estimatedShippingDate": null,
  "shippedDate": null,
  "trackingNumber": null,
  "shippingService": "UPS Ground",
  "value": 774.45,
  "paymentStatus": "Paid",
  "poNumber": "PO-2026-042",
  "projectId": 12345,
  "projectName": "Power Supply Board",
  "revisionId": 99001,
  "revisionNumber": 3
}

Request body

The five selection fields are the same as the preview request.

FieldTypeDescription
expectedTotalstringRequired. Exact decimal total reviewed in the preview, in the quote currency
poNumberstringOptional. Your purchase order reference; not a duplicate-request key

CircuitHub validates the quote, address, shipping service, billing details, payment method and stock again. It recalculates the total and refuses a mismatch before attempting payment. An equal total is a price check, not a guarantee that every component of the preview is unchanged.

The response uses the existing order representation below. Its value is the existing stored order value, not the new preview’s all-in total or amount due. The read API does not return a saved copy of the preview. Keep your reviewed calculation if you need it; invoices and payments are separate billing records.

Submission errors and retries

HTTP statusCodeAction
400VALIDATION_ERRORCorrect the supplied selections or expected total
409TOTAL_CHANGEDReview a new preview and submit its total
404NOT_FOUNDA selection is not the workspace's; use IDs it lists
409ORDER_NOT_ORDERABLEResolve stock, billing, shipping or payment eligibility blockers. A quote that has already been ordered is refused this way, and the message names its order
410QUOTE_EXPIREDRequest a fresh quote, shipping options and preview
422PAYMENT_FAILEDReview the payment failure; check for an existing order before trying again
503TAX_UNAVAILABLEWait for tax calculation to recover and preview again
503SHIPPING_UNAVAILABLEWait for shipping rates to recover, then request shipping options and a preview again

Do not automatically retry order submission after a timeout, connection loss or server error. An order or payment may already exist. List the workspace’s recent orders and inspect likely matches. A PO number is not unique; if the result is uncertain, contact CircuitHub before resubmitting. Submissions are not idempotent and charging is not exactly-once. Two submissions of the same quote at the same moment are not prevented from both taking payment, so submit an order once and wait for the outcome.

GET /orders

List orders for a workspace.

ParameterInTypeDescription
workspacequerystringRequired. Workspace slug

Response: 200 OK

{
  "orders": [
    {
      "id": 8001,
      "urn": "acme-electronics/00001f41",
      "name": "ORD-8001",
      "status": "in-progress",
      "quantity": 10,
      "createdAt": "2025-03-01T14:00:00Z",
      "updatedAt": "2025-03-02T11:00:00Z",
      "quotedShippingDate": "2025-03-15T00:00:00Z",
      "estimatedShippingDate": "2025-03-14T00:00:00Z",
      "shippedDate": null,
      "trackingNumber": null,
      "shippingService": null,
      "value": 790.50,
      "paymentStatus": "Paid",
      "poNumber": "PO-2025-042",
      "projectId": 12345,
      "projectName": "Power Supply Board",
      "revisionId": 99001,
      "revisionNumber": 3
    }
  ]
}

Order fields:

FieldTypeDescription
idintegerOrder ID
urnstringOrder URN
namestringOrder name/number
statusstringOrder status (e.g. in-progress, completed)
quantityintegerNumber of boards ordered
createdAtstringISO 8601 creation timestamp
updatedAtstringISO 8601 last update timestamp
quotedShippingDatestringQuoted shipping date, ISO 8601
estimatedShippingDatestring | nullEstimated shipping date, ISO 8601
shippedDatestring | nullActual ship date, ISO 8601
trackingNumberstring | nullShipment tracking number
shippingServicestring | nullShipping carrier/service
valuenumberExisting stored order value in USD; not the preview’s all-in total
paymentStatusstringUnpaid, Paid or PartiallyPaid
poNumberstringPurchase order number
projectIdintegerProject this order belongs to
projectNamestringProject name
revisionIdintegerProject revision used for this order
revisionNumberintegerHuman-readable revision number

GET /orders/:orderId

Get a single order by ID.

Response: 200 OK

{
  "id": 8001,
  "urn": "acme-electronics/00001f41",
  "name": "ORD-8001",
  "status": "in-progress",
  "quantity": 10,
  "createdAt": "2025-03-01T14:00:00Z",
  "updatedAt": "2025-03-02T11:00:00Z",
  "quotedShippingDate": "2025-03-15T00:00:00Z",
  "estimatedShippingDate": "2025-03-14T00:00:00Z",
  "shippedDate": null,
  "trackingNumber": null,
  "shippingService": null,
  "value": 790.50,
  "paymentStatus": "Paid",
  "poNumber": "PO-2025-042",
  "projectId": 12345,
  "projectName": "Power Supply Board",
  "revisionId": 99001,
  "revisionNumber": 3
}

Parts

GET /parts

Search for parts by MPN or description.

ParameterInTypeDescription
qquerystringRequired. Search query

Response: 200 OK

{
  "query": "LM358",
  "exactMatches": [
    {
      "id": 5001,
      "mpn": "LM358DR",
      "manufacturer": "Texas Instruments",
      "description": "Dual Operational Amplifier",
      "available": 12500,
      "moq": 1
    }
  ],
  "inexactMatches": [
    {
      "id": 5010,
      "mpn": "LM358ADR",
      "manufacturer": "Texas Instruments",
      "description": "Dual Op-Amp, Improved",
      "available": 4200,
      "moq": 1
    }
  ]
}
FieldTypeDescription
querystringOriginal search query
exactMatchesarrayExact MPN matches (case-insensitive)
inexactMatchesarrayFuzzy/related matches

Part fields:

FieldTypeDescription
idintegerPart ID
mpnstringManufacturer part number
manufacturerstringManufacturer name
descriptionstring | nullPart description
availableintegerCurrent stock availability
moqinteger | nullMinimum order quantity

Issues

Issues are coming soon. The endpoints below describe the planned API surface — they are not yet available.

GET /issues

List issues for a workspace.

ParameterInTypeDescription
workspacequerystringRequired. Workspace slug
orderqueryintegerFilter by order ID (optional)
statusquerystringFilter by status: open, closed (optional)

POST /orders/:orderId/issues

Create a new issue on an order.

ParameterInTypeDescription
orderIdpathintegerRequired. Order ID to file the issue against
FieldInTypeDescription
titlebodystringRequired. Short summary of the issue
commentbodystringOptional initial comment attached to the issue

See the Issues guide for full examples and response shapes.


Billing

All billing endpoints are scoped to a workspace.

GET /workspaces/:workspace/billing/details

Read the billed party and address. The endpoint returns null if billing details have not been set.

Response: 200 OK

{
  "name": "Acme Electronics",
  "isPerson": false,
  "email": "accounts@example.com",
  "phone": "+16175550100",
  "address": {
    "street1": "100 Main Street",
    "street2": null,
    "street3": null,
    "city": "Boston",
    "state": "MA",
    "zip": "02110",
    "countryCode": "US"
  }
}

Billing detail fields

FieldTypeDescription
namestringRequired. Legal name of the billed company or person
isPersonbooleanRequired. True for an individual, false for a company
emailstringRequired. Billing contact email
phonestring or nullBilling contact telephone
addressobjectRequired. Billing address; use the street, city, state, zip and countryCode fields shown below

Billing details may belong to a customer shared by multiple workspaces. Updating them can affect billing for those workspaces too. This call does not alter past invoices or grant tax exemption or payment terms.

Billing details are those of the workspace's CircuitHub billing account, which CircuitHub sets up and which its accounting system receives. A workspace without one is refused with 409 BILLING_ACCOUNT_REQUIRED. Changing details shared with workspaces you cannot change is refused with 403 FORBIDDEN.

PUT /workspaces/:workspace/billing/details

Send the full billing details. Use the API body as billing.json. Setting billing details does not add a shipping address. Request a new order preview after changing them.

Request body:

{
  "name": "Acme Electronics",
  "isPerson": false,
  "email": "accounts@example.com",
  "phone": "+16175550100",
  "address": {
    "street1": "100 Main Street",
    "street2": null,
    "street3": null,
    "city": "Boston",
    "state": "MA",
    "zip": "02110",
    "countryCode": "US"
  }
}

Response: 200 OK

{
  "name": "Acme Electronics",
  "isPerson": false,
  "email": "accounts@example.com",
  "phone": "+16175550100",
  "address": {
    "street1": "100 Main Street",
    "street2": null,
    "street3": null,
    "city": "Boston",
    "state": "MA",
    "zip": "02110",
    "countryCode": "US"
  }
}

POST /workspaces/:workspace/billing/payment-methods

CircuitHub does not yet provide a secure card collection flow for API clients, so for now add cards in CircuitHub Settings. This endpoint takes a card token collected through the payment provider’s secure collection flow. Submit the token once. The endpoint uses the Portal’s existing Stripe token/source flow, not a PaymentIntent flow. tok_example illustrates a token and cannot be used to pay. Never send card numbers or security codes to this API.

Request body:

{
  "token": "tok_example"
}

Response: 201 Created

{
  "id": "card_abc123",
  "type": "card",
  "brand": "visa",
  "last4": "4242",
  "isDefault": true
}

Request body

FieldTypeDescription
tokenstringRequired. A fresh provider token from secure collection; do not log it

A token is not a saved payment-method ID. Keep the returned id for ordering. Adding a method does not place an order, charge it, grant credit or enable bank terms.

GET /workspaces/:workspace/billing/payment-methods/:paymentMethodId

Read a method available to this workspace. Treat its ID as opaque.

Response: 200 OK

{
  "id": "card_abc123",
  "type": "card",
  "brand": "visa",
  "last4": "4242",
  "isDefault": true
}

PUT /workspaces/:workspace/billing/payment-methods/:paymentMethodId/default

Set a saved card as the workspace default. Order submissions still explicitly select a method. This does not change payment on existing orders.

Response: 200 OK

{
  "id": "card_abc123",
  "type": "card",
  "brand": "visa",
  "last4": "4242",
  "isDefault": true
}

DELETE /workspaces/:workspace/billing/payment-methods/:paymentMethodId

Remove a saved card. Approved terms and wire-payment access are managed by CircuitHub and cannot be created or removed through this call. Existing payments are unchanged.

Response: 204 No Content

No response body.

Payment method errors

Errors: 400 VALIDATION_ERROR for an invalid or used token, 404 NOT_FOUND for an unavailable method, 409 PAYMENT_METHOD_UNAVAILABLE for a method that cannot be used or changed, including a token that is not for a card, and 503 PAYMENTS_UNAVAILABLE when the provider is unavailable. Do not retry a token submission blindly after an uncertain result; list methods first.

GET /workspaces/:workspaceSlug/billing/invoices

List invoices for a workspace.

ParameterInTypeDescription
statusquerystringFilter: open, paid_in_full, approved_for_posting, fully_applied (optional)
fromquerystringStart date, ISO 8601 (optional)
toquerystringEnd date, ISO 8601 (optional)

Response: 200 OK

{
  "invoices": [
    {
      "id": 50124,
      "name": "INV-4835",
      "type": "charge",
      "recordType": "invoice",
      "orderId": 8001,
      "transactionDate": "2025-03-01T00:00:00Z",
      "dueDate": "2025-03-20T00:00:00Z",
      "totalNoTax": 80.00,
      "tax": 5.00,
      "total": 85.00,
      "amountPaid": 0.00,
      "amountUnpaid": 85.00,
      "status": "open",
      "description": "Additional testing services"
    }
  ]
}

GET /workspaces/:workspaceSlug/billing/invoices/:invoiceId

Get a single invoice by ID.

Response: 200 OK

{
  "id": 50124,
  "name": "INV-4835",
  "type": "charge",
  "recordType": "invoice",
  "orderId": 8001,
  "transactionDate": "2025-03-01T00:00:00Z",
  "dueDate": "2025-03-20T00:00:00Z",
  "totalNoTax": 80.00,
  "tax": 5.00,
  "total": 85.00,
  "amountPaid": 0.00,
  "amountUnpaid": 85.00,
  "status": "open",
  "description": "Additional testing services"
}

GET /workspaces/:workspaceSlug/billing/invoices/:invoiceId/pdf

Download an invoice as PDF.

Response: 200 OK with Content-Type: application/pdf

GET /workspaces/:workspaceSlug/billing/statement

Generate a PDF statement for a date range.

ParameterInTypeDescription
startDatequerystringRequired. Start date, ISO 8601
endDatequerystringRequired. End date, ISO 8601

Response: 200 OK with Content-Type: application/pdf

GET /workspaces/:workspaceSlug/billing/payments

List payments for a workspace. Includes both applied NetSuite payments and pending Stripe payments.

Response: 200 OK

{
  "payments": [
    {
      "id": 60001,
      "name": "PMT-1192",
      "date": "2025-02-16T00:00:00Z",
      "amount": 1240.00,
      "method": "card",
      "invoices": ["INV-4821"],
      "orders": [{ "id": 8001, "projectName": "Power Supply Board" }],
      "status": "applied"
    }
  ]
}

GET /workspaces/:workspaceSlug/billing/payment-methods

List payment methods configured for a workspace.

Response: 200 OK

{
  "paymentMethods": [
    {
      "id": "card_abc123",
      "type": "card",
      "brand": "visa",
      "last4": "4242",
      "isDefault": true
    },
    {
      "id": "net_301",
      "type": "net_terms",
      "terms": "net_30",
      "isDefault": false
    }
  ]
}

GET /workspaces/:workspaceSlug/billing/balance

Get credit balance and limit for a workspace. Returns null if no credit is enabled.

Response: 200 OK

{
  "balance": 2450.00,
  "creditLimit": 10000.00
}

Error responses

All endpoints return errors in this format:

{
  "error": "Project not found",
  "code": "NOT_FOUND"
}

Two kinds of refusal use other bodies. A request about a workspace or resource you cannot access can be refused with 403 and a body of the form {"message": "You are not authorized to access the resource."}. A request body that is not valid JSON, or whose fields have the wrong types, is refused with 400 and a plain-text explanation. Branch on the status code first.

CodeMeaning
NOT_FOUNDRequested resource does not exist
VALIDATION_ERRORRequest payload or query parameters were invalid
INVALID_REDIRECT_URIredirect_uri is not an absolute localhost URL (CLI login) or not an allowed MCP server callback (MCP sign-in)
INVALID_GRANTAn MCP sign-in code is not valid, already used or expired, or its code_verifier or redirect_uri does not match
SIGN_IN_NOT_CONFIRMEDA CLI or MCP sign-in was not confirmed on CircuitHub's own page, or the confirmation was for another user or sign-in. Sent back to the client as error=access_denied, or shown in the small print of CircuitHub's error page
INVALID_CLIENTPOST /auth/mcp/token, /auth/mcp/revoke or /auth/mcp/introspect was called without the CircuitHub MCP server's X-Connection-Client-Secret (status 401)
INSUFFICIENT_SCOPEA connection token was used on an endpoint outside the scopes its user granted it (status 403); requiredScope names the scope (see Scopes)
FILE_TOO_LARGEUpload exceeded the maximum allowed size
INVALID_UPLOAD_SIZEUpload size was invalid
INVALID_OBJECT_KEYUploaded object key did not match the workspace upload prefix
FORBIDDENAuthenticated user does not have permission to access the resource
UNAUTHORIZEDMissing or invalid authentication
RATE_LIMITEDToo many requests
CREATION_FAILEDProject or revision creation failed after validation
INTERNAL_ERRORUnexpected server error

On this page

HealthGET /healthAuthenticationGET /auth/cliGET /auth/mcpPOST /auth/mcp/tokenPOST /auth/mcp/revokePOST /auth/mcp/introspectScopesWorkspacesGET /workspacesProjectsGET /projectsGET /projects/:projectIdPOST /projects/upload-urlsPOST /projectsGET /projects/:projectId/statusGET /projects/:projectId/revisionsPOST /projects/:projectId/revisionsGET /projects/:projectId/bomPUT /bom/:bomLineId/partPUT /bom/:bomLineId/placements/:ref/dnpBoard configurationGET /revisions/:revisionId/boardResponse fieldsBoard measurements and stackupPATCH /revisions/:revisionId/boardRequest bodyPUT /revisions/:revisionId/board/impedancesRequest bodyErrorsShipping addressesGET /shipping-addressesAddress fieldsGET /shipping-addresses/:addressIdPOST /shipping-addressesPUT /shipping-addresses/:addressIdDELETE /shipping-addresses/:addressIdErrorsQuotesPOST /quotesResponse fieldsOffer fieldsPrice fieldsBlockersGET /quotes/:quoteIdErrors and expiryOrdersPOST /quotes/:quoteId/offers/:offerId/shipping-optionsRequest and response fieldsPOST /orders/previewRequest bodyPreview fieldsPOST /ordersRequest bodySubmission errors and retriesGET /ordersGET /orders/:orderIdPartsGET /partsIssuesGET /issuesPOST /orders/:orderId/issuesBillingGET /workspaces/:workspace/billing/detailsBilling detail fieldsPUT /workspaces/:workspace/billing/detailsPOST /workspaces/:workspace/billing/payment-methodsRequest bodyGET /workspaces/:workspace/billing/payment-methods/:paymentMethodIdPUT /workspaces/:workspace/billing/payment-methods/:paymentMethodId/defaultDELETE /workspaces/:workspace/billing/payment-methods/:paymentMethodIdPayment method errorsGET /workspaces/:workspaceSlug/billing/invoicesGET /workspaces/:workspaceSlug/billing/invoices/:invoiceIdGET /workspaces/:workspaceSlug/billing/invoices/:invoiceId/pdfGET /workspaces/:workspaceSlug/billing/statementGET /workspaces/:workspaceSlug/billing/paymentsGET /workspaces/:workspaceSlug/billing/payment-methodsGET /workspaces/:workspaceSlug/billing/balanceError responses