Developers

API reference

The fabPlane API manages organizations, shopping carts (a BOM where each line has a purchase destination) and a shared team inventory. The fabplane CLI wraps every endpoint here, and the machine-readable OpenAPI 3.1 document lists each operation by its operationId.

Authentication

Base URL https://api.fabplane.com. Every route below is under /v1/private and accepts either the browser session cookie or Authorization: Bearer <token>, where the token is a personal API token (fpk_…, created in the dashboard or with POST /v1/private/tokens), a device token (fpd_…) from fabplane login, or a bot token (fpb_…) for an agent.

export FABPLANE_TOKEN=fpk_…
curl https://api.fabplane.com/v1/private/orgs -H "Authorization: Bearer $FABPLANE_TOKEN"

Conventions

Me and API tokens

Personal API tokens start with fpk_, act as you, and are stored only as a SHA-256 hash. The secret is returned once, when you create the token; the dashboard at app.fabplane.com/dashboard/tokens does the same.

GET /v1/private/me [getMe]

Principal plus email, emailVerified and personalOrgId.

GET /v1/private/tokens [listTokens]

Your tokens (never the secret).

POST /v1/private/tokens [createToken]

{ name, expiresInDays? 1–3650 } → 201 { token, secret }.

DELETE /v1/private/tokens/{tokenId} [deleteToken]

Revoke → 204.

curl -X POST https://api.fabplane.com/v1/private/tokens \
  -H "Authorization: Bearer $FABPLANE_TOKEN" -H "content-type: application/json" \
  -d '{"name":"ci","expiresInDays":90}'

Organizations

Every account has one personal org (it can invite people, but cannot be deleted or use domain join). Roles: member reads the org and reads/writes carts and inventory; admin adds invites, member roles, destinations, domain join and rename; owner adds delete and owner changes. The last owner cannot leave or be demoted (409). Orgs you are not in answer 404.

GET /v1/private/orgs [listOrgs]

Your memberships.

POST /v1/private/orgs [createOrg]

{ name, slug? } → 201; 409 when the slug is taken.

GET /v1/private/orgs/{orgId} [getOrg]

One org.

PATCH /v1/private/orgs/{orgId} [updateOrg] · admin

{ name?, domainJoin?: { domain, enabled } | null }.

DELETE /v1/private/orgs/{orgId} [deleteOrg] · owner

Deletes carts, inventory and images too.

GET /v1/private/orgs/joinable [listJoinableOrgs]

Orgs your verified email domain may join.

POST /v1/private/orgs/{orgId}/join [joinOrg]

Join through domain join; 403 if not eligible.

GET /v1/private/orgs/{orgId}/members [listMembers]

Members with role and email.

PATCH /v1/private/orgs/{orgId}/members/{userId} [updateMember] · admin

{ role }; only owners grant or revoke owner.

DELETE /v1/private/orgs/{orgId}/members/{userId} [removeMember] · admin or self

Remove a member, or leave.

GET /v1/private/orgs/{orgId}/invites [listInvites] · admin

Pending invites with their links.

POST /v1/private/orgs/{orgId}/invites [createInvite] · admin

{ email?, role? admin|member, expiresInDays? 1–30 } → invite with url. No email is sent.

DELETE /v1/private/orgs/{orgId}/invites/{inviteId} [revokeInvite] · admin

Revoke → 204.

GET /v1/private/invites/{token} [getInvite]

Preview an invite link.

POST /v1/private/invites/{token}/accept [acceptInvite]

Single use. Email-bound invites need your verified email to match (403).

curl -X PATCH https://api.fabplane.com/v1/private/orgs/$ORG \
  -H "Authorization: Bearer $FABPLANE_TOKEN" -H "content-type: application/json" \
  -d '{"domainJoin":{"domain":"example.com","enabled":true}}'

Purchase destinations

Where cart items are bought. Built-ins have ids like builtin:jlcpcb, builtin:pcbway, builtin:oshpark, builtin:digikey, builtin:mouser, builtin:lcsc, builtin:arrow, builtin:farnell, builtin:aliexpress, builtin:amazon and builtin:manual. Admins add regional or private ones.

GET /v1/private/orgs/{orgId}/destinations [listDestinations]

Built-ins first, then the org's own.

POST /v1/private/orgs/{orgId}/destinations [createDestination] · admin

{ name, url, kind? fab|distributor|manual, baseId? }.

DELETE /v1/private/orgs/{orgId}/destinations/{destinationId} [deleteDestination] · admin

Custom only; its items move to builtin:manual.

curl -X POST https://api.fabplane.com/v1/private/orgs/$ORG/destinations \
  -H "Authorization: Bearer $FABPLANE_TOKEN" -H "content-type: application/json" \
  -d '{"name":"DigiKey Thailand","url":"https://www.digikey.co.th","baseId":"builtin:digikey"}'

Carts (BOM with purchase destinations)

A cart is a shopping list or BOM, optionally tagged with repos and a client project id. Items without a destinationId go to the cart's fabDestinationId (JLCPCB unless changed): the fab house sources everything unless you move a line to a distributor or to manual purchase. Repo filters are normalized (lowercase host, no .git or trailing slash, git@host:o/r becomes https://host/o/r). Moving is a dashboard-only, one-way personal-to-team operation: custom destinations collapse to their built-in base or manual, and source-org inventory links are cleared.

GET /v1/private/orgs/{orgId}/carts?repo=&projectId= [listCarts]

Cart summaries, newest first.

POST /v1/private/orgs/{orgId}/carts [createCart]

{ name, repos?, projectId?, notes?, fabDestinationId? }.

GET /v1/private/orgs/{orgId}/carts/{cartId} [getCart]

Cart with items and byDestination totals.

PATCH /v1/private/orgs/{orgId}/carts/{cartId} [updateCart]

Change name, repos, projectId, notes or fab.

DELETE /v1/private/orgs/{orgId}/carts/{cartId} [deleteCart]

→ 204.

POST /v1/private/orgs/{orgId}/carts/{cartId}/move [moveCart] · human

{ targetOrgId }; personal workspace → a team workspace you belong to. 400 unsupported direction, 403 bot, 404 inaccessible cart or target.

POST /v1/private/orgs/{orgId}/carts/{cartId}/items [addCartItems]

{ items: CartItemInput[1..500] } → 201.

PUT /v1/private/orgs/{orgId}/carts/{cartId}/items [replaceCartItems]

{ items: CartItemInput[0..2000], source? } replaces every line (BOM sync).

PATCH /v1/private/orgs/{orgId}/carts/{cartId}/items/{itemId} [updateCartItem]

Any CartItemInput fields: destinationId, quantity, status…

DELETE /v1/private/orgs/{orgId}/carts/{cartId}/items/{itemId} [deleteCartItem]

→ 204.

GET /v1/private/orgs/{orgId}/carts/{cartId}/export.csv?destinationId= [exportCartCsv]

text/csv, optionally one destination.

curl -X PUT https://api.fabplane.com/v1/private/orgs/$ORG/carts/$CART/items \
  -H "Authorization: Bearer $FABPLANE_TOKEN" -H "content-type: application/json" \
  -d '{"source":"fabdesk","items":[
        {"kind":"pcb","description":"Main board","quantity":5},
        {"mpn":"RC0402FR-0710KL","refs":["R1","R2"],"quantity":10,"sku":"C25744"},
        {"mpn":"LM1117-3.3","quantity":5,"destinationId":"builtin:digikey"}]}'

Team inventory

Parts and boards your team has on hand. externalId (with source) is an idempotency key: posting the same pair again updates the item and answers 200. Photos go through the API into object storage and come back as 15-minute signed URLs.

GET /v1/private/orgs/{orgId}/inventory?q=&category=&location=&tag=&limit=&cursor= [listInventory]

{ items, nextCursor }; q matches name, MPN, manufacturer, description and SKU.

POST /v1/private/orgs/{orgId}/inventory [createInventoryItem]

JSON, or multipart with item (JSON) and image file(s). 201, or 200 on upsert.

POST /v1/private/orgs/{orgId}/inventory/bulk [bulkUpsertInventory]

{ items[1..500] } → { items, created, updated }.

GET /v1/private/orgs/{orgId}/inventory/{itemId} [getInventoryItem]

One item.

PATCH /v1/private/orgs/{orgId}/inventory/{itemId} [updateInventoryItem]

Partial update.

DELETE /v1/private/orgs/{orgId}/inventory/{itemId} [deleteInventoryItem]

Also deletes its photos.

POST /v1/private/orgs/{orgId}/inventory/{itemId}/adjust [adjustInventory]

{ delta, reason? }, atomic; 409 if it would go below zero.

POST /v1/private/orgs/{orgId}/inventory/{itemId}/images [uploadInventoryImage]

multipart image (plus optional source and http(s) sourceUrl fields), or a raw image body. ≤10 MiB, png/jpeg/webp/gif/heic/heif, ≤10 per item.

GET /v1/private/orgs/{orgId}/inventory/{itemId}/images/{imageId} [getInventoryImage]

302 to a signed URL.

DELETE /v1/private/orgs/{orgId}/inventory/{itemId}/images/{imageId} [deleteInventoryImage]

→ 204.

curl -X POST https://api.fabplane.com/v1/private/orgs/$ORG/inventory \
  -H "Authorization: Bearer $FABPLANE_TOKEN" \
  -F 'item={"name":"ESP32-S3 module","mpn":"ESP32-S3-WROOM-1","quantity":12,"location":"Lab / drawer A3","source":"manual","externalId":"esp32-s3-a3"}' \
  -F image=@module.jpg

curl -X POST https://api.fabplane.com/v1/private/orgs/$ORG/inventory/$ITEM/adjust \
  -H "Authorization: Bearer $FABPLANE_TOKEN" -H "content-type: application/json" \
  -d '{"delta":-2,"reason":"rev B build"}'

Inventory photo queue

Items with no photo that are not marked skipped form the org's photo queue. A worker on your machine (for example a local AI job) claims items with a lease, finds a product photo, and uploads it with source and sourceUrl — which takes the item out of the queue — or releases it. Deleting an item's last photo puts it back. Every inventory item carries photoSearch { status, attempts, note, leaseUntil, leaseOwner }.

GET /v1/private/orgs/{orgId}/inventory/photo-queue?limit=&cursor=&include=available|all [listPhotoQueue]

{ items, nextCursor, counts: { queued, available, leased, skipped } }. available (default) lists claimable items; all adds leased and skipped ones.

POST /v1/private/orgs/{orgId}/inventory/photo-queue/claim [claimPhotoQueue]

{ limit? 1–25 (5), leaseSeconds? 60–3600 (900), worker? } → { items, leaseUntil }. Atomic: parallel workers never get the same item.

POST /v1/private/orgs/{orgId}/inventory/{itemId}/photo-queue/release [releasePhotoQueueItem]

{ outcome: retry | not_found, note?, leaseToken? }. Pass the leaseToken from your claim; without it only unleased items can be released (409 otherwise). retry drops the lease; not_found marks the item skipped.

POST /v1/private/orgs/{orgId}/inventory/{itemId}/photo-queue/requeue [requeuePhotoQueueItem] · admin

Back to queued with zero attempts.

curl -X POST https://api.fabplane.com/v1/private/orgs/$ORG/inventory/photo-queue/claim \
  -H "Authorization: Bearer $FABPLANE_TOKEN" -H "content-type: application/json" \
  -d '{"limit":5,"worker":"openclaw"}'

curl -X POST https://api.fabplane.com/v1/private/orgs/$ORG/inventory/$ITEM/images \
  -H "Authorization: Bearer $FABPLANE_TOKEN" \
  -F image=@photo.jpg -F source=web -F sourceUrl=https://www.example.com/product-page

Bot accounts

Agents such as openclaw, Hermes, CI jobs and fabdesk automations join an org as bot members with their own fpb_ token, never with a person's session. A bot works only inside its own org, with its role, on carts, inventory, the photo queue, destinations and the member list; other orgs answer 404. It gets 403 on org create and delete, invites, joining, domain join, API tokens, bot management and settings. Bots show up in the member list with kind: "bot"; removing one from the org deletes it. The connect flow works like device login: the agent prints a link and code, an org admin approves it in the dashboard, and the agent's next poll receives the token once.

POST /v1/auth/bot/connect [startBotConnect]

Anonymous, rate limited. { name, clientId, org?, agentKind? } → { connectCode, userCode, verificationUri, verificationUriComplete, expiresIn: 900, interval: 5 }.

POST /v1/auth/bot/token [pollBotConnect]

{ connectCode } → 428 authorization_pending, 429 slow_down, 400 access_denied or expired_token, or 200 { accessToken, tokenType, bot, org } exactly once.

GET /v1/private/bots/connect/{userCode} [getBotConnectRequest]

The request and the orgs you administer.

POST /v1/private/bots/connect/{userCode}/approve [approveBotConnect] · admin of orgId

{ orgId, role? member|admin } → { bot }. Personal orgs are allowed.

POST /v1/private/bots/connect/{userCode}/deny [denyBotConnect]

→ 204.

GET /v1/private/orgs/{orgId}/bots [listBots] · admin

Bots with agent kind, role and last use.

POST /v1/private/orgs/{orgId}/bots [createBot] · admin

{ name, role?, agentKind? } → 201 { bot, token }. For headless setups; the token is shown once.

POST /v1/private/orgs/{orgId}/bots/{botId}/tokens [rotateBotToken] · admin

New token; earlier ones stop working.

DELETE /v1/private/orgs/{orgId}/bots/{botId} [deleteBot] · admin

Revokes its tokens and removes it.

# on the agent's machine
curl -X POST https://api.fabplane.com/v1/auth/bot/connect -H "content-type: application/json" \
  -d '{"name":"openclaw on bench-pc","clientId":"fabplane-cli","agentKind":"openclaw"}'
# open verificationUriComplete as an org admin and approve, then poll every interval seconds:
curl -X POST https://api.fabplane.com/v1/auth/bot/token -H "content-type: application/json" \
  -d '{"connectCode":"<connectCode>"}'

Cart item fields

FieldNotes
kindpcb, part (default) or other
mpn, manufacturer, description, value, footprintFree text
refsDesignators, up to 500
quantityRequired integer ≥ 0
destinationIdbuiltin:<key> or a custom destination id; omitted means the cart's fab
sku, url, unitPrice, currencyDistributor part number (e.g. LCSC C-number), link, price, ISO 4217 code
statusneeded (default), ordered, received
notes, inventoryItemIdNotes, and a link to an inventory item in the same org

CSV export columns: destination,kind,mpn,manufacturer,description,refs,quantity,sku,url,unitPrice,currency,status,notes.

Inventory item fields

name (required), mpn, manufacturer, sku, category, description, quantity (default 0), unit (default pcs), location, tags (up to 50), attributes (up to 100 keys of string, number, boolean or null), source and externalId.

Server-side AI

fabPlane does not yet extract part data from photos on the server. Sending serverAiProcessing: true returns 501 server_ai_unavailable and stores nothing. Run a model locally (fabdesk and the CLI's MCP server work with Claude Code or Codex on your machine), then send the fields it extracts, putting anything semi-structured in attributes.

Errors

Errors look like { "error": "snake_case_code", "message": "optional detail" }.

StatuserrorWhen
400invalid_json, invalid_requestBody is not JSON, or fails validation (unknown keys are rejected).
401unauthorizedNo valid session cookie or bearer token.
403forbiddenYour role is too low, or you are not eligible (invites, domain join).
404not_foundMissing, or in an org you do not belong to.
409conflictSlug taken, last owner, quantity below zero, image limit, duplicate externalId.
413payload_too_largeImage over 10 MiB.
415unsupported_media_typeImage type not accepted.
501server_ai_unavailableserverAiProcessing: true was sent.
503authentication_unavailable, storage_unavailableSign-in or object storage is not configured on this server.