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.
| Parameter | In | Type | Description |
|---|---|---|---|
redirect_uri | query | string | Required. 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.
| Parameter | In | Type | Description |
|---|---|---|---|
redirect_uri | query | string | Required. A CircuitHub MCP server callback; any other URL is refused |
code_challenge | query | string | Required. PKCE challenge |
code_challenge_method | query | string | Required. Must be S256 |
state | query | string | Required. Returned unchanged in the redirect |
client_name | query | string | Required. The MCP client's name, shown to the user and recorded with the connection |
scope | query | string | The 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_kind | query | string | How 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_host | query | string | Where 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.
| Scope | Endpoints |
|---|---|
read | Everything that only reads: workspaces, projects, revisions, BOMs, board settings, orders, issues, quotes, parts, shipping addresses, billing details, saved cards, invoices, statements, payments and balance |
design | Requesting quotes, choosing BOM parts, do-not-place, board settings and impedances, and creating projects and revisions, including their export classification |
issues | Raising issues on orders, which emails CircuitHub staff and the order's followers |
ordering | Previewing and placing orders, shipping options, and adding, changing and removing shipping addresses |
billing | Changing billing details, and adding, removing and choosing the default saved card |
| Choice | Scopes |
|---|---|
| Read only | read |
| Engineering | read, design, issues |
| Full access | Every 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"
}
]
}| Field | Type | Description |
|---|---|---|
id | integer | Workspace ID |
slug | string | URL-safe identifier |
name | string | Display name |
Projects
GET /projects
List projects accessible to the authenticated user.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace | query | string | Required. 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 }
]
}| Field | In | Type | Description |
|---|---|---|---|
workspace | body | string | Required. Workspace slug |
files | body | array | Required. Files to upload |
files[].filename | body | string | Required. Filename |
files[].size | body | integer | Required. 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" }
]
}| Field | In | Type | Description |
|---|---|---|---|
name | body | string | Required. Project name |
workspace | body | string | Required. Workspace slug |
exportClassification | body | object | Required. {"tag": "NotExportControlled"}, {"tag": "EAR99"}, or {"tag": "ECCN", "contents": "<ECCN>"} |
files | body | array | Required. Uploaded file references |
files[].objectKey | body | string | Required. S3 object key from upload-urls response |
files[].filename | body | string | Required. 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
}| Field | Type | Description |
|---|---|---|
projectId | integer | Project ID |
status | string | "importing", "complete", or "failed" |
message | string | null | Error 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" }
]
}| Field | In | Type | Description |
|---|---|---|---|
files | body | array | Required. Uploaded file references |
files[].objectKey | body | string | Required. S3 object key from upload-urls response |
files[].filename | body | string | Required. 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
}
}| Field | Type | Description |
|---|---|---|
revisionId | integer | null | Revision the BOM belongs to; null while the project has no revision |
BOM line fields:
| Field | Type | Description |
|---|---|---|
id | integer | BOM line ID |
value | string | null | Component value |
footprint | string | null | Component footprint |
description | string | null | Component description |
referenceDesignators | string[] | Reference designators |
status | string | "resolved" | "unresolved" | "error" |
resolvedPartMpn | string | null | Matched MPN |
resolvedPartDescription | string | null | Matched part description |
resolvedManufacturer | string | null | Matched manufacturer |
Summary fields:
| Field | Type | Description |
|---|---|---|
total | integer | Total BOM lines |
resolved | integer | Matched to a purchasable part |
unresolved | integer | No match found |
errored | integer | Resolution 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 }| Field | In | Type | Description |
|---|---|---|---|
partId | body | integer | Required. Part ID from GET /parts |
Response: 200 OK
{
"partId": 5001,
"mpn": "LM358DR",
"manufacturer": "Texas Instruments"
}| Field | Type | Description |
|---|---|---|
partId | integer | Part ID that was set on the BOM line |
mpn | string | Manufacturer part number |
manufacturer | string | Manufacturer 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 }| Field | In | Type | Description |
|---|---|---|---|
dnp | body | boolean | Required. true to mark as DNP, false to unmark |
| Parameter | In | Type | Description |
|---|---|---|---|
ref | path | string | Required. Reference designator (e.g. "C3") |
Response: 200 OK
{
"ref": "C3",
"dnp": true
}| Field | Type | Description |
|---|---|---|
ref | string | Reference designator |
dnp | boolean | Current 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
| Field | Type | Description |
|---|---|---|
boardId | integer | Identity of this revision’s current board |
version | integer | Existing board version; changes when board settings or impedance requirements change |
updatedAt | string | ISO 8601 update time |
spec | object | Imported geometry and selected manufacturing settings |
impedances | array | Controlled-impedance requirements; empty when none |
stackup | object or null | Resolved stackup, when available |
possibleStackupCombinations | array | Available stackup combinations; may be empty |
messages | array | Fabrication messages to review |
hasCamReport | boolean | Whether a CAM report exists |
circuitHubCAMComments | string or null | CircuitHub 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
| Field | Type | Description |
|---|---|---|
expectedBoardId | integer | Required. boardId from the last read |
expectedVersion | integer | Required. version from the last read |
boardThickness | number | Selected thickness in mm; read as spec.selectedThicknessMM |
dielectricMaterial | string | For example FR4, Aluminum, Roger, RigidFlex or Flex |
tgLevel | string | Material glass-transition requirement: Low, Medium or High |
outerCopperWeightOz / innerCopperWeightOz | number | Copper weight in oz |
surfaceFinish | string | ENIG or ENEPIG |
soldermaskColor / silkscreenColor | string | Solder mask: black, purple, green, green-matte, red, yellow, blue, white, black-matte, orange. Silkscreen: white or black. Read as selectedSolderMaskColor and selectedSilkscreenColor |
goldFingers / goldFingersHardGoldPlating / goldFingersChamfer | boolean | Gold finger options; disabling fingers also disables plating and chamfer |
viaPlugging / viaFillAndCap | string | Via locations; read as smViaFillLocations and viaInPadFillLocations |
viaIsConductive | boolean | Conductive via fill requirement |
blindBuriedLayerSets | integer | Number of blind/buried layer sets |
panelizationMethod | string | Any, VScore or TabRoute |
needsCastellatedEdges | boolean | Read as spec.castellatedEdges |
hasCustomRequirements / hasCustomStackup | string | Read 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
| Field | Type | Description |
|---|---|---|
expectedBoardId / expectedVersion | integer | Required. Current board identity and version |
rows | array | Required. Complete list of impedance requirements |
rows[].id | integer or null | Existing row ID, or null for a new row |
rows[].layers | string | Applicable layer or layers |
rows[].width / rows[].space | number / number or null | Trace width and pair spacing in unit; spacing is null for single-ended traces |
rows[].unit | string | mm or mil |
rows[].mode | string | single-ended or differential |
rows[].transmissionLineType | string | Transmission line geometry, for example microstrip |
rows[].value | integer | Target impedance in ohms |
rows[].tolerance | integer | Allowed percentage deviation |
rows[].coupon | boolean | Whether a test coupon is required |
rows[].comments | string or null | Additional 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
| Field | Type | Description |
|---|---|---|
id | integer | Server-assigned saved address ID; not a create input |
name | string | Recipient name |
company | string or null | Recipient company |
street1 | string | Street address |
street2 / street3 | string or null | Additional address lines |
city | string | City |
state | string or null | State or region; required where the destination needs one |
zip | string or null | Postal code; required where the destination needs one |
countryCode | string | Two-letter country code, for example US |
phoneNumber | string | Contact 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}| Field | Type | Description |
|---|---|---|
revisionId | integer | Required. The specific revision to quote |
quantity | integer | Required. 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
| Field | Type | Description |
|---|---|---|
id | integer | Stable quote ID |
projectId | integer | Project containing the quoted revision |
revisionId | integer | Revision used for pricing |
revisionNumber | integer | Human-readable revision number |
quantity | integer | Requested board quantity; applies to every offer |
currency | string | Currency for all amounts; USD in this version |
createdAt | string | ISO 8601 quote creation time |
expiresAt | string | ISO 8601 expiry time |
locked | boolean | Whether the quote is locked. Read-only: the API cannot lock or unlock quotes |
status | string | ready or manual_quote_required; ready alone does not mean orderable |
type | string or null | assembled, board_only, or null when a manufacturing type is not available |
orderable | boolean | Whether the quote can currently be used to order; not a reservation of stock |
blockers | array | Reasons ordering is blocked, with affected BOM line IDs |
offers | array | Available manufacturing offers for the requested quantity; empty when manual review is required |
designFeedback | array | Changes 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
| Field | Type | Description |
|---|---|---|
id | integer | Stable offer ID; select this with the quote ID when ordering |
service | string | priority, standard, flexible, manual_quote, or board_only |
shipDate | object | earliest and latest calendar dates in YYYY-MM-DD format; equal for a single ship date |
fabricationCountry | string or null | Board fabrication country code, when known |
assemblyLine | string or null | Assembly line identifier, when applicable |
price | object | Manufacturing price, detailed below |
checkoutUrl | string | Portal 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.
| Field | Type | Description |
|---|---|---|
lineItems | array or null | Undiscounted manufacturing service amounts; null if not recorded |
manufacturingSubtotal | string or null | Sum of undiscounted line amounts; null if not recorded |
discounts | array or null | Discounts in application order; null if not recorded, [] if known to have none |
manufacturingTotal | string | Manufacturing amount after discounts, excluding shipping and tax |
manufacturingUnitPrice | string | Rounded manufacturing total divided by board quantity; for display only |
excludes | string[] | 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.
| Code | Meaning | What to do |
|---|---|---|
part_not_chosen | A BOM line has no approved part | Choose a suitable part |
no_supplier | No usable supplier for a selected part | Review the part and its sourcing |
insufficient_stock | Available stock cannot cover the build | Choose an alternative or change the quantity |
parts_require_review | An offer cannot be ordered for a reason no BOM line explains; bomLineIds is empty | Contact 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.
| Parameter | In | Type | Description |
|---|---|---|---|
quoteId | path | integer | Required. 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 status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid revision or quantity |
| 409 | REVISION_NOT_READY | The revision has not finished importing |
| 409 | QUOTE_LOCKED | A different quantity is currently locked |
| 410 | QUOTE_EXPIRED | The requested quote has expired |
| 429 | RATE_LIMITED | Request limit reached; follow Retry-After |
| 503 | QUOTING_UNAVAILABLE | Quoting 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
| Field | Type | Description |
|---|---|---|
shippingAddressId | integer | Required request field. Saved address available to the quote’s workspace |
shippingOptions[].id | string | Opaque service option ID to use in preview |
shippingOptions[].label | string | Service display name |
shippingOptions[].amount | string | Shipping charge before tax, as a decimal string |
currency | string | USD |
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
| Field | Type | Description |
|---|---|---|
quoteId / offerId | integer | Required. An offer belonging to this quote |
shippingAddressId | integer | Required. Saved delivery address |
shippingOptionId | string | Required. Option returned for this offer and address |
paymentMethodId | string | Required. Available workspace payment method |
Preview fields
| Field | Type | Description |
|---|---|---|
currency | string | Currency for all amounts; USD |
orderable | boolean | Whether this selection can currently be submitted |
blockers | array | Reasons it cannot be ordered; objects with code and message |
price.manufacturingSubtotal | string or null | Manufacturing price before discounts; null if historical detail was not recorded |
price.discounts | array or null | Positive savings, already reflected in manufacturingTotal |
price.manufacturingTotal | string | Manufacturing price after discounts |
price.shipping | string | Shipping charge before tax |
price.tax | string or null | Calculated tax; null when unavailable, never substituted with zero |
price.total | string or null | Manufacturing total + shipping + tax; null when tax is unavailable |
payment.amountDueNow / payment.amountDueLater | string or null | Amounts payable now and later under the selected method; null when a complete price cannot be calculated |
payment.terms | string or null | Approved payment terms when applicable |
taxStatus | string | calculated or unavailable; a calculated zero tax is valid |
quoteExpiresAt | string | Original 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.
| Field | Type | Description |
|---|---|---|
expectedTotal | string | Required. Exact decimal total reviewed in the preview, in the quote currency |
poNumber | string | Optional. 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 status | Code | Action |
|---|---|---|
| 400 | VALIDATION_ERROR | Correct the supplied selections or expected total |
| 409 | TOTAL_CHANGED | Review a new preview and submit its total |
| 404 | NOT_FOUND | A selection is not the workspace's; use IDs it lists |
| 409 | ORDER_NOT_ORDERABLE | Resolve stock, billing, shipping or payment eligibility blockers. A quote that has already been ordered is refused this way, and the message names its order |
| 410 | QUOTE_EXPIRED | Request a fresh quote, shipping options and preview |
| 422 | PAYMENT_FAILED | Review the payment failure; check for an existing order before trying again |
| 503 | TAX_UNAVAILABLE | Wait for tax calculation to recover and preview again |
| 503 | SHIPPING_UNAVAILABLE | Wait 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.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace | query | string | Required. 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:
| Field | Type | Description |
|---|---|---|
id | integer | Order ID |
urn | string | Order URN |
name | string | Order name/number |
status | string | Order status (e.g. in-progress, completed) |
quantity | integer | Number of boards ordered |
createdAt | string | ISO 8601 creation timestamp |
updatedAt | string | ISO 8601 last update timestamp |
quotedShippingDate | string | Quoted shipping date, ISO 8601 |
estimatedShippingDate | string | null | Estimated shipping date, ISO 8601 |
shippedDate | string | null | Actual ship date, ISO 8601 |
trackingNumber | string | null | Shipment tracking number |
shippingService | string | null | Shipping carrier/service |
value | number | Existing stored order value in USD; not the preview’s all-in total |
paymentStatus | string | Unpaid, Paid or PartiallyPaid |
poNumber | string | Purchase order number |
projectId | integer | Project this order belongs to |
projectName | string | Project name |
revisionId | integer | Project revision used for this order |
revisionNumber | integer | Human-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.
| Parameter | In | Type | Description |
|---|---|---|---|
q | query | string | Required. 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
}
]
}| Field | Type | Description |
|---|---|---|
query | string | Original search query |
exactMatches | array | Exact MPN matches (case-insensitive) |
inexactMatches | array | Fuzzy/related matches |
Part fields:
| Field | Type | Description |
|---|---|---|
id | integer | Part ID |
mpn | string | Manufacturer part number |
manufacturer | string | Manufacturer name |
description | string | null | Part description |
available | integer | Current stock availability |
moq | integer | null | Minimum 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.
| Parameter | In | Type | Description |
|---|---|---|---|
workspace | query | string | Required. Workspace slug |
order | query | integer | Filter by order ID (optional) |
status | query | string | Filter by status: open, closed (optional) |
POST /orders/:orderId/issues
Create a new issue on an order.
| Parameter | In | Type | Description |
|---|---|---|---|
orderId | path | integer | Required. Order ID to file the issue against |
| Field | In | Type | Description |
|---|---|---|---|
title | body | string | Required. Short summary of the issue |
comment | body | string | Optional 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
| Field | Type | Description |
|---|---|---|
name | string | Required. Legal name of the billed company or person |
isPerson | boolean | Required. True for an individual, false for a company |
email | string | Required. Billing contact email |
phone | string or null | Billing contact telephone |
address | object | Required. 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
| Field | Type | Description |
|---|---|---|
token | string | Required. 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.
| Parameter | In | Type | Description |
|---|---|---|---|
status | query | string | Filter: open, paid_in_full, approved_for_posting, fully_applied (optional) |
from | query | string | Start date, ISO 8601 (optional) |
to | query | string | End 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.
| Parameter | In | Type | Description |
|---|---|---|---|
startDate | query | string | Required. Start date, ISO 8601 |
endDate | query | string | Required. 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.
| Code | Meaning |
|---|---|
NOT_FOUND | Requested resource does not exist |
VALIDATION_ERROR | Request payload or query parameters were invalid |
INVALID_REDIRECT_URI | redirect_uri is not an absolute localhost URL (CLI login) or not an allowed MCP server callback (MCP sign-in) |
INVALID_GRANT | An MCP sign-in code is not valid, already used or expired, or its code_verifier or redirect_uri does not match |
SIGN_IN_NOT_CONFIRMED | A 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_CLIENT | POST /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_SCOPE | A connection token was used on an endpoint outside the scopes its user granted it (status 403); requiredScope names the scope (see Scopes) |
FILE_TOO_LARGE | Upload exceeded the maximum allowed size |
INVALID_UPLOAD_SIZE | Upload size was invalid |
INVALID_OBJECT_KEY | Uploaded object key did not match the workspace upload prefix |
FORBIDDEN | Authenticated user does not have permission to access the resource |
UNAUTHORIZED | Missing or invalid authentication |
RATE_LIMITED | Too many requests |
CREATION_FAILED | Project or revision creation failed after validation |
INTERNAL_ERROR | Unexpected server error |