Skip to content

Brick Work API v2: contract

Status: the contract of the whole v2 surface, written for M.11a (issue #113, 2026-10-27). The transport, the meta endpoints and item addressing are implemented in M.11a, the reads in M.11b, the writes in M.11c and the orders, jobs, sources, sync, imports, identifications, tokens and admin operations in M.11d, and the MCP and the parity test in M.11e: every operation of the contract is implemented (a piece that has not landed answers 501 not_implemented, none is left). The machine-readable twin is openapi-v2.yaml; the data model behind it is data-model-design.md (sections 3.1, 3.2, 3.5, 3.6, 3.8 and 7).

How to read this file. v2 is v1 with the 4.0 identity, not a redesign (api-v1.md is the v1 contract and stays as the reference for what v2 inherits). Section 1 says what changed and what did not. Section 2 holds the conventions, section 3 the resource catalogue with every v1 operation mapped to its v2 operation (or dropped, with the reason), section 4 the pieces and the 501 list. Each operation carries the piece that implements it: M.11a the contract, transport, meta and item addressing; M.11b the reads; M.11c the writes; M.11d orders, jobs, sources, keys, sync, imports, identifications, tokens and admin; M.11e the MCP regenerated from this file and the parity test (no operations of its own).


1. What changed from v1, and what did not

v1 answers 410 Gone on the 4.0 line (its /health excepted) because it cannot represent local or unidentified items; the 410 text names /api/v2 and this file.

Unchanged (a client changes only how it names items): envelopes (data, page, write receipts), cursor pagination and the filter vocabulary, ETag / If-Match, Idempotency-Key, problem+json errors with stable codes, bearer tokens with the read < write < admin scopes, BW_API_TOKEN, the throttle on failed token checks, read-only instances, archive instead of delete, one event per write with via=api and actor = token name, and Undo through POST /events/{id}/undo. The code behind them is shared with v1 (brickwork/api/transport.py).

Changed:

Topic v1 v2
Item address /items/{type}/{no}, BrickLink type letter and number /items/{id} (Brick Work id, per install) and number paths: /sets/by-number/{no}, /parts/by-number/{no}, /minifigures/by-number/{no}, GET /lookup?provider=&kind=&key=
Item in a body item_type, item_no item: {id, kind, name, unidentified, bricklink: [{type, no, color_id, preferred}], rebrickable: {key} or null}
Colour in a body color_id (BrickLink) color: {id, name, rgb, bricklink_id, rebrickable_id}
Item in a request item_type, item_no, color_id item: {id} or item: {provider, kind, key} and color in the same provider space; 422 unidentified when the number resolves to nothing
The ? type placeholder type for unidentified items gone: unidentified: true
Unresolved matches /unresolved, /id-matches links: /links?status=to_check, /items/{id}/links, /items/{id}/provider-status, merge and unmerge
Merged ids none 307 to the survivor on GET, 409 merged on writes
Codes mapped to words some stored as letters words stored and served (Q8); the v1 mappings are dropped
Wish ids integers uuids (Q6)
Copy body no part-list state adds inventory: {source, version, ordinal, of, pending, changed_since_build}
Settings v1 whitelist adds prefer_bricklink_inventory, candidate_sets_common_max and the picture order inside source_priority
Catalogue status loaded, counts builds (dump, mapping version, local mapping manifest, BrickLink list), coverage per link status, last reconcile
MCP tools take BrickLink type and number regenerated from this file; tools take numbers and a provider, never a Brick Work id; tool names stay where the operation exists

Ids are per install (data model 3.1). A Brick Work id from one install means nothing in another, and a restore from a different archive can renumber. Clients store provider numbers and use ids only within a session.


2. Conventions

2.1 Identity and addressing

Thing Address Notes
Item /items/{id}, a canonical integer (no sign, no leading zero; anything else is 404, so a redirect's Location never equals the URL it answers) Catalogue ids are running numbers from the catalogue build (up to six digits today); local items (BrickLink-born, API-born, unidentified) start at 1,000,000,000. An item the collection has not projected yet is still addressable: the read falls back to the attached catalogue.
Item by number /sets/by-number/{no}, /parts/by-number/{no}, /minifigures/by-number/{no}, GET /lookup?provider=&kind=&key= Same item body as /items/{id}, answered with 200, not a redirect, so a client needs one round trip.
Colour color query parameter or body member In the provider's space (below). GET /colors/{id} takes a Brick Work colour id.
Copy, stock line, lot, storage, place, flag uuid text As v1.
Wish uuid text Was an integer.
Wishlist text id (default for the first) As v1.
Event, job, order, order line, instruction file integer As v1.

Number paths and /lookup (decisions 2 to 4 of #113):

  • provider is bricklink (the default) or rebrickable. BrickLink is the default because every current client, order e-mail and the House API speak BrickLink numbers. A set number is the same at both providers, so a set is looked up at both and the answers merge.
  • kind is set, part, minifigure, instructions, box, gear, book or catalog; the three number paths fix it. Rebrickable has numbers only for sets, parts and minifigures; another kind with provider=rebrickable is 422.
  • A BrickLink number resolves through the item's links (every confirmed link: manual, matched, inferred) and through a BrickLink-born local item's own number. A Rebrickable number resolves through the born key.
  • A BrickLink figure number the catalogue holds as a part (#150). BrickLink files the old Technic figures (tech028) as minifigures, where the catalogue links the same number to a Rebrickable part. With provider=bricklink and kind=minifigure, a number that no minifigure holds but exactly one part holds, as that very same BrickLink number, answers with the part (kind: part). A catalogue minifigure always wins; two parts keep the answer 404; no other number is tried. An order line or a BrickLink subsets list that names such a figure becomes a part line in no colour.
  • Colour-scoped links. With color, the colour is read in the provider's space (bricklink: a BrickLink colour id, rebrickable: a Rebrickable colour id) and the lookup is the inverse of the resolution function (data model 3.2): a link scoped to that colour applies, an unscoped link applies only when the item has none scoped to the colour (the leg rule), and a manual link decides alone. The answer then carries data.color, the resolved Brick Work colour, so a client can map provider colours. Without color, a link of any scope matches.
  • A BrickLink-born local item's own number is an unscoped claim that follows the same precedence: a manual link for the number decides alone, and the item does not answer by its born number in a colour it has a scoped link for. Across different items a colour-scoped link and an unscoped one both apply, so they are 409 ambiguous together.
  • One match is 200 with the item body. Several is 409 ambiguous with candidates (item bodies) and the problem fields; none is 404. A merged item found by number is followed to its survivor silently.
  • Unknown query names are 400 unknown_filter. A colour that names no colour, or a bad kind or provider, is 422 validation_failed.

Colours in a request are in the provider's space (decision 4): a BrickLink colour id with provider=bricklink, a Rebrickable colour id with provider=rebrickable, and a Brick Work colour id with provider=brickwork. The brickwork space is for paths that already name the item by id (/items/{id}/...); on a number path it is 422, since a Brick Work id is not a number.

Item and colour bodies (data model 7):

{"id": 15011, "kind": "set", "name": "...", "unidentified": false,
 "bricklink": [{"type": "S", "no": "6081-1", "color_id": null, "color_scoped": false, "brickwork_color_id": null, "preferred": true}],
 "rebrickable": {"key": "6081-1"}}

bricklink lists the confirmed BrickLink links, preferred first; a colour-scoped link names its BrickLink colour in color_id and has color_scoped: true, an unscoped one has color_id: null and color_scoped: false. A link scoped to a colour that has no BrickLink id also reads color_id: null, but with color_scoped: true: it never reads as "every colour", and brickwork_color_id (set exactly when color_scoped) names the scope in Brick Work's own space. (color_scoped and brickwork_color_id are additions to the design 7 shape, made for this case.) A BrickLink-born local item with no link of its own lists its born number. rebrickable is the born key or null. unidentified is true for a local item that is not born at Rebrickable (data model 3.8): no born key, or a BrickLink-only number. Every field of a row that names an item (item, child, parent_minifig, part, figure) carries this body, and every colour (color) the colour body {id, name, rgb, bricklink_id, rebrickable_id}.

Merged ids (decision 5, amended 2026-10-27):

  • GET of a merged id (any /items/{id}... path) answers 307 Temporary Redirect with Location to the same path under the survivor (the query string is kept), Cache-Control: no-store and a problem body (code: moved, survivor).
  • This deviates from data model 3.6 and 7, which say 301. A merge can be undone exactly (unmerge restores every moved row), and a cached permanent 301 would keep sending the unmerged id to the old survivor. 307 also keeps the method, though only GET redirects.
  • Resolving an id and serialising its item are one read snapshot, so a merge that commits in between never mixes two states in one answer.
  • A write that names a merged id is 409 merged with survivor; nothing is written, since a client that silently writes to a moved id would surprise. POST /items/{id}/unmerge is the one write that names a merged id on purpose.

2.2 Value formats

As v1: money is a decimal string with a currency code ^[A-Z]{3,8}$; time is ISO-8601 UTC with offset; dates the user types are stored as given. Stored words are served as stored (Q8): conditions new, used (stock adds damaged; wishes new, used, any), completeness sealed, complete, incomplete with completeness_source manual or derived, kinds as above. The v1 code-to-word mappings are gone because nothing is stored as a letter any more.

2.3 Envelopes

  • Single read: {"data": {...}} with ETag on copies, stock and wishes.
  • List: {"data": [...], "page": {"next_cursor": "...|null", "limit": 50, "total": 123|null}}.
  • Write: {"data": {...}, "event_id": 412, "undo_url": "/api/v2/events/412/undo", "idempotent_replay": false}.
  • Queued work: 202 Accepted, the write envelope with the Job as data, header Location: /api/v2/jobs/{id}.
  • Every response of the API carries Cache-Control: no-store.

2.4 Resource states

As v1: archived rows are hidden from lists (archived=true|include|only), a GET of an archived record returns it with archived_at, writes to one fail with 409 archived.

2.5 Pagination, filters, sort

As v1: opaque cursor, limit 1 to 200 (default 50), sort and direction with a stable tie-break, unknown filter names are 400 unknown_filter. The filters that named an item by BrickLink type and number change: item_type becomes kind, item_no becomes number with provider, and color_id / colour become color (in the provider space, default bricklink; provider=brickwork takes a Brick Work colour id). type on GET /items is kind.

2.6 ETag and If-Match

As v1: copies, stock and wishes carry an ETag; PATCH, PUT and DELETE accept If-Match; a mismatch is 412 precondition_failed and nothing is written. POST /copies/{id}/inventory accepts If-Match as well, since a switch rewrites the copy's lines.

2.7 Idempotency

As v1: Idempotency-Key: <1 to 80 characters of [A-Za-z0-9-]>, stored in the one event row of the request; same key and body replays the stored response (idempotent_replay: true), another body is 409 idempotency_conflict, a key is honoured for 30 days. Fingerprint version: the request fingerprint (method, path, sorted query, body, file hash) carries a version v (now 1), and the receipt stores the version it was made under; a replay is compared under the receipt's own version, so a later change to what is hashed bumps v and old receipts keep replaying. Required on: creates (POST /copies, /stock, /wishes, /found, /stock/booklets, /lots, /wishlists) and the wishlist edits (PATCH and DELETE /wishlists/{id}, POST /wishlists/{id}/restore and /transfer), quantity moves (/copies/{id}/take, /stock/{id}/give, /wishes/{id}/to-owned, /storages/{id}/contents, /copies/{id}/parts/bulk), orders (/orders/documents, /orders/{id}/allocate, /orders/{id}/allocations/retry), prices (/prices/observations, /items/{id}/prices), /events/{id}/undo, and the v2 identity writes (PUT /items/{id}/links, POST /items/{id}/merge, POST /items/{id}/unmerge, POST /items/{id}/change-variant, POST /copies/{id}/inventory). A missing key is 428 idempotency_key_required. Optional elsewhere.

2.8 Errors

Media type application/problem+json, as v1:

{"type":"urn:brickwork:error:ambiguous","title":"Ambiguous","status":409,"code":"ambiguous",
 "detail":"bricklink part 3001 names 2 items; address one by id",
 "candidates":[{"id":44280,"kind":"part","name":"...","unidentified":false,"bricklink":[],"rebrickable":{"key":"3001"}}],
 "instance":"/api/v2/parts/by-number/3001"}

Optional members: retry_after, job_id, source, and in v2 candidates (with ambiguous), survivor (with merged and moved), piece and error (with not_implemented). Codes and statuses are in Appendix A; x-error-status in the OpenAPI file lists the same table. A path under /api/v2 that does not exist is 404 not_found and a wrong method 405 method_not_allowed with Allow, both problem+json.

2.9 Authentication, scopes, tokens

As v1: Authorization: Bearer <token>; read < write < admin; BW_API_TOKEN is a bootstrap admin token named bootstrap; stored tokens are hashed, scoped and may expire; with neither, 503 api_disabled. /health and /openapi.yaml need no token. BW_READ_ONLY makes every non-GET 403 read_only_instance whatever the scope. Failed token checks back off per client (429 too_many_attempts, Retry-After); forwarded addresses count only with BW_TRUSTED_PROXIES. The scope of every v2 operation is the one its security entry in openapi-v2.yaml names; the blueprint reads it from there, so the contract is the single place that says who may call what. Request bodies are refused unread over their cap (JSON 4 MB, uploads per route) with 413 payload_too_large.

2.10 Reads (M.11b) and writes (M.11c)

What the 60 read operations (M.11b) and the 67 write operations (M.11c, further down) do beyond v1, and where the first contract text changed when they were built (each is also in openapi-v2.yaml).

  • The query is the contract's. A query name an operation does not declare is 400 unknown_filter, a name given twice is 400 bad_request unless the contract declares it an array (copy_id on the exports). limit, direction, cursor and sort are accepted where the operation lists them.
  • Filters on an item or colour take the provider space of decision 4: number (with provider, bricklink by default, rebrickable) finds the live items that number stands for in any kind, or in kind; color is read in the same provider space, and provider=brickwork is for color only (a Brick Work id is not a number: number with it is 422). A filter that names nothing is an empty list; a colour that does not exist is 422.
  • Lists are filtered and sorted over the rows the screens use, and paged with the cursor of v1: it holds the sort position, never an offset, so rows that arrive meanwhile never move a page. page.total is the exact count of the filtered rows (v1 left it null under q and archived); the SQL-paged histories (/events, /items/{id}/prices/history) give total: null (a long log is paged by its key, never counted), and /catalogue/items is paged in SQL with the exact count of the entries that match. archived=true|include|only as before; status=unknown is a copy with no recorded completeness; storage, place, lot take null for none.
  • Rows that name an item or a colour carry the bodies of section 2.1, built for a whole page in a fixed number of queries. The tie-break of every sort is the row's key, so a page is stable.
  • etag is in the body of a copy, stock line or wish on lists and single reads; the ETag header is on the single read. It is a hash of the stored row and its flag ids, so a change of the item's name does not move it.
  • A merged id is 307 on every /items/{id}/... path and on GET /minifigures/{id}; the Location keeps the rest of the path and the query.
  • Corrections to the first contract text, found by validating every answer against it: the id of an Image is the picture number or the 32-character hex id of an own photo (/images/{id}/content takes either); Lot.created_date is created_at; StaleItem.source_table is copy, stock or wish (the 4.0 tables); ItemLink says color_scoped and brickwork_color_id as the item body does; ItemDetail also carries used_in, gaps and matches; the unpriced lines of a copy's part-out value are {item, color}; GET /export/missing.json names its lines by item and colour body with the BrickLink type, number and colour number beside them, and lists the lines the XML must leave out under skipped; GET /wishlists pages like every list; the virtual location "Not stored" has the id unstored (null stays the filter spelling); has_file on GET /instruction-files is whether the PDF is on disk.
  • Gaps of a missing booklet or box name the copy's set as their item (target instructions or box, target_id the copy id); every other gap names the item that is missing.
  • The catalogue is the attached catalogue.db, not v1's BrickLink file. GET /catalogue/items finds a text by number (the Rebrickable number, then any linked BrickLink number) and by name; without the file the answer is 424 catalogue_not_loaded.

Writes (M.11c). Every write calls the core writer that owns the change, in one transaction with its answer; a write that has no core writer got one beside the code it belongs to (checker.mark, checker.set_checked, checker.mark_stock_parts, admin.attach_instruction, the colour scope of identity_admin.add_link, the BrickLink preference in admin.patch_settings).

  • The request is checked against brickwork/api2/write_contract.json, generated from openapi-v2.yaml by python -m scripts.generate_api_writes (the gate fails when it differs). An object that lists properties is closed: an unknown member, a wrong type, a value out of its enum, pattern or range is 422 validation_failed naming the member; wrong media type is 415, a body that is not a JSON object 400, a query name the operation does not declare 400 unknown_filter. The Idempotency-Key is read after the body, so a malformed body is 422 before a missing key is 428.
  • An item in a body is an ItemKey: by id (a merged id is 409 merged with the survivor) or by provider, kind and key (bricklink unless named). A number that names no item, or an id that does not exist, is 422 unidentified; one that names several is 409 ambiguous with the candidates as item bodies; a kind Rebrickable has no numbers for is 422. A colour next to it is read in the provider's space of the key and in Brick Work's own space when the item is named by id. On a path that names the item (POST /items/{id}/prices) and on PATCH /wishes/{id} the colour is a Brick Work colour id.
  • Creating goes through the Add flow (add.resolve, validate_edits, confirm), so an API copy has the part list the web would give it and its catalogue import is queued for the token that asked (the embedded worker is woken after the commit). decision is another (add although the set is owned or wished), skip or absent. Absent on something already owned or wished is 409 conflict; skip on it is 200 with {data: {skipped: true, existing: <the item's body>}, event_id: null} and writes nothing (the client asked for the skip, so it is an answer, not an error; changed in the follow-up to M.11c), skip on something new adds it (201). Loose stock merges into an identical line (the answer lists that line's id); POST /stock of a set and POST /copies of anything but a set are 422. A new wish for a list and an item and colour already wished adds to the wished quantity on decision=another.
  • The envelope. event_id is the last event of the write that a registered undoer takes (else its last event), undo_url is that event's Undo (left out when nothing takes it). Undoing that event undoes the write whole: the undoable events the same request made before it, newest first, in one transaction (so Undo of POST /wishes/{id}/to-owned takes the copies away and puts the wish back). A preview (dry_run) and a write that changed nothing have event_id: null where the contract allows it (WrittenBulkResult, and, since M.11c, WrittenSettings and WrittenItemLink).
  • Idempotency is stored as a bw_api event op:<key> in the transaction of the write, per token, for 30 days; a failed write leaves no record, so the same key can be sent again. The record is hidden from /events (no values). A preview stores nothing. The wishlist writes require the key as well (the contract says so; section 2.7 lists them since M.11c).
  • If-Match is checked in the write's transaction, after the 404 and before 409 archived. A restore of a record that is not archived is 409; every write to an archived record is 409 archived.
  • Marks of a copy's lines (PATCH /copies/{id}/parts/{part_id}, POST .../parts/bulk, PATCH .../figures/{item_id}, PUT .../checked) are one part_check event of the shape a check answer writes; Undo is refused (409 undo_blocked) when the lines changed after it, so marks are undone in the order they were made. Each count is bounded 0 to the line's quantity on its own. A figure's own parts are parts/{part_id} too and never change the set's derived completeness; a whole missing figure does. parts/bulk names the set's non-spare lines by item and colour: mode=set replaces missing, mode=add adds to it (over the quantity is 422, nothing written); a line that is not there, or a number that names no item or several, is listed under skipped with its field (items[n]) and code (not_found, unidentified, ambiguous); dry_run reports the same without writing. PATCH /stock/{id}/parts takes spare as 0 or 1 (null is 0); its event is a collection receipt over the loose figure's parts.
  • POST /copies/{id}/check takes the checker's operation id from the Idempotency-Key (a random one without it) and answers the full CheckState, as GET does; a repeated operation answers from the checker's own receipt, which is then the event.
  • Take, Give, Found, booklets answer a Receipt (event_id, message); Take of a whole minifigure with a colour is 422; a stock or gap that changed is 409 stale_match.
  • Storages: DELETE of a storage that holds things, or that other storages sit in, is 409 conflict unless move_to_unstored=true (it never moves storages); the id unstored is the target of POST /storages/unstored/contents. Flags: the app's own status flags (system) are 409 to rename or archive. An archived place or flag keeps its references; restoring brings them back.
  • PDFs. POST /instruction-files is multipart with item as JSON text ({"id": 15011}), label and file; the PDF belongs to a set, a booklet names the set with its number. A photo (POST /copies/{id}/photos, /stock/{id}/photos) is an image up to 15 MB; the copy's or line's image_id stays the cached picture, the own photo is in /items/{id}/images.
  • Settings. GET /settings reads admin.settings; source_priority is {step: {kind or "*": [source ids]}} over pictures, metadata and market_price (a step that is sent replaces that step), prefer_bricklink_inventory is the global switch of design 4.4, candidate_sets_common_max (a whole number of 25 or more, default 300) is the most sets a part may be held by and still count in Lot Detective (#200). A key outside the whitelist is 422 unsupported_setting. Changing settings needs admin.
  • Links, merge, unmerge. PUT /items/{id}/links makes sure the item has the BrickLink link as a manual one (a link to check is confirmed; one scoped to a colour is its own row, in the BrickLink colour space) and, with preferred: true, makes it the preferred one of its scope; the BrickLink type must be the item's. It is idempotent per (type, number, colour): putting what is there writes nothing. preferred: false means no change to the preference: a preference is replaced by marking another link preferred, never cleared to none (there is always a resolver order). POST /items/{id}/merge names the loser in the path and the survivor as an ItemKey; Merge.receipt is {event_id, counts}. POST /items/{id}/unmerge names the merged id in the path; merge_id is optional and must belong to it; an unmerge has no Undo (merge again). No M.11c operation writes provider status: a person resolves a to-check row by a link or a merge.
  • Change variant (#151). POST /items/{id}/change-variant takes {"to": <ItemKey>} and names the old variant in the path; both are catalogue figures (or parts, or other items) of one kind, neither merged. Every owned row moves from the item in the path to to, keeping its id: loose stock, wishes, the figure lines of copies (a copy keeps its part list; a line that named a catalogue list to does not have becomes a migrated one) and the part lines of loose parts. BrickLink's number (the old item's preferred one, else the one link to check on to) becomes to's preferred manual link, and the old item's link of it becomes to_check; the old item is left as it is. VariantChange is {from, to, number, receipt: {event_id, counts}}. One event: its Undo (POST /events/{id}/undo) puts every row and link back, and is 409 once any of them changed. Refused with 409: a merged item on either side (merged with survivor), another kind, a set, the same item, a job in flight for either, a copy, loose figure or part list with a line for both items, a row holding a fetched part list of the old item, and a live stock row or wish that would meet a live one of to on its key (nothing is added up).
  • Sets versus a pile of parts (#199). GET /planning/set-scores ranks the catalogue's sets by how much of a pile of loose parts each explains: give exactly one of lot, storage (with the storages inside it), loose=true or stock (repeated line ids), else 422; an unknown lot or storage is 404, a stock id that is not a live loose part or figure 422. limit (1 to 100, default 20), offset and detail (matched lines per set, 0 to 50, default 10) page and size the answer; the ranking is cached per catalogue build and input, so a later page is cheap. Each set carries evidence (the sum, over the lines it holds, of the rarity weight ln(N/df) times the covered share of the set's quantity), coverage (evidence over the set's own total weight), covered, complete and distinctive lines, the matched lines with the stock flags passed through (flags sums them per flag) and swaps (the same part in another colour or as an alternate mould, listed apart and in no number). It reads and writes nothing, so a read token may call it, and it is a GET: an arbitrary parts list goes through the Python service brickwork.core.set_scoring.score. The formula and the decisions are ADR 0026; the how-to for developers is the reference page Set scoring.
  • Which sets came in this lot (#200). GET /lots/{id}/candidate-sets ranks the sets a lot may have come from with the engine of GET /planning/set-scores, read-only; an unknown lot is 404. score is 0 to 100: the set's evidence over the best candidate's, so the best set that passes the set filters scores 100; it is not a probability. reasons are sentences ("7 of 9 distinctive parts found", "2 printed parts only in this set"), distinctive_found_parts and distinctive_missing_parts list up to 20 distinctive parts each (held by at most 25 sets) with their picture (id, content_url of the newest local one, image_url of the catalogue). Filters: min_score, theme (a catalogue theme id or name, with its sub-themes; an unknown one is 422), year_from, year_to, parts_min, parts_max (the catalogue's part count), ignore_common (default true: lines held by more than candidate_sets_common_max sets are dropped before scoring) and limit (1 to 100, default 20) and offset, which page after the filters. A bad boolean is 400 unknown_filter, any other bad value 422. The rules and the code are in Set scoring.
  • Value and gain (#57). GET /valuation (MCP tool get_valuation) answers copies (every live copy, in set-number order) and statistics (the Paid vs value figures of Statistics), all in the display currency of Settings. A copy has paid (its price paid, null with paid_reason no_price_paid for none or 0.00, or no_rate), worth (null with worth_reason no_quote or no_rate) and gain (worth minus paid, negative for a loss, with its whole percent of the price paid; null unless both are known). A money figure is {amount, currency, converted, rate, rate_as_of, original}: a converted one carries the rate used, its date and the amount as recorded, and a missing rate is a null figure with the reason, never a guess; two currencies never share a figure. worth_source is override (the copy's own value) or used or new (the sold average of its set, chosen as the Prices card chooses it, for the copy's condition and completeness; a new or sealed copy is priced new, one with a missing or damaged line has the incomplete-set discount taken off, as the set page does, worth_discount_pct), with worth_date (the day of the quote), worth_sales and worth_stale (the Prices card's rule). statistics counts only the copies that have both a price paid and a worth (comparable_copies; enough is three or more), so the gain compares like with like; without_price_paid, without_quote and without_rate count the rest apart, and by_theme and by_year list {label, paid, worth, gain, percent, copies} (a purchase year that is not recorded is Not recorded). GET /export/insurance.csv is the insurance report: every live copy, one row each, columns Set number, Name, Year, Condition, Completeness, Purchase date, Price paid, Price paid currency, Current value, Current value currency, Value source, Value date, Photos (the copy's own photos, a count) and Remarks; text cells are made safe for a spreadsheet. Both are read-only. The column set is a proposal until the Discussions say what insurers ask for.
  • Stickers (#201). GET /lots/{id}/stock/{stock_id}/stickers lists the sets whose latest inventory holds the part line's part in its colour and a sticker sheet (a part of the category Stickers), with the sheet and its picture, and the line's sticker state (flagged, confirmed). POST /lots/{id}/stock/{stock_id}/sticker with set_item_id and sheet_item_id from that list confirms the choice (409 when the line has no Sticker flag, 422 for a pair that is not a candidate); DELETE on the same path is "Not a sticker": it removes the flag and stops counting a confirmation. Both are one undoable event, an unchanged state writes nothing (changed: false, event_id: null when there is no event) and the Idempotency-Key is optional. The candidate-sets answer lists each set's swaps. Filing with sticker: true is a Part Spotter (web) matter; the API sets the flag with PUT /stock/{id}/flags as before. See Set scoring for the evidence rule.
  • The copy's part list. POST /copies/{id}/inventory takes source, version (Rebrickable; default the latest) and, new in M.11c, remember (also the set's preference for new copies, in the same event). BrickLink's list without a key is 424 source_not_configured; with no fetched list the answer is 202 with Location: /api/v2/jobs/{id} and the copy waits (inventory.pending); a copy already waiting, or already on that list, is 409. DELETE .../inventory/pending cancels the wait and puts back the preference it remembered.
  • Refusals of the 4.0 store are problems: a state that does not allow the change (nothing to switch, a marked copy, a pending change, a refused merge) is 409, a missing catalogue 424, the rest 422; the words are the refusal's own with folders cut.
  • Corrections to the first contract text of the writes: WishlistTransfer.ids are texts (wish ids are uuids); Settings.source_priority is nested as above; CopyInventorySwitch has remember; WrittenSettings.event_id and WrittenItemLink.event_id may be null; InstructionUpload.item is JSON text in the form; PriceCreate.color and WishPatch.color are Brick Work colour ids; LooseFigurePartKey.spare null reads as 0.

Orders, jobs, sources, sync, imports, identifications, tokens and admin (M.11d). What the 57 remaining operations do beyond v1, and where the first contract text changed when they were built.

  • Detached writes. A write that reaches the network (a key test, a connect, the ECB feed, a pulled order, a sync plan or send, a photo identification, the catalogue import, a drain) or runs long (a BrickTracker plan) runs outside any transaction; the core writers open their own short ones, and the answer is stored in one more. A claim row (api:claim:<token>:<key>, 15 minutes) makes a second request with the same key while the first is out 409 idempotency_conflict; a call that fails stores no receipt and releases the claim, so the key can be sent again. A reply is replayed before any network call. An exhausted budget is 429 budget_exhausted (a remote throttle 429 source_throttled) with Retry-After; a provider's reply that cannot be used is 502 source_error. Where a call changes nothing in the collection (a key test, a plan) event_id is null. An order imported by BrickLink number fetches first and imports in the transaction that stores the answer, so it is whole or not at all.
  • A body whose members are all optional may be left out (POST /jobs/{id}/retry, POST /items/{id}/import, POST /items/{id}/prices/refresh): an empty body is {}. A body that holds key material or a password (PUT /sources/{source}/key, PUT /sources/{source}/user, POST .../user/connect) is fingerprinted with the keyed digest, never a plain hash, and needs BW_SECRET_KEY.
  • POST /colors/sync is dropped (section 3.3): the BrickLink colour table comes with the catalogue builds since M.8f. POST /catalogue/import loads the newest folder of BrickLink item lists beside the collection and reconciles (the page's Load lists) and answers CatalogueStatus as the 4.0 catalogue is: whether it is loaded, its items, the builds the collection activated, the link coverage per status and the last reconcile.
  • Orders. Order carries the counts and phase the Orders page shows (lines, pieces, received, allocated, held). OrderLine names its item and colour as the bodies of section 2.1 (null while the importer could not place it, and then unplaced_reason says why in words, null when placed; raw holds the provider's words and suggestions the items the line could be, best first: the ones a name that matches several items names, or, for a name no item has exactly, the nearest names of the loaded BrickLink item list, a longest-prefix match first and then names that hold all its words (#155); a suggestion is never placed by itself). A match names the item as an ItemKey and the colour in its provider space (color); a part needs its colour. A BrickLink number that no item holds but the loaded BrickLink item list names (an instruction booklet, a BrickLink-only part) becomes a local BrickLink-born item, at import and when matched by number (#154); an import's Undo leaves that item, unused. A line or document that is not in the order is 404; the core's refusals (a stale count, a gap that changed, a document in the wrong phase) are 422. GET /orders/{id}/allocations lists Allocation as the 4.0 table has it (target_type, copy_id, stock_id, item, color, intent_before, intent_after, target_label, undo_event_id); .../allocation-review the saved lines with what is available and the gaps they could fill (destinations); .../history the order's saves from the event log with via and actor. short.xml and export.csv are downloads. Every order write is one orders:<id> event and its Undo restores the order rows.
  • Jobs. Job is a row of job with its job_step rows (interrupted is a running job whose worker is gone). GET /jobs pages by id and takes state and failed. Retry and resume are for the pipeline jobs (kind: item); the two fetch kinds answer 404 there. Queueing answers 202 with Location: /api/v2/jobs/{id} when it made one job, else a JobList (POST /jobs/pictures, /jobs/instruction-identities; POST /prices/refresh answers 200). POST /jobs/pictures takes scope (missing or all), kinds and expected_count (the call is refused when the count has changed) and queues one pictures job per held item of those kinds that has none running, replacing v1's source and types. POST /items/{id}/import takes steps only; the sources of a step are the stored order of its kind. A price refresh of an item with an unfinished job answers that job.
  • Sources and keys. Source.key is {configured, origin, saved_at} and nothing else. The user token or hash of the collection sync is the key store under <source>_user. Undo of a key save, a key removal and a source switch is admin's, as making them was.
  • Sync and import. SyncPlan also carries remote and, for a send, state; ImportPlan lists unplaced lines (not unresolved); SyncApplied and ImportApplied also carry event_id. A plan applied twice is applied once (already_applied), whatever the key. A pull apply is a write-scope operation and so is its Undo; a send apply and an import apply are admin's, and an import's Undo is admin's. The uploaded database is streamed to a private file under the import folder (up to BW_IMPORT_MAX_MB, 200 by default) and deleted whatever happens.
  • Identifications. A candidate is {item, score, name} with owned (#45) on the answer of the upload, which also lists the predictions that named no item or several under unresolved. A read of an archived identification returns it with archived_at (section 2.4, v1 hid it); DELETE of an archived one is 409. GET .../thumb is the 256 px JPEG.
  • Tokens, exchange rates, database. A token's secret is in the answer of its creation only (the replay record keeps it out: a replay of the creation answers secret: null, so a lost answer means revoking the token and creating another); bootstrap is reserved and cannot be revoked. A rate is a decimal string per EUR; PUT with null removes a hand-entered one and the ECB feed keeps hand-entered rates. GET /database/check adds catalogue_integrity_check when a catalogue is attached. GET /database/export is a snapshot of the collection without the stored source keys and refuses (409) if a known secret would remain in it; it writes nothing.
  • v1 answers 410 Gone and names v2 on every path but GET /health; its modules and route table are gone.

The MCP (M.11e). The MCP server reads openapi-v2.yaml and calls /api/v2; its 65 tools are the operations that carry x-mcp-tool: the 61 tools of v1 keep their names (decision 7) and get_set_by_number, get_part_by_number, get_minifigure_by_number and lookup_item are new. Its 8 resources are x-mcp.resources. How a client uses it is in mcp.md.

  • A tool takes numbers, never a Brick Work item id. The contract marks the item-id path parameters x-mcp-item: true (/items/{id} for get_item, import_item, get_prices, record_price and refresh_price; item_id in /copies/{id}/figures/{item_id} for set_figure_missing) and the ItemKey schema x-mcp-item: key. A tool with a marked path parameter takes provider (bricklink by default, or rebrickable), kind and no instead, and the adapter calls GET /lookup?provider=&kind=&key= before the call and puts the id into the path. A tool whose body names an item (add_copies, add_stock, add_wish, take_for_gap) takes item as {provider, kind, key} and has no id member. The rule is read from the contract, not listed in the adapter.
  • A lookup that fails is the tool's error, unchanged: 404 not_found, or 409 ambiguous with the candidates. Nothing is written then.
  • On get_prices the provider is also the colour space of color (decision 4), so provider=rebrickable reads color as a Rebrickable colour id. Elsewhere color stays as the operation has it (record_price takes a Brick Work colour id, as POST /items/{id}/prices does).
  • Collection records keep their ids (copy, stock line, wish, wishlist, storage, lot, event, job, order, plan, purchase location): a client takes them from the list tools. bricklink_id and bricklink_order_id are numbers of BrickLink and stay.
  • Resources: brickwork://items/{provider}/{kind}/{no} is get_lookup (the contract maps the URI's variables to its query); the others are as in v1.
  • Fixed in the contract: PATCH /copies/{id}/figures/{item_id} named the figure's parameter with the id of an item path, so two parameters were called id; it now uses Path_figure_item_id.

3. Resource catalogue

3.1 Summary

201 operations in all (M.11a 9, M.11b 64, M.11c 71, M.11d 57 by piece); v1 had 179 operations.

Group Operations Pieces
Meta 4 M.11a 4
Catalogue 5 M.11b 5
Items 8 M.11a 1, M.11b 6, M.11d 1
Identity 11 M.11a 4, M.11b 3, M.11c 4
Images 1 M.11b 1
Copies 19 M.11b 6, M.11c 13
Copy parts 5 M.11b 2, M.11c 3
Loose stock 13 M.11b 3, M.11c 10
Minifigures 2 M.11b 2
Instructions 7 M.11b 3, M.11c 4
Prices 9 M.11b 5, M.11c 2, M.11d 2
Wishlist 16 M.11b 5, M.11c 11
Storages 7 M.11b 2, M.11c 5
Lots 7 M.11b 3, M.11c 4
Places 5 M.11b 1, M.11c 4
Flags 5 M.11b 1, M.11c 4
Planning 9 M.11b 9
Orders 15 M.11d 15
Events 3 M.11b 2, M.11c 1
Jobs 7 M.11d 7
Sources 6 M.11d 6
Settings 5 M.11c 2, M.11d 3
Admin 10 M.11b 1, M.11d 9
Matching 2 M.11b 2
Tokens 3 M.11d 3

3.2 Operations

Scope none means no token. Every path is relative to /api/v2. The v1 column names the v1 operation the row replaces when its path changed, says same when the path is unchanged and NEW when v1 had no such operation. Write rows are one transaction plus one event; the receipt kinds and Undo dispatch per write group are v1's (api-v1.md section 4) and move to this file with M.11c.

Meta

Operation What it does Scope Piece v1
GET /health Liveness, app, schema and API version. No token. none M.11a same
GET /me The calling token: name, scopes, expiry. read M.11a same
GET /openapi.yaml This contract as OpenAPI 3.1 YAML. No token. none M.11a same
GET /version Build, API and schema versions, enabled features. read M.11a same

Catalogue

Operation What it does Scope Piece v1
GET /catalogue/items Search the local BrickLink catalogue file by type and text. read M.11b same
GET /catalogue/items/{id} One catalogue entry. read M.11b GET /catalogue/items/{type}/{no}
GET /catalogue/status Catalogue file state (loaded, item counts, imported_at). read M.11b same
GET /colors All colours. read M.11b same
GET /colors/{color_id} One colour. read M.11b same

Items

Operation What it does Scope Piece v1
GET /items Items known to the store, with text search over name, number and properties. read M.11b same
GET /items/{id} Item detail: properties, counts, owned and wished summary. read M.11a GET /items/{type}/{no}
GET /items/{id}/colors Colours this part is known in. read M.11b GET /items/{type}/{no}/colors
GET /items/{id}/detail Item detail: properties, counts, owned and wished summary, prices, matches (what v1's GET /items/{type}/{no} returned). read M.11b NEW
GET /items/{id}/images Local pictures of the item. read M.11b GET /items/{type}/{no}/images
POST /items/{id}/import Create the item identity and queue the six import steps. write M.11d POST /items/{type}/{no}/import
GET /items/{id}/inventory Parts and figures the item is made of (inventory lines). read M.11b GET /items/{type}/{no}/inventory
GET /items/{id}/used-in Owned copies that contain this part or figure. read M.11b GET /items/{type}/{no}/used-in

Identity

Operation What it does Scope Piece v1
GET /items/{id}/links The item's provider links with status, method, origin, preferred and colour scope (design 3.2), to-check candidates included. read M.11b NEW
PUT /items/{id}/links Set one manual link of the item, or make one preferred for its colour scope (design 3.2). Idempotent per (provider, type, number, colour). write M.11c NEW
POST /items/{id}/merge Merge this local item into a survivor (design 3.6): every reference moves, the id answers 307 to the survivor afterwards. Refused for a live catalogue item. write M.11c NEW
GET /items/{id}/provider-status What is known about the item at a provider when there is no key: looked and nothing found, to check without candidate, kind differs (design 3.2). read M.11b NEW
POST /items/{id}/unmerge Undo a merge by its receipt (design 3.6): the merged id answers for itself again. Refused if a moved row changed since. write M.11c NEW
GET /links Links and provider statuses across items, by status: status=to_check is the list that replaces v1's /unresolved. read M.11b NEW
GET /lookup Find the item for any provider number: provider, kind and key (and a colour in the provider space). Same body and errors as the by-number paths. read M.11a NEW
GET /minifigures/by-number/{no} Find the item for a minifigure number; the same item body as /items/{id} (decision 2). Default provider bricklink; provider=rebrickable for Rebrickable numbers. Several items answer 409 ambiguous with the candidates, none 404. read M.11a NEW
GET /parts/by-number/{no} Find the item for a part number; the same item body as /items/{id} (decision 2). Default provider bricklink; provider=rebrickable for Rebrickable numbers. Several items answer 409 ambiguous with the candidates, none 404. read M.11a NEW
GET /sets/by-number/{no} Find the item for a set number; the same item body as /items/{id} (decision 2). A set number is the same at both providers. Several items answer 409 ambiguous with the candidates, none 404. read M.11a NEW

Images

Operation What it does Scope Piece v1
GET /images/{id}/content Image bytes from the local picture store. read M.11b same

Copies

Operation What it does Scope Piece v1
GET /copies Owned set copies. read M.11b same
POST /copies Add owned copies of a set (quantity N makes N copies) and queue its import. write M.11c same
DELETE /copies/{id} Archive the copy (the UI calls this deleting a set). write M.11c same
GET /copies/{id} One copy. read M.11b same
PATCH /copies/{id} Edit condition, completeness, storage, purchase and estimate fields, note. write M.11c same
GET /copies/{id}/check Part-check session state for this copy. read M.11b same
POST /copies/{id}/check Apply one part-check action (start, answer, found_one, skip, select, back, next, spares, recheck, finish, undo; start takes scope all or missing). write M.11c same
PUT /copies/{id}/checked Set or clear the "Set is checked" flag and recompute derived completeness. write M.11c same
GET /copies/{id}/completeness Stored and derived completeness, checked flag, missing and damaged counts. read M.11b same
PUT /copies/{id}/flags Replace the copy flags. write M.11c same
GET /copies/{id}/gaps Missing, damaged and absent items of the copy. read M.11b same
POST /copies/{id}/inventory Switch the copy's part list in place (design 4.4): to Rebrickable (a version) or BrickLink. A BrickLink switch is pending until its fetch job lands. write M.11c NEW
DELETE /copies/{id}/inventory/pending Cancel a pending BrickLink switch (design 4.4): the copy keeps its current list. write M.11c NEW
GET /copies/{id}/matches Loose stock that would fill the copy gaps. read M.11b same
POST /copies/{id}/photos Upload an own photo (multipart, 15 MB). write M.11c same
PUT /copies/{id}/pin Pin or unpin a copy. write M.11c same
POST /copies/{id}/restore Clear the archive mark. write M.11c same
POST /copies/{id}/take Take N of one missing part (with colour) or whole minifigure (no colour) from loose stock into the copy. write M.11c same

Copy parts

Operation What it does Scope Piece v1
GET /copies/{id}/figures Whole figures of the copy. read M.11b same
PATCH /copies/{id}/figures/{item_id} Set the missing count of one whole figure. write M.11c PATCH /copies/{id}/figures/{type}/{no}
GET /copies/{id}/parts Part lines of the copy (required, spare, figure components). read M.11b same
POST /copies/{id}/parts/bulk Apply missing counts to many lines by item and colour (mode set or add, optional dry run). write M.11c same
PATCH /copies/{id}/parts/{part_id} Set missing, damaged, checked for one line (mark missing or found). write M.11c same

Loose stock

Operation What it does Scope Piece v1
POST /found Record found parts into copies, a loose line, or a list. write M.11c same
GET /stock Loose stock lines (parts, minifigures, booklets, anything). read M.11b same
POST /stock Add loose stock; identical lines merge. write M.11c same
POST /stock/booklets Add loose booklets from lines "set number, quantity". write M.11c same
DELETE /stock/{id} Archive the line. write M.11c same
GET /stock/{id} One stock line. read M.11b same
PATCH /stock/{id} Edit a stock line (may merge into an existing line; response lists merged ids). write M.11c same
PUT /stock/{id}/flags Replace the line flags. write M.11c same
POST /stock/{id}/give Give N from this line to a copy gap or a loose-figure part gap. write M.11c same
GET /stock/{id}/parts Parts of a loose minifigure. read M.11b same
PATCH /stock/{id}/parts Set missing and damaged on parts of a loose minifigure. write M.11c same
POST /stock/{id}/photos Upload an own photo (multipart, 15 MB). write M.11c same
POST /stock/{id}/restore Clear the archive mark. write M.11c same

Minifigures

Operation What it does Scope Piece v1
GET /minifigures Figures by identity: owned in sets, loose, missing. read M.11b same
GET /minifigures/{id} One figure: sets, loose lines, missing, matches, prices. read M.11b GET /minifigures/{no}

Instructions

Operation What it does Scope Piece v1
GET /instruction-files Booklet files and booklet identities with coverage. read M.11b same
POST /instruction-files Attach a PDF (multipart, up to 20 MB) to an item. write M.11c same
DELETE /instruction-files/{id} Archive a PDF association. write M.11c same
GET /instruction-files/{id} One file record. read M.11b same
PATCH /instruction-files/{id} Rename the label. write M.11c same
GET /instruction-files/{id}/content Download the PDF. read M.11b same
POST /instruction-files/{id}/restore Restore an archived record. write M.11c same

Prices

Operation What it does Scope Piece v1
GET /items/{id}/prices Current prices and recorded observations of one item. read M.11b GET /items/{type}/{no}/prices
POST /items/{id}/prices Record one manual or valuation observation. write M.11c POST /items/{type}/{no}/prices
GET /items/{id}/prices/history Observation history of one item. read M.11b GET /items/{type}/{no}/prices/history
POST /items/{id}/prices/refresh Queue a market price refresh for one item. write M.11d POST /items/{type}/{no}/prices/refresh
GET /prices Current price per item, colour, condition and guide. read M.11b same
POST /prices/observations Record up to 500 observations in one transaction. write M.11c same
POST /prices/refresh Queue refresh jobs for every stale item. write M.11d same
GET /prices/stale Items whose price is older than the freshness rule. read M.11b same
GET /valuation Value and gain per live copy, and the Paid vs value figures of Statistics (#57). read M.11b NEW

Wishlist

Operation What it does Scope Piece v1
GET /wishes Wishlist rows; under_max filters to rows priced under the wish maximum. read M.11b same
POST /wishes Add a wish (same colour and item adds to the live row on decision=another). write M.11c same
DELETE /wishes/{id} Archive the wish. write M.11c same
GET /wishes/{id} One wish. read M.11b same
PATCH /wishes/{id} Edit quantity, colour, condition, max price, max price with box and their currencies, note. write M.11c same
PUT /wishes/{id}/flags Replace the wish flags. write M.11c same
POST /wishes/{id}/restore Restore an archived wish. write M.11c same
POST /wishes/{id}/to-owned Turn a wish into owned copies and archive the wish, one transaction. write M.11c same
GET /wishlists get wishlists read M.11b same
POST /wishlists post wishlists write M.11c same
DELETE /wishlists/{id} delete wishlists id write M.11c same
PATCH /wishlists/{id} patch wishlists id write M.11c same
GET /wishlists/{id}/export.csv Export wishlist as Rebrickable CSV read M.11b same
GET /wishlists/{id}/export.xml Export wishlist as BrickLink wanted list XML read M.11b same
POST /wishlists/{id}/restore post wishlists id restore write M.11c same
POST /wishlists/{id}/transfer post wishlists id transfer write M.11c same

Storages

Operation What it does Scope Piece v1
GET /storages Active storages with counts, plus the virtual "Not stored". read M.11b same
POST /storages Create a storage. write M.11c same
DELETE /storages/{id} Archive; refuses non-empty unless move_to_unstored=true. write M.11c same
GET /storages/{id} Storage with its contents. read M.11b same
PATCH /storages/{id} Rename. write M.11c same
POST /storages/{id}/contents Move copies, stock and lots into this storage (id "unstored" for none). A lot carries its parts that sit in the lot's own storage, in the same receipt. write M.11c same
POST /storages/{id}/restore Restore an archived record. write M.11c same

Lots

Operation What it does Scope Piece v1
GET /lots Part lots. read M.11b same
POST /lots Create a lot from existing stock lines. write M.11c same
DELETE /lots/{id} Archive the lot. write M.11c same
GET /lots/{id} Lot with its stock. read M.11b same
PATCH /lots/{id} Edit name, note, storage, purchase fields. write M.11c same
POST /lots/{id}/restore Restore an archived record. write M.11c same

Places

Operation What it does Scope Piece v1
GET /places Purchase locations with counts and totals per currency. read M.11b same
POST /places Create a purchase location. write M.11c same
DELETE /places/{id} Archive, retaining references. Undo restores the place. write M.11c same
PATCH /places/{id} Rename. write M.11c same
POST /places/{id}/restore Restore an archived record. write M.11c same

Flags

Operation What it does Scope Piece v1
GET /flags Flags of kind owner, status and tag. read M.11b same
POST /flags Create a flag. write M.11c same
DELETE /flags/{id} Archive, retaining flag links. Undo restores the flag. write M.11c same
PATCH /flags/{id} Rename. write M.11c same
POST /flags/{id}/restore Restore an archived record. write M.11c same

Planning

Operation What it does Scope Piece v1
GET /export/insurance.csv The insurance report as CSV: one row per live copy with price paid, worth, source and date of the quote (#57). read M.11b NEW
GET /export/missing.csv Missing parts of the chosen copies as CSV (the printable list, #43). read M.11b same
GET /export/missing.json The same missing-item aggregation and eligibility as XML, returned as JSON. read M.11b same
GET /export/missing.xml BrickLink wanted-list XML of missing items for chosen copies. read M.11b same
GET /export/picker.csv Part Runner results for the chosen copies as CSV, in pick order (#43). read M.11b same
GET /export/wishlist.csv The wishlist (one list or all) as CSV (#43). read M.11b same
GET /planning/ranking Owned copies ranked by cost to complete, with the repair settings used. read M.11b same

Orders

Operation What it does Scope Piece v1
GET /orders Orders with line and receipt state. read M.11d same
POST /orders/documents Import an order document: pasted email, BrickLink XML, or fetch by order number with the user key. write M.11d same
GET /orders/export.csv Saved order lines (kind=lines) or one row per order with new and used totals (kind=summary) as CSV, filtered by order date and store (#44). Totals never mix currencies. read M.11d same
GET /orders/{id} Order with documents and lines. read M.11d same
POST /orders/{id}/allocate Allocate received lines to copies or stock. write M.11d same
GET /orders/{id}/allocation-review Saved lines, available received quantities and current copy-gap destinations, paginated. read M.11d same
GET /orders/{id}/allocations Allocation history, paginated. read M.11d same
POST /orders/{id}/allocations/retry Retry reviewed chosen allocations against current counts and gaps. Requires an Idempotency-Key. write M.11d same
POST /orders/{id}/documents/{doc_id}/phase Move a document: review, reopen or save. write M.11d same
GET /orders/{id}/history Ledger receipts from web and API operations, with Undo state, paginated. read M.11d same
PATCH /orders/{id}/lines/{line_id} Match a line to a catalogue identity. write M.11d same
PUT /orders/{id}/lines/{line_id}/hold Hold or release a line. write M.11d same
POST /orders/{id}/received Record received counts per line. write M.11d same
POST /orders/{id}/reconcile Reconcile the order. write M.11d same
GET /orders/{id}/short.xml BrickLink XML of the still-missing lines. read M.11d same

Events

Operation What it does Scope Piece v1
GET /events Audit events, newest first. read M.11b same
GET /events/{id} One event with old and new values. read M.11b same
POST /events/{id}/undo Undo a receipt event. One endpoint for all receipt kinds. write M.11c same

Jobs

Operation What it does Scope Piece v1
GET /jobs Import and refresh jobs with steps. read M.11d same
POST /jobs/drain Run one drain of due jobs in the request worker. write M.11d same
POST /jobs/instruction-identities Queue identity refresh for booklets without a file. write M.11d same
POST /jobs/pictures Queue a picture refresh for a scope. write M.11d same
GET /jobs/{id} One job. read M.11d same
POST /jobs/{id}/resume Resume an interrupted or budget-waiting job; a failed job needs retry. write M.11d same
POST /jobs/{id}/retry Retry failed steps (all, or one step). write M.11d same

Sources

Operation What it does Scope Piece v1
POST /catalogue/import Rebuild the catalogue file from the configured BrickLink XML directory. admin M.11d same
GET /sources Sources with key state (configured yes or no), budget, usage today, last success and error. read M.11d same
PATCH /sources/{source} Enable or disable a source. admin M.11d same
DELETE /sources/{source}/key Archive stored key; Undo restores it. admin M.11d same
PUT /sources/{source}/key Store an encrypted source key (replaces a stored one). Write-only. admin M.11d same
POST /sources/{source}/key/test One budgeted call with the key in use; the result carries no key material. admin M.11d same

Settings

Operation What it does Scope Piece v1
GET /exchange-rates Exchange rates per 1 EUR (hand-entered or ECB) and the display currency (#39). Stored amounts never change currency. read M.11d same
POST /exchange-rates/ecb Load the ECB daily reference rates (one keyless request); hand-entered rates are kept. admin M.11d same
PUT /exchange-rates/{code} Enter (or with null remove) one rate per 1 EUR by hand. admin M.11d same
GET /settings Allowed settings and freshness rules. read M.11c same
PATCH /settings Update allowed settings (whitelist). admin M.11c same

part_list_grouping is a settings key: grouped (default), bricklink, or none. PATCH uses the same save and undo event as the Settings page. It changes the set detail list only.

Admin

Operation What it does Scope Piece v1
GET /database/check Integrity, foreign-key check and row counts. admin M.11d same
GET /database/export SQLite snapshot download. Stored source keys, even encrypted, are removed from the copy; refuses (409) if known plaintext credentials would remain. admin M.11d same
GET /identifications Photo identification history; archived rows hidden. read M.11d same
POST /identifications Upload an image for Brickognize identification (image, up to 8 MB). write M.11d same
DELETE /identifications/{id} Archive identification; Undo restores it. write M.11d same
GET /identifications/{id} Identification result. read M.11d same
GET /identifications/{id}/thumb Thumbnail bytes. read M.11d same
POST /imports/bricktracker Upload a BrickTracker app.db (any upstream version, up to 200 MB) and get an import plan. A copy is upgraded with upstream's migrations and compared with this collection; nothing is written to the collection. admin M.11d same
POST /imports/{id}/apply Apply an import plan: rows marked add are written in one transaction; locked rows (already imported, duplicate wishes) are always skipped. Undo with POST /events/{id}/undo (admin). admin M.11d same
GET /statistics Counts by completeness, theme, storage, place. read M.11b same

Matching

Operation What it does Scope Piece v1
GET /gaps Missing items over all copies and loose figures. read M.11b same
GET /matches Suggestions: loose stock that fills a gap. read M.11b same

Tokens

Operation What it does Scope Piece v1
GET /tokens Tokens including revoked rows; cursor, limit and direction. Never the secret or hash. admin M.11d same
POST /tokens Create a token. The secret is shown once in this response. admin M.11d same
DELETE /tokens/{id} Revoke a token; a second revocation is 409 conflict. admin M.11d same

3.3 Dropped v1 operations

v1 operation Why it is gone
GET /unresolved Replaced by GET /links?status=to_check. A link always has an item (design 3.2), so nothing is "unresolved without an item"; a to-check candidate is a link, and "looked, found nothing" is the item's provider status.
POST /unresolved/resolve Resolving is adding or preferring a link (PUT /items/{id}/links) or merging the local item into a catalogue item (POST /items/{id}/merge).
GET /id-matches The id match table is the link table: GET /items/{id}/links and GET /links.
POST /colors/sync The BrickLink colour table comes with the catalogue builds (M.8f), not from a source call; POST /catalogue/import loads the BrickLink item lists and reconciles.

The ? item type and the item_type, item_no and color_id members are gone from every body (section 1). Nothing else in v1's 179 operations is dropped: 175 are carried (13 of them with a changed path, the rest on the same path with changed bodies where they named an item).


4. Pieces and what is implemented

Piece What Operations
M.11a This contract and the OpenAPI twin; the /api/v2 blueprint (auth, scopes, throttle, read-only, audit origin, envelopes, errors, body caps); meta; item addressing and the item and colour serializers; the merged-id 307; v1's 410 names v2; the contract check covers v2 9
M.11b Reads: catalogue, colours, items (detail, inventory, used-in, colours, images, links, provider status), copies, stock, minifigures, gaps and matches, instructions, prices, wishes and wishlists, storages, lots, places, flags, planning, exports, events, statistics, set scores, candidate sets of a lot 59
M.11c Writes: the v2 write contract, create by numbers with 422 unidentified, the copy inventory switch and cancel, links, merge and unmerge, settings, undo 65
M.11d Orders, jobs, sources and keys, sync, imports, identifications, tokens, admin; the dead v1 callers removed 57
M.11e The MCP regenerated from this file, the v2 parity test, API and MCP docs. Done (2026-10-28) none

Operations by piece: M.11a 9, M.11b 64, M.11c 71, M.11d 57 (201 in all). The two tables below are generated from the contract and the routes by scripts/check_api_contract.py --write, and that check (run by scripts/check.sh) fails when the contract, the routes or these tables disagree. A piece lands by adding itself to brickwork.api2.DONE.

Implemented

Implemented operation Piece Backing
POST /api/v2/catalogue/import M.11d brickwork.api2.sources.post_catalogue_import
GET /api/v2/catalogue/items M.11b brickwork.api2.catalogue.get_catalogue_items
GET /api/v2/catalogue/items/{id} M.11b brickwork.api2.catalogue.get_catalogue_items_by_id
GET /api/v2/catalogue/status M.11b brickwork.api2.catalogue.get_catalogue_status
GET /api/v2/colors M.11b brickwork.api2.catalogue.get_colors
GET /api/v2/colors/{color_id} M.11b brickwork.api2.catalogue.get_colors_by_color_id
GET /api/v2/copies M.11b brickwork.api2.copies.get_copies
POST /api/v2/copies M.11c brickwork.api2.write_collection.post_copies
DELETE /api/v2/copies/{id} M.11c brickwork.api2.write_collection.delete_copies_by_id
GET /api/v2/copies/{id} M.11b brickwork.api2.copies.get_copies_by_id
PATCH /api/v2/copies/{id} M.11c brickwork.api2.write_collection.patch_copies_by_id
GET /api/v2/copies/{id}/check M.11b brickwork.api2.copies.get_copies_by_id_check
POST /api/v2/copies/{id}/check M.11c brickwork.api2.write_collection.post_copies_by_id_check
PUT /api/v2/copies/{id}/checked M.11c brickwork.api2.write_collection.put_copies_by_id_checked
GET /api/v2/copies/{id}/completeness M.11b brickwork.api2.copies.get_copies_by_id_completeness
GET /api/v2/copies/{id}/figures M.11b brickwork.api2.copies.get_copies_by_id_figures
PATCH /api/v2/copies/{id}/figures/{item_id} M.11c brickwork.api2.write_collection.patch_copies_by_id_figures_by_item_id
PUT /api/v2/copies/{id}/flags M.11c brickwork.api2.write_collection.put_copies_by_id_flags
GET /api/v2/copies/{id}/gaps M.11b brickwork.api2.copies.get_copies_by_id_gaps
POST /api/v2/copies/{id}/inventory M.11c brickwork.api2.write_identity.post_copies_by_id_inventory
DELETE /api/v2/copies/{id}/inventory/pending M.11c brickwork.api2.write_identity.delete_copies_by_id_inventory_pending
GET /api/v2/copies/{id}/matches M.11b brickwork.api2.copies.get_copies_by_id_matches
GET /api/v2/copies/{id}/parts M.11b brickwork.api2.copies.get_copies_by_id_parts
POST /api/v2/copies/{id}/parts/bulk M.11c brickwork.api2.write_collection.post_copies_by_id_parts_bulk
PATCH /api/v2/copies/{id}/parts/{part_id} M.11c brickwork.api2.write_collection.patch_copies_by_id_parts_by_part_id
POST /api/v2/copies/{id}/photos M.11c brickwork.api2.write_collection.post_copies_by_id_photos
PUT /api/v2/copies/{id}/pin M.11c brickwork.api2.write_collection.put_copies_by_id_pin
POST /api/v2/copies/{id}/restore M.11c brickwork.api2.write_collection.post_copies_by_id_restore
POST /api/v2/copies/{id}/take M.11c brickwork.api2.write_collection.post_copies_by_id_take
GET /api/v2/database/check M.11d brickwork.api2.system.get_database_check
GET /api/v2/database/export M.11d brickwork.api2.system.get_database_export
GET /api/v2/events M.11b brickwork.api2.history.get_events
GET /api/v2/events/{id} M.11b brickwork.api2.history.get_events_by_id
POST /api/v2/events/{id}/undo M.11c brickwork.api2.write_identity.post_events_by_id_undo
GET /api/v2/exchange-rates M.11d brickwork.api2.system.get_exchange_rates
POST /api/v2/exchange-rates/ecb M.11d brickwork.api2.system.post_exchange_rates_ecb
PUT /api/v2/exchange-rates/{code} M.11d brickwork.api2.system.put_exchange_rates_by_code
GET /api/v2/export/insurance.csv M.11b brickwork.api2.valuation.get_export_insurance_csv
GET /api/v2/export/missing.csv M.11b brickwork.api2.ranking.get_export_missing_csv
GET /api/v2/export/missing.json M.11b brickwork.api2.ranking.get_export_missing_json
GET /api/v2/export/missing.xml M.11b brickwork.api2.ranking.get_export_missing_xml
GET /api/v2/export/picker.csv M.11b brickwork.api2.ranking.get_export_picker_csv
GET /api/v2/export/wishlist.csv M.11b brickwork.api2.ranking.get_export_wishlist_csv
GET /api/v2/flags M.11b brickwork.api2.places.get_flags
POST /api/v2/flags M.11c brickwork.api2.write_where.post_flags
DELETE /api/v2/flags/{id} M.11c brickwork.api2.write_where.delete_flags_by_id
PATCH /api/v2/flags/{id} M.11c brickwork.api2.write_where.patch_flags_by_id
POST /api/v2/flags/{id}/restore M.11c brickwork.api2.write_where.post_flags_by_id_restore
POST /api/v2/found M.11c brickwork.api2.write_collection.post_found
GET /api/v2/gaps M.11b brickwork.api2.loose.get_gaps
GET /api/v2/health M.11a brickwork.api2.get_health
GET /api/v2/identifications M.11d brickwork.api2.identifications.get_identifications
POST /api/v2/identifications M.11d brickwork.api2.identifications.post_identifications
DELETE /api/v2/identifications/{id} M.11d brickwork.api2.identifications.delete_identifications_by_id
GET /api/v2/identifications/{id} M.11d brickwork.api2.identifications.get_identifications_by_id
GET /api/v2/identifications/{id}/thumb M.11d brickwork.api2.identifications.get_identifications_by_id_thumb
GET /api/v2/images/{id}/content M.11b brickwork.api2.item_views.get_images_by_id_content
POST /api/v2/imports/bricktracker M.11d brickwork.api2.sync.post_imports_bricktracker
POST /api/v2/imports/{id}/apply M.11d brickwork.api2.sync.post_imports_by_id_apply
GET /api/v2/instruction-files M.11b brickwork.api2.instructions.get_instruction_files
POST /api/v2/instruction-files M.11c brickwork.api2.write_where.post_instruction_files
DELETE /api/v2/instruction-files/{id} M.11c brickwork.api2.write_where.delete_instruction_files_by_id
GET /api/v2/instruction-files/{id} M.11b brickwork.api2.instructions.get_instruction_files_by_id
PATCH /api/v2/instruction-files/{id} M.11c brickwork.api2.write_where.patch_instruction_files_by_id
GET /api/v2/instruction-files/{id}/content M.11b brickwork.api2.instructions.get_instruction_files_by_id_content
POST /api/v2/instruction-files/{id}/restore M.11c brickwork.api2.write_where.post_instruction_files_by_id_restore
GET /api/v2/items M.11b brickwork.api2.catalogue.get_items
GET /api/v2/items/{id} M.11a brickwork.api2.items.get_items_by_id
POST /api/v2/items/{id}/build M.11c brickwork.api2.write_collection.post_items_by_id_build
POST /api/v2/items/{id}/change-variant M.11c brickwork.api2.write_identity.post_items_by_id_change_variant
GET /api/v2/items/{id}/colors M.11b brickwork.api2.item_views.get_items_by_id_colors
GET /api/v2/items/{id}/detail M.11b brickwork.api2.item_views.get_items_by_id_detail
GET /api/v2/items/{id}/images M.11b brickwork.api2.item_views.get_items_by_id_images
POST /api/v2/items/{id}/import M.11d brickwork.api2.jobs.post_items_by_id_import
GET /api/v2/items/{id}/inventory M.11b brickwork.api2.item_views.get_items_by_id_inventory
GET /api/v2/items/{id}/links M.11b brickwork.api2.item_views.get_items_by_id_links
PUT /api/v2/items/{id}/links M.11c brickwork.api2.write_identity.put_items_by_id_links
POST /api/v2/items/{id}/merge M.11c brickwork.api2.write_identity.post_items_by_id_merge
GET /api/v2/items/{id}/prices M.11b brickwork.api2.prices.get_items_by_id_prices
POST /api/v2/items/{id}/prices M.11c brickwork.api2.write_money.post_items_by_id_prices
GET /api/v2/items/{id}/prices/history M.11b brickwork.api2.prices.get_items_by_id_prices_history
POST /api/v2/items/{id}/prices/refresh M.11d brickwork.api2.jobs.post_items_by_id_prices_refresh
GET /api/v2/items/{id}/provider-status M.11b brickwork.api2.item_views.get_items_by_id_provider_status
POST /api/v2/items/{id}/unmerge M.11c brickwork.api2.write_identity.post_items_by_id_unmerge
GET /api/v2/items/{id}/used-in M.11b brickwork.api2.item_views.get_items_by_id_used_in
GET /api/v2/jobs M.11d brickwork.api2.jobs.get_jobs
POST /api/v2/jobs/drain M.11d brickwork.api2.jobs.post_jobs_drain
POST /api/v2/jobs/instruction-identities M.11d brickwork.api2.jobs.post_jobs_instruction_identities
POST /api/v2/jobs/pictures M.11d brickwork.api2.jobs.post_jobs_pictures
GET /api/v2/jobs/{id} M.11d brickwork.api2.jobs.get_jobs_by_id
POST /api/v2/jobs/{id}/resume M.11d brickwork.api2.jobs.post_jobs_by_id_resume
POST /api/v2/jobs/{id}/retry M.11d brickwork.api2.jobs.post_jobs_by_id_retry
GET /api/v2/links M.11b brickwork.api2.item_views.get_links
GET /api/v2/lookup M.11a brickwork.api2.items.get_lookup
GET /api/v2/lots M.11b brickwork.api2.places.get_lots
POST /api/v2/lots M.11c brickwork.api2.write_where.post_lots
DELETE /api/v2/lots/{id} M.11c brickwork.api2.write_where.delete_lots_by_id
GET /api/v2/lots/{id} M.11b brickwork.api2.places.get_lots_by_id
PATCH /api/v2/lots/{id} M.11c brickwork.api2.write_where.patch_lots_by_id
GET /api/v2/lots/{id}/candidate-sets M.11b brickwork.api2.ranking.get_lots_by_id_candidate_sets
GET /api/v2/lots/{id}/pull-list/{set_id} M.11b brickwork.api2.pull_list.get_lots_by_id_pull_list_by_set_id
POST /api/v2/lots/{id}/pull-list/{set_id}/found M.11c brickwork.api2.pull_list.post_lots_by_id_pull_list_by_set_id_found
POST /api/v2/lots/{id}/pull-list/{set_id}/make-copy M.11c brickwork.api2.pull_list.post_lots_by_id_pull_list_by_set_id_make_copy
POST /api/v2/lots/{id}/restore M.11c brickwork.api2.write_where.post_lots_by_id_restore
DELETE /api/v2/lots/{id}/stock/{stock_id}/sticker M.11c brickwork.api2.stickers.delete_lots_by_id_stock_by_stock_id_sticker
POST /api/v2/lots/{id}/stock/{stock_id}/sticker M.11c brickwork.api2.stickers.post_lots_by_id_stock_by_stock_id_sticker
GET /api/v2/lots/{id}/stock/{stock_id}/stickers M.11b brickwork.api2.stickers.get_lots_by_id_stock_by_stock_id_stickers
GET /api/v2/matches M.11b brickwork.api2.loose.get_matches
GET /api/v2/me M.11a brickwork.api2.get_me
GET /api/v2/minifigures M.11b brickwork.api2.loose.get_minifigures
GET /api/v2/minifigures/by-number/{no} M.11a brickwork.api2.items.get_minifigures_by_number_by_no
GET /api/v2/minifigures/{id} M.11b brickwork.api2.item_views.get_minifigures_by_id
GET /api/v2/openapi.yaml M.11a brickwork.api2.get_openapi_yaml
GET /api/v2/orders M.11d brickwork.api2.orders.get_orders
POST /api/v2/orders/documents M.11d brickwork.api2.orders.post_orders_documents
GET /api/v2/orders/export.csv M.11d brickwork.api2.orders.get_orders_export_csv
GET /api/v2/orders/{id} M.11d brickwork.api2.orders.get_orders_by_id
POST /api/v2/orders/{id}/allocate M.11d brickwork.api2.orders.post_orders_by_id_allocate
GET /api/v2/orders/{id}/allocation-review M.11d brickwork.api2.orders.get_orders_by_id_allocation_review
GET /api/v2/orders/{id}/allocations M.11d brickwork.api2.orders.get_orders_by_id_allocations
POST /api/v2/orders/{id}/allocations/retry M.11d brickwork.api2.orders.post_orders_by_id_allocations_retry
POST /api/v2/orders/{id}/documents/{doc_id}/phase M.11d brickwork.api2.orders.post_orders_by_id_documents_by_doc_id_phase
GET /api/v2/orders/{id}/history M.11d brickwork.api2.orders.get_orders_by_id_history
PATCH /api/v2/orders/{id}/lines/{line_id} M.11d brickwork.api2.orders.patch_orders_by_id_lines_by_line_id
PUT /api/v2/orders/{id}/lines/{line_id}/hold M.11d brickwork.api2.orders.put_orders_by_id_lines_by_line_id_hold
POST /api/v2/orders/{id}/received M.11d brickwork.api2.orders.post_orders_by_id_received
POST /api/v2/orders/{id}/reconcile M.11d brickwork.api2.orders.post_orders_by_id_reconcile
GET /api/v2/orders/{id}/short.xml M.11d brickwork.api2.orders.get_orders_by_id_short_xml
GET /api/v2/parts/by-number/{no} M.11a brickwork.api2.items.get_parts_by_number_by_no
GET /api/v2/places M.11b brickwork.api2.places.get_places
POST /api/v2/places M.11c brickwork.api2.write_where.post_places
DELETE /api/v2/places/{id} M.11c brickwork.api2.write_where.delete_places_by_id
PATCH /api/v2/places/{id} M.11c brickwork.api2.write_where.patch_places_by_id
POST /api/v2/places/{id}/restore M.11c brickwork.api2.write_where.post_places_by_id_restore
GET /api/v2/planning/build-scout M.11b brickwork.api2.ranking.get_planning_build_scout
GET /api/v2/planning/ranking M.11b brickwork.api2.ranking.get_planning_ranking
GET /api/v2/planning/set-scores M.11b brickwork.api2.ranking.get_planning_set_scores
GET /api/v2/prices M.11b brickwork.api2.prices.get_prices
POST /api/v2/prices/observations M.11c brickwork.api2.write_money.post_prices_observations
POST /api/v2/prices/refresh M.11d brickwork.api2.jobs.post_prices_refresh
GET /api/v2/prices/stale M.11b brickwork.api2.prices.get_prices_stale
GET /api/v2/sets/by-number/{no} M.11a brickwork.api2.items.get_sets_by_number_by_no
GET /api/v2/settings M.11c brickwork.api2.write_money.get_settings
PATCH /api/v2/settings M.11c brickwork.api2.write_money.patch_settings
GET /api/v2/sources M.11d brickwork.api2.sources.get_sources
PATCH /api/v2/sources/{source} M.11d brickwork.api2.sources.patch_sources_by_source
DELETE /api/v2/sources/{source}/key M.11d brickwork.api2.sources.delete_sources_by_source_key
PUT /api/v2/sources/{source}/key M.11d brickwork.api2.sources.put_sources_by_source_key
POST /api/v2/sources/{source}/key/test M.11d brickwork.api2.sources.post_sources_by_source_key_test
DELETE /api/v2/sources/{source}/user M.11d brickwork.api2.sources.delete_sources_by_source_user
PUT /api/v2/sources/{source}/user M.11d brickwork.api2.sources.put_sources_by_source_user
POST /api/v2/sources/{source}/user/connect M.11d brickwork.api2.sources.post_sources_by_source_user_connect
POST /api/v2/sources/{source}/user/test M.11d brickwork.api2.sources.post_sources_by_source_user_test
GET /api/v2/statistics M.11b brickwork.api2.history.get_statistics
GET /api/v2/stock M.11b brickwork.api2.loose.get_stock
POST /api/v2/stock M.11c brickwork.api2.write_collection.post_stock
POST /api/v2/stock/booklets M.11c brickwork.api2.write_collection.post_stock_booklets
DELETE /api/v2/stock/{id} M.11c brickwork.api2.write_collection.delete_stock_by_id
GET /api/v2/stock/{id} M.11b brickwork.api2.loose.get_stock_by_id
PATCH /api/v2/stock/{id} M.11c brickwork.api2.write_collection.patch_stock_by_id
PUT /api/v2/stock/{id}/flags M.11c brickwork.api2.write_collection.put_stock_by_id_flags
POST /api/v2/stock/{id}/give M.11c brickwork.api2.write_collection.post_stock_by_id_give
GET /api/v2/stock/{id}/parts M.11b brickwork.api2.loose.get_stock_by_id_parts
PATCH /api/v2/stock/{id}/parts M.11c brickwork.api2.write_collection.patch_stock_by_id_parts
POST /api/v2/stock/{id}/photos M.11c brickwork.api2.write_collection.post_stock_by_id_photos
POST /api/v2/stock/{id}/restore M.11c brickwork.api2.write_collection.post_stock_by_id_restore
GET /api/v2/storages M.11b brickwork.api2.places.get_storages
POST /api/v2/storages M.11c brickwork.api2.write_where.post_storages
DELETE /api/v2/storages/{id} M.11c brickwork.api2.write_where.delete_storages_by_id
GET /api/v2/storages/{id} M.11b brickwork.api2.places.get_storages_by_id
PATCH /api/v2/storages/{id} M.11c brickwork.api2.write_where.patch_storages_by_id
POST /api/v2/storages/{id}/contents M.11c brickwork.api2.write_where.post_storages_by_id_contents
POST /api/v2/storages/{id}/restore M.11c brickwork.api2.write_where.post_storages_by_id_restore
GET /api/v2/sync/{source} M.11d brickwork.api2.sync.get_sync_by_source
PUT /api/v2/sync/{source}/link M.11d brickwork.api2.sync.put_sync_by_source_link
GET /api/v2/sync/{source}/lists M.11d brickwork.api2.sync.get_sync_by_source_lists
POST /api/v2/sync/{source}/pull/apply M.11d brickwork.api2.sync.post_sync_by_source_pull_apply
POST /api/v2/sync/{source}/pull/plan M.11d brickwork.api2.sync.post_sync_by_source_pull_plan
POST /api/v2/sync/{source}/send/apply M.11d brickwork.api2.sync.post_sync_by_source_send_apply
POST /api/v2/sync/{source}/send/plan M.11d brickwork.api2.sync.post_sync_by_source_send_plan
GET /api/v2/tokens M.11d brickwork.api2.system.get_tokens
POST /api/v2/tokens M.11d brickwork.api2.system.post_tokens
DELETE /api/v2/tokens/{id} M.11d brickwork.api2.system.delete_tokens_by_id
GET /api/v2/valuation M.11b brickwork.api2.valuation.get_valuation
GET /api/v2/version M.11a brickwork.api2.get_version
GET /api/v2/wishes M.11b brickwork.api2.wishes.get_wishes
POST /api/v2/wishes M.11c brickwork.api2.write_collection.post_wishes
DELETE /api/v2/wishes/{id} M.11c brickwork.api2.write_collection.delete_wishes_by_id
GET /api/v2/wishes/{id} M.11b brickwork.api2.wishes.get_wishes_by_id
PATCH /api/v2/wishes/{id} M.11c brickwork.api2.write_collection.patch_wishes_by_id
PUT /api/v2/wishes/{id}/flags M.11c brickwork.api2.write_collection.put_wishes_by_id_flags
POST /api/v2/wishes/{id}/restore M.11c brickwork.api2.write_collection.post_wishes_by_id_restore
POST /api/v2/wishes/{id}/to-owned M.11c brickwork.api2.write_collection.post_wishes_by_id_to_owned
GET /api/v2/wishlists M.11b brickwork.api2.wishes.get_wishlists
POST /api/v2/wishlists M.11c brickwork.api2.write_collection.post_wishlists
DELETE /api/v2/wishlists/{id} M.11c brickwork.api2.write_collection.delete_wishlists_by_id
PATCH /api/v2/wishlists/{id} M.11c brickwork.api2.write_collection.patch_wishlists_by_id
GET /api/v2/wishlists/{id}/export.csv M.11b brickwork.api2.wishes.get_wishlists_by_id_export_csv
GET /api/v2/wishlists/{id}/export.xml M.11b brickwork.api2.wishes.get_wishlists_by_id_export_xml
POST /api/v2/wishlists/{id}/restore M.11c brickwork.api2.write_collection.post_wishlists_by_id_restore
POST /api/v2/wishlists/{id}/transfer M.11c brickwork.api2.write_collection.post_wishlists_by_id_transfer

Not implemented yet (answer 501)

501 not_implemented with a problem body that also carries {"error": "not_implemented", "piece": "M.11x"}. Authentication, scope and read-only come first, so a client without the right token still sees 401 or 403.

Operation (answers 501 until its piece lands) Piece

5. Decisions

Decided by brick-works under Ulf's delegation on #113 (2026-10-27); design section 7 is the approved frame.

  1. v2 is v1's resource set with the 4.0 identity, not a redesign.
  2. /items/{id} is canonical; the number paths and /lookup answer 200 with the same item body.
  3. The default provider for numbers is bricklink (parts, minifigures); provider=rebrickable for Rebrickable numbers; a set number is the same in both. Several matches are 409 ambiguous, none 404, 422 unidentified on create.
  4. Colours in a request are in the provider's space.
  5. A merged id answers 307 on GET (amended from 301, section 2.1) and 409 merged on writes.
  6. Wish ids are uuids; stored words are served.
  7. The MCP keeps its tool names; its tools take numbers and a provider, never a Brick Work id.

Choices M.11a made where the scope comment and design section 7 were silent:

  • GET /items/{id} returns exactly the section 7 item body. v1's item detail (properties, counts, copies, stock, wishes, prices) moves to GET /items/{id}/detail (M.11b), with links where v1 had matches.
  • /minifigures/{id} (M.11b) is v1's figure aggregate (sets, loose lines, missing, matches, prices) by Brick Work id; the item body of a figure by number is /minifigures/by-number/{no}.
  • provider=brickwork is for colours on id paths only; on number paths and /lookup it is 422.
  • A lookup given color answers data.color next to the item fields.
  • GET /items/{id}/provider-status is read-only: reconcile writes item_provider_status; a person resolves a to-check row by adding a link or merging.
  • PUT /items/{id}/links sets one manual link or makes one preferred for its colour scope; there is no link delete (Undo reverses a mistake).
  • POST /items/{id}/merge takes {"survivor": <item key>} and names the merged item in the path; POST /items/{id}/unmerge names the merged id in the path and an optional merge_id.
  • GET /settings is implemented with the settings writes (M.11c), not with the reads.
  • Undo (POST /events/{id}/undo) is a write and belongs to M.11c.

Appendix A: error codes

code HTTP When
bad_request 400 malformed JSON, wrong content type
unknown_filter 400 filter or sort name not in the vocabulary
unauthorized 401 no, bad, expired or revoked token
forbidden 403 scope too low
read_only_instance 403 BW_READ_ONLY set and a write was attempted
not_found 404 no such path, item or record
method_not_allowed 405 unsupported HTTP method; Allow lists supported methods
api_retired 410 only API v1 answers it: the whole line is gone from 4.0 (its /health excepted); the text names v2
moved 307 new in v2. GET of a merged item id: 307 with Location to the survivor and Cache-Control: no-store; also carries survivor
archived 409 write to an archived record
ambiguous 409 new in v2. a number names several items; candidates lists them as item bodies
merged 409 new in v2. a write names a merged item id; survivor names the live item
conflict 409 state changed, duplicate
stale_match 409 stock or gap counts changed
undo_blocked 409 later change to the same rows
not_undoable 409 event is not a receipt
idempotency_conflict 409 key reused with another body
precondition_failed 412 If-Match mismatch
payload_too_large 413 document over 1 MiB, PDF over 20 MB, photo over 15 MB; any body over its cap is refused unread
unsupported_media_type 415 not JSON or multipart where required
validation_failed 422 field errors
unsupported_setting 422 key not in the whitelist
unidentified 422 new in v2. a create names a number that resolves to no item and cannot make one
source_not_configured 424 key missing for the source
source_disabled 424 source disabled
catalogue_not_loaded 424 no catalogue file
idempotency_key_required 428 required key missing
budget_exhausted 429 daily budget spent, synchronous call
source_throttled 429 remote asked to wait
too_many_attempts 429 too many failed token checks from this client; Retry-After and retry_after give the wait
source_error 502 upstream failure, redacted text
not_set_up 503 database file missing
api_disabled 503 no token configured
db_unavailable 503 SQLite error
internal_error 500 anything else, no detail leaked
not_implemented 501 new in v2. the operation's piece has not landed; the body also carries piece and error