Blueprints
The member’s blueprint set: which products they can craft. Reading needs
exchange.blueprints.read, changing needs exchange.blueprints.write.
A blueprint #
A blueprint is one product in the set:
| Field | Meaning |
|---|---|
key |
Opaque identifier of the product. It equals ref.bt: the Basetool’s product key, or h: and a SHA-256 when that key is longer than 128 characters. |
ref |
{bt, name}; name is the product’s display name, cut to 200 characters. |
acquiredAt |
When the member got it, when known. |
provenance |
{source} when the Basetool recorded where the blueprint came from. |
isDefault |
Granted to every member by default. It cannot be removed. |
note |
The member’s note. Read-only in v1. |
Reading the set — GET /exchange/v1/me/blueprints #
| Parameter | Meaning |
|---|---|
cursor |
Opaque position from an earlier page. Without it the answer is a snapshot. |
limit |
Page size, 1 to 1000, default 500. Anything else is 400 SCHEMA_INVALID at /limit. |
The answer is a page: items, removed, nextCursor and hasMore.
Snapshot. Without cursor, the pages walk through the whole set; removed is always empty.
While hasMore is true, ask again with nextCursor. The last page has hasMore: false and a
nextCursor that is now a feed position: store it.
Feed. With a feed cursor, a page answers every product that changed after it, each once: its
current state in items, or a tombstone in removed when it is gone. nextCursor is always set;
hasMore: true means more changes are waiting. A change made while a snapshot was read follows in
the feed, so a product can arrive in both — apply entries by key.
{
"items": [
{"key": "k1", "ref": {"bt": "k1", "name": "CF-337 Panther Repeater"}, "isDefault": false}
],
"removed": [
{
"key": "k2",
"removedAt": "2026-09-26T12:00:00Z",
"removedBy": {"channel": "client", "clientId": "versekit", "installationId": "inst-7f3c2a9e"}
}
],
"nextCursor": "c-18",
"hasMore": false
}
A tombstone names who removed the entry: channel is web, app, client (with
clientId, and installationId of the removing installation) or system — default-grant changes,
administrative clean-ups and account deletion. A tombstone whose installationId is your own is
your own removal.
Tombstones are kept 90 days. A cursor older than that, or one the server did not issue, answers
410 CURSOR_EXPIRED: start over as the
sync guide describes. A feed page that
reaches the end moves the cursor up to the present, so a client that reads its feed at least once
every 90 days never loses its cursor.
Changing the set — POST /exchange/v1/me/blueprints/changes #
Send an Idempotency-Key and a
change-set of 1 to 500 ops, dryRun optional:
{
"ops": [
{
"op": "add",
"ref": {"bt": "bp-cf-337-panther-repeater", "name": "CF-337 Panther Repeater"},
"provenance": {"source": "log", "observedAt": "2026-09-26T12:00:00Z"}
},
{"op": "remove", "key": "k9", "opId": "r1"}
]
}
| Op | Fields |
|---|---|
add |
ref, optional acquiredAt, provenance, override |
remove |
key from the feed (preferred) or ref |
Every op may carry an opId of your own, echoed in its result. The server resolves the references
as catalog/resolve does, decides the ops in order as if the earlier ones had already
run, and writes the batch in one transaction through the same paths the web uses: the member sees the
change live, audited under your client’s name. add records provenance.source, and a client’s
default is stored as other.
How an op that is not applied is reported:
| Case | result |
reason |
|---|---|---|
add of a product the member already has |
unchanged |
— |
remove of a product the member does not have |
unchanged |
— |
The reference matches nothing, or an h: key no product hashes to |
unmatched |
UNMATCHED |
| The reference matches several products | ambiguous |
AMBIGUOUS |
remove of a default-granted blueprint |
rejected |
DEFAULT_NOT_REMOVABLE |
add of a product whose last change was its removal by the web, the app, the system, another client or another installation |
rejected |
REMOVED_ELSEWHERE |
REMOVED_ELSEWHERE holds while that removal is within the 90 days the feed keeps it. An
installation may re-add what it removed itself. To re-add something removed elsewhere, ask the
member first, then send the add again with override: true; the override is recorded.
The answer is a change-result:
{
"dryRun": false,
"applied": 1,
"unchanged": 0,
"notApplied": 1,
"results": [{"index": 1, "opId": "r1", "result": "rejected", "reason": "DEFAULT_NOT_REMOVABLE"}]
}
applied counts the ops that changed something, unchanged those that found what they asked for,
notApplied the rest. results[] lists every op that was not applied, unchanged ones included,
by its index in the batch. warnings[] reports fields the schema does not declare as
UNKNOWN_FIELD.
dryRun: true decides every op and answers the same counts, but writes nothing and does not ask
the mass-change guard — a dry run cannot tell you whether the real batch will need the member’s
confirmation.
Mass changes. Removals count against the mass-change guard. A batch that trips it writes nothing
and answers 409 MASS_CHANGE_CONFIRMATION_REQUIRED with a confirmationUrl; see the
sync guide.
Errors #
| Code | When |
|---|---|
400 SCHEMA_INVALID |
The change set, limit or a parameter breaks the contract. |
400 IDEMPOTENCY_KEY_MISSING |
A change set without a well-formed Idempotency-Key. |
409 MASS_CHANGE_CONFIRMATION_REQUIRED |
The batch removes too much; the member confirms it. |
410 CURSOR_EXPIRED |
The cursor is older than the tombstones, or not one the server issued. |
413 BATCH_TOO_LARGE |
More than 500 ops, or too large to hold for the member’s confirmation. |
The full list is the error registry.