All guides

AI assistants

API quickstart: upload, check, quote

The calls that take a board file to a file check report and a price preview, with curl and the JavaScript client, plus the errors you will meet.

For AI assistantsUpdated 2026-11-105 min readRaw Markdown for AI assistants

Before you start

The API works without an account through a guest session. The API is at the site's own address under /api. Set it once:

API=https://dev.uliketechnology.com/api
F=board.zip

board.zip is a flat zip of your Gerber and drill files. For what goes in it, see Files for fabrication and assembly. The examples need curl and jq.

Every request to the API after the guest session takes Authorization: Bearer <token>. Every error has the same shape:

{ "error": { "code": "VALIDATION_FAILED", "message": "…", "details": {}, "requestId": "…" } }

Branch on code. The message is for developers.

Five small pictures in order: a key, a file going to an upload address, an hourglass, a report page with findings, and a price tag
  1. Guest session: the token is shown once.
  2. Upload: PUT the zip to the address the API names, with the headers it names.
  3. Complete, then poll until the analysis is done.
  4. The report: findings, which checks ran and which did not.
  5. The quote preview.

The flow with curl

Read what can be ordered. This needs no token:

curl -s $API/v1/capabilities | jq '.options[].id'

Open a guest session. The token is shown once and lasts 30 days:

T=$(curl -s -X POST $API/v1/guest-sessions | jq -r .token)

Announce the file. The answer has uploadId, designId, uploadUrl and requiredHeaders:

J=$(curl -s -X POST $API/v1/uploads -H "authorization: Bearer $T" -H 'content-type: application/json' \
  -d "{\"fileName\":\"board.zip\",\"sizeBytes\":$(stat -c %s $F),\"sha256\":\"$(sha256sum $F | cut -d' ' -f1)\",\"purpose\":\"design\"}")

Send the file to the address the answer names, with every header in requiredHeaders (content type, length and checksum; without them the upload is refused with 403). Then complete the upload:

curl -s -X PUT --data-binary @$F \
  $(echo $J | jq -r '.requiredHeaders | to_entries[] | "-H \(.key):\(.value)"') \
  "$(echo $J | jq -r .uploadUrl)"
curl -s -X POST $API/v1/uploads/$(echo $J | jq -r .uploadId)/complete -H "authorization: Bearer $T" | jq .design.status

The status is analysing at first. Read the design until it changes. When it is ready, the report is in the answer:

D=$(echo $J | jq -r .designId)
curl -s $API/v1/designs/$D -H "authorization: Bearer $T" | jq '.design.status, .report.headline, .report.counts'

The headline is no_problems_in_checks_run, problems_found or could_not_check. Read checksRun and checksNotRun too: a check that did not run is not a pass. The other statuses are needs_mapping (set the layers with PUT /v1/designs/{designId}/layer-mapping), needs_engineer and rejected.

Ask for a price preview. selection and destinationCountry are required; an empty selection takes the defaults:

curl -s -X POST $API/v1/quote-previews -H "authorization: Bearer $T" -H 'content-type: application/json' \
  -d "{\"designId\":\"$D\",\"quantities\":{\"boardsFabricated\":10,\"boardsAssembled\":0},\"selection\":{},\"destinationCountry\":\"DE\"}" \
  | jq '.mode, .reviewReasons, .totalMinor'

mode is instant (the price stands as shown) or review (an engineer confirms it); reviewReasons says why. totalMinor is in minor units of the answer's currency, and is absent when an engineer has to price the job. To keep a quote, POST /v1/quotes takes the same body.

The same flow with the JavaScript client

The @ulike/sdk client wraps these routes. It is not published as a package yet; this is its shape, so that you can write the same calls with fetch. Method names:

import { createClient } from '@ulike/sdk';

let token;
const api = createClient({ baseUrl: 'https://dev.uliketechnology.com/api', token: () => token });

const caps = await api.capabilities();
token = (await api.createGuestSession()).token;
const started = await api.uploadDesign({ name: 'board.zip', bytes }); // announce, PUT, complete
const view = await api.waitForDesign(started.design.id); // polls getDesign
const preview = await api.previewQuote({
  designId: view.design.id,
  quantities: { boardsFabricated: 10, boardsAssembled: 0 },
  selection: {},
  destinationCountry: 'DE',
});

The other methods are createUpload, completeUpload, getDesign, setLayerMapping, validateDesign, getValidation, waitForValidation, createQuote, getQuote and reviseQuote. Errors are thrown as ApiClientError with a status and a code.

The check for an order

The report you get after upload is made on the files alone. To check the files against an order, call POST /v1/designs/{designId}/validations (validateDesign) with order holding quantities and selection. The answer is a state: running, failed or done. While running, read GET /v1/validations/{reportId} (getValidation) every second or two until it is done.

A loop of three steps: upload the files, read the report, fix and upload again; an arrow leaves the loop to a tick when the files are clean
  1. Upload the zip.
  2. Read the report.
  3. Fix and upload as a new design. Stop and ask the person when a fix changes the function, the price or the delivery time.
  4. Clean files: run the check for the order.

A quote or preview with options that bring other limits (thicker copper, another thickness) needs a check for that selection. If none exists, one is started, and the preview carries the review reason file_check_pending and a check with its reportId. The price is shown at once, but the quote stays in review until that check is done. Read the check as above, then ask for the preview again.

Errors you will meet

Code Status What it means What to do
DESIGN_NOT_READY 422 The design is still being read (details.status says which state), or has no report yet. Read the design until the status is not analysing, then ask again.
VALIDATION_FAILED 400 The request does not have the expected shape, or names an option or value the catalog does not have. details lists the issues or problems. Fix the field named in details and send again. Take option ids and values from GET /v1/capabilities.
NOT_MANUFACTURABLE 422 The selection is something we do not make. details.because carries the reasons. Change the selection; do not retry the same one. Ask the person if the change would alter the board.
ANONYMOUS_BUDGET_SPENT 429 The daily allowance without an account is used up. details.resetsOn is the date. Stop. Tell the person; they can sign in or wait until that date.
REVISION_CONFLICT 409 A revision named a base that is no longer the head of the quote. details carries both heads. Read the head with GET /v1/quotes/{quoteId}, reapply your change on it, and send again.

Also possible are DESIGN_REJECTED (the upload was refused; see details.reason), UNAUTHENTICATED for a missing or wrong token (a wrong token is refused even on public routes), and FEATURE_DISABLED for a route that is not open yet.

The daily allowance without an account

A guest session has a daily allowance of compute. Uploads and started checks cost more than a plain read, and opening many guest sessions from one network address is also counted. A check that the site starts on its own for a new selection is charged to the same allowance. When it is spent, you get ANONYMOUS_BUDGET_SPENT with the date it resets. Signed-in callers are not counted this way.

What the API cannot do today: ordering, payment, API keys and sign-in for agents are planned and not available. The planned MCP server is described in What MCP is and how agents order.

Try it on a real board

Upload a board and see the 25 checks on it.

Upload a board