---
title: 'API quickstart: upload, check, quote'
summary: 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.
category: ai
audience: agents
order: 3
updated: 2026-11-10
---

## 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:

```sh
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](https://dev.uliketechnology.com/learn/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:

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

Branch on `code`. The `message` is for developers.

Drawing: 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:

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

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

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

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

```sh
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:

```sh
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:

```sh
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:

```sh
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:

```js
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`.

Drawing: 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](https://dev.uliketechnology.com/learn/what-mcp-is-and-how-agents-order).
