A bid, end to end
One whole bid on an integration key, from an empty start to the order ReSync creates when the bid converts: one customer, one bid, a design set with five lines, an installation line and a modification. Every response below is a real capture from one continuous run against a running server, in the order it happened — nothing here is illustrative. The resources are a test customer, "Harbor Homes", and its kitchen bid; your ids, names and price levels will differ.
Start with the Quickstart: it sets up the token and the api() wrapper, and shows the five calls this
page builds on — who your key is, the type codes, the vocabulary, a list read and an attestation.
| # | Step | Status of the bid | Doors |
|---|---|---|---|
| 1 | Bring in the customer and the bid by your own identifiers | Draft | POST /v1/resources/find-or-create, PATCH /v1/resources/:id |
| 2 | Fill the bid's three fields | Draft | GET …/ports, GET …/ports/:portId/legal-targets, POST /v1/resource-interactions |
| 3 | A design set inside the bid, and its ten fields | Draft | POST /v1/resources/:id/members, legal-targets |
| 4 | Lines, each with its product | Draft | find-or-create, members, PATCH, legal-targets with q |
| 5 | Change or remove a field; the one-set limit | Draft | POST /v1/resource-interactions with replaceInteractionId, DELETE /v1/resource-interactions/:id |
| 6 | Installation and modification lines | Draft | find-or-create, members |
| 7 | Freeze bottom-up | Draft → lines and set Frozen | GET /v1/state-transitions, GET …/requirements, PATCH …/status |
| 8 | Submit | Sent | attestations, GET …/requirements, PATCH …/status |
| 9 | Accept | Accepted | attestations, GET …/events |
| 10 | Convert — one call; the move records the conversion | Converted | GET …/requirements, GET …/advancement, PATCH …/status |
| 11 | The order | Order: Received | GET /v1/resources/:id, …/events, …/members, GET /v1/resources?typeKey=cab_order&q=… |
| 12 | Accepted internally, by a named person | Accepted | attestations with actorResourceId |
Steps that use the same doors again (the second field, the next line) are shown once.
1. Bring in the customer and the bid
find-or-create is the idempotent way in: the same identifier from your system always answers the same resource. Its
body is snake_case, unlike everywhere else. The customer comes first (type_key: "cab_customer"), then the bid:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"type_key":"cab_bid","external_system":"pdmac-sql","external_key":"docs-bid-1","name":"Harbor Homes — kitchen"}' \
"$BASE/v1/resources/find-or-create"
From bid-foc-create.json — status 201:
{
"resource_id": 24911,
"created": true
}
Called again with the same identifier, it answers 200 with "created": false and changes nothing.
external_system names an external system ReSync already knows; your administrator tells you which.
Set the bid's own values with a PATCH. properties merges — keys you do not send are left alone:
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"properties":{"tax_rate_bp":825}}' "$BASE/v1/resources/<bidId>"
From bid-patch-tax.json — status 200:
{
"resourceId": "24911",
"resourceTypeId": "176",
"ownerResourceId": "5",
"name": "Harbor Homes — kitchen",
"identifier": null,
"properties": {
"tax_rate_bp": 825
},
"statusId": "280",
"isVerified": false,
"confirmedKeys": {},
"createdAt": "2026-10-21T18:47:21.014Z"
}
Behind this call ReSync recorded an event saying which property changed, from what, to what, and that your key wrote it — see events.md.
2. Fill the bid's three fields
A bid's customer, price level and payment terms are fields: a port that holds one choice. Three calls fill one:
GET /v1/resources/$BID/ports— find the port by itsportName(customer,level,terms); keep itsresourcePortIdanddefaultInteractionTypeId.GET /v1/resources/$BID/ports/$PORT/legal-targets?scope=type— the resources the field may hold, each with averdictand the target port to use (viableTargetPorts[0]). Narrow a long list withqandlimit.POST /v1/resource-interactions— the attachment.
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"sourceResourceId":<bidId>,"targetResourceId":<customerId>,"interactionTypeId":<defaultInteractionTypeId>,"sourcePortId":<customerPortId>,"targetPortId":<targetPortId>}' \
"$BASE/v1/resource-interactions"
From bid-field-customer.json — status 201:
{
"interactionId": 2720,
"sourceResourceId": 24911,
"targetResourceId": 24910,
"interactionTypeId": 23,
"contextId": null
}
The customer's own price level and payment terms are filled the same way, before the bid. Read the bid back — its
edges list the three fields:
From bid-get-draft.json — status 200:
{
"resourceId": "24912",
"resourceTypeId": "176",
"ownerResourceId": "5",
"name": "Harbor Homes — kitchen",
"identifier": null,
"properties": {
"tax_rate_bp": 825
},
"statusId": "280",
"isVerified": false,
"createdAt": "2026-10-24T00:14:12.321Z",
"archivedAt": null,
"derivedFromTypeId": null,
"resourceTypeName": "Bid",
"propertySchema": {
"tax_rate_bp": {
"type": "number",
"label": "Tax rate (basis points)",
"derivable": false
}
},
"declaredPropertySchema": {
"tax_rate_bp": {
"type": "number",
"label": "Tax rate (basis points)",
"derivable": false
}
},
"orgName": "Popular Design Mill & Cabinet, Inc.",
"ownerOrgId": "1",
"canvasMode": null,
"statusName": "Draft",
"statusColorCode": "#64748b",
"icon": "box",
"lifecycleId": 108,
"canvasId": null,
"derivedFrom": [],
"derivatives": [],
"derivedKeys": [],
"edges": [
{
"interactionId": 2724,
"typeName": "bid_for",
"direction": "out",
"otherResourceId": 24911,
"otherResourceName": "Harbor Homes",
"thisResourcePortId": 7714
},
{
"interactionId": 2725,
"typeName": "priced_at",
"direction": "out",
"otherResourceId": 13808,
"otherResourceName": "Contractor",
"thisResourcePortId": 7715
},
{
"interactionId": 2726,
"typeName": "pays_on",
"direction": "out",
"otherResourceId": 14013,
"otherResourceName": "Net 30",
"thisResourcePortId": 7716
}
],
"otherEdges": [],
"confirmedKeys": [],
"readOnly": null
}
What to notice. statusName is Draft, and readOnly is null: everything about a Draft bid can still change.
3. A design set inside the bid
Create the set with find-or-create (type_key: "cab_design_set"), then put it inside the bid:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"childResourceId":<setId>}' "$BASE/v1/resources/<bidId>/members"
From members-enrol.json — status 201:
{
"componentId": 262526,
"parentResourceId": 24911,
"childResourceId": 24912,
"componentRole": null,
"sourceSlotId": null,
"sourceRuleId": null,
"componentStatus": "active",
"statusChangedAt": "2026-10-21T18:47:22.424Z",
"sortOrder": 0,
"properties": {},
"createdAt": "2026-10-21T18:47:22.424Z"
}
childResourceId is a JSON number. A design set has ten material and hardware fields — Species, Finish, Sheen,
Interior, Upper Door Style, Lower Door Style, Drawer, Slide, Front, Hinge — filled like any field (step 2). Each list
says, per candidate, whether it fits what the set already holds:
From set-axis-legal-targets.json — status 200:
{
"sourcePort": {
"portId": 7717,
"portName": "Species",
"direction": "out",
"maxConnections": 1,
"held": 0,
"capRemaining": 1
},
"candidates": [
{
"resourceId": 7272,
"name": "Outsource Specie",
"resourceTypeId": 1404,
"verdict": "legal",
"reasons": [],
"viableTargetPorts": [
{
"portId": 1501,
"portName": "DesignSets",
"direction": "in"
}
]
},
{
"resourceId": 7271,
"name": "TBD",
"resourceTypeId": 1404,
"verdict": "legal",
"reasons": [],
"viableTargetPorts": [
{
"portId": 1500,
"portName": "DesignSets",
"direction": "in"
}
]
},
{
"resourceId": 7270,
"name": "Rustic Walnut",
"resourceTypeId": 1404,
"verdict": "legal",
"reasons": [],
"viableTargetPorts": [
{
"portId": 1499,
"portName": "DesignSets",
"direction": "in"
}
]
},
{
"resourceId": 7269,
"name": "HDF/Paint Grade",
"resourceTypeId": 1404,
"verdict": "legal",
"reasons": [],
"viableTargetPorts": [
{
"portId": 1498,
"portName": "DesignSets",
"direction": "in"
}
]
},
{
"resourceId": 7268,
"name": "Rift White Oak",
"resourceTypeId": 1404,
"verdict": "legal",
"reasons": [],
"viableTargetPorts": [
{
"portId": 1497,
"portName": "DesignSets",
"direction": "in"
}
]
}
],
"truncated": true,
"total": 17
}
A verdict other than legal comes with reasons naming why — a value the set's other choices rule out is
compat_deny. Legal-targets is your compatibility check: it gives the same answer the attachment would, before you
make it. There is no bulk read of compatibility rules for a key; ask the field.
4. Lines, each with its product
Look the product up in the price book first — a list over its type with q:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/resources?typeKey=cab_catalog_product&q=APO&limit=5&include=properties"
The answer is list-products-q.json in the fixture set: each product's identifier and its list_cents. Then, for
each line: find-or-create (type_key: "cab_line"), POST /v1/resources/$SET/members, a PATCH for its quantities
and prices, and its product field.
From line-foc-create.json — status 201:
{
"resource_id": 24913,
"created": true
}
The product field's list, narrowed to the product's identifier:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/resources/<lineId>/ports/<productPortId>/legal-targets?scope=type&q=APO06&limit=5"
From legal-targets-q.json — status 200:
{
"sourcePort": {
"portId": 7736,
"portName": "product",
"direction": "out",
"maxConnections": 1,
"held": 0,
"capRemaining": 1
},
"candidates": [
{
"resourceId": 9009,
"name": "APO06",
"resourceTypeId": 182,
"verdict": "legal",
"reasons": [],
"viableTargetPorts": [
{
"portId": 4606,
"portName": "Sold as",
"direction": "in"
}
]
}
],
"truncated": false,
"total": 1
}
From line-product-field.json — status 201:
{
"interactionId": 2733,
"sourceResourceId": 24913,
"targetResourceId": 9009,
"interactionTypeId": 278,
"contextId": null
}
A line never carries the product identifier itself; it points at the product. A line inherits each of the set's ten
fields unless it chooses its own — its own list for one of them is line-axis-legal-targets.json in the fixture set.
Save the whole bid in one request
Sections 1 to 4 and 6 build the bid one resource at a time. The same find-or-create door can take the whole bid at
once: each resource carries its fields — by portName, pointing at the resource you chose — and the resources inside
it as children. The ids are the ones you would have picked in §2–§4: the customer, the price level and terms, the ten
design choices (each from its field's legal targets) and each line's product. This is the body of the capture below,
with lines three to five left out here — they have the same shape as line two:
{
"type_key": "cab_bid",
"external_system": "pdmac-sql",
"external_key": "docs-tree-bid",
"name": "Harbor Homes — laundry",
"properties": {
"tax_rate_bp": 825
},
"fields": {
"customer": 24910,
"level": 13808,
"terms": 14013
},
"children": [
{
"type_key": "cab_design_set",
"external_system": "pdmac-sql",
"external_key": "docs-tree-set",
"name": "Laundry",
"fields": {
"Species": 7272,
"Finish": 7340,
"Sheen": 7347,
"Interior": 7344,
"Upper Door Style": 24753,
"Lower Door Style": 24783,
"Drawer": 7382,
"Slide": 7392,
"Front": 7350,
"Hinge": 7387
},
"children": [
{
"type_key": "cab_line",
"external_system": "pdmac-sql",
"external_key": "docs-tree-line-1",
"name": "Sink base",
"properties": {
"qty": 1,
"area": "Laundry",
"unit_cents": 7001,
"ext_cents": 7001
},
"fields": {
"product": 9009
},
"children": [
{
"type_key": "cab_modification",
"external_system": "pdmac-sql",
"external_key": "docs-tree-mod-1",
"name": "Finished end"
}
]
},
{
"type_key": "cab_line",
"external_system": "pdmac-sql",
"external_key": "docs-tree-line-2",
"name": "Base, two drawer",
"properties": {
"qty": 1,
"area": "Laundry",
"unit_cents": 14003,
"ext_cents": 14003
},
"fields": {
"product": 9010
}
}
]
},
{
"type_key": "cab_install",
"external_system": "pdmac-sql",
"external_key": "docs-tree-install",
"name": "Installation",
"properties": {
"qty": 1,
"override_list_cents": 50000,
"unit_cents": 50000,
"ext_cents": 50000
}
}
]
}
One request, one 201. The answer mirrors the body — every resource's id, whether it was created, and its children in
the order you sent them:
From foc-tree-create.json — status 201:
{
"resource_id": 24925,
"created": true,
"children": [
{
"resource_id": 24926,
"created": true,
"children": [
{
"resource_id": 24927,
"created": true,
"children": [
{
"resource_id": 24928,
"created": true,
"children": []
}
]
},
{
"resource_id": 24929,
"created": true,
"children": []
},
{
"resource_id": 24930,
"created": true,
"children": []
},
{
"resource_id": 24931,
"created": true,
"children": []
},
{
"resource_id": 24932,
"created": true,
"children": []
}
]
},
{
"resource_id": 24933,
"created": true,
"children": []
}
]
}
It is all or nothing. If any part is refused — here line three's product field points at a design choice, not a
product — nothing in the request is created, and the answer is that part's own error with item saying which part:
From foc-tree-refused-item.json — status 422:
{
"error": "accepted_type",
"message": "This port does not accept resources of type Species.",
"portId": 7928,
"endpoint": "source",
"offendingResourceId": 7272,
"offendingTypeId": 1404,
"item": {
"path": "children[0].children[2]",
"external_key": "docs-tree-bad-line-3"
}
}
Sending the same body again changes nothing and answers 200 with created: false throughout
(foc-tree-replay.json), so a request that timed out can simply be resent. From here the bid goes on as in §7: freeze,
Submit, and so on — those are still one request per move. A field changes as in §5.
5. Change or remove a field; the one-set limit
A field holds one choice. To change it, send replaceInteractionId with the connection it replaces — here line two
moves to a different product in one call:
From interaction-replace.json — status 201:
{
"interactionId": 2739,
"sourceResourceId": 24914,
"targetResourceId": 9009,
"interactionTypeId": 278,
"contextId": null
}
To remove a choice, delete the connection: DELETE /v1/resource-interactions/<interactionId> answers 204 with no body
(interaction-delete.json). Here that removes line one's own Hinge, and the line inherits the set's Hinge again.
A line sits in at most one design set. Putting it in a second is refused, and nothing changes:
From members-422-containment-cap.json — status 422:
{
"error": "containment_cap",
"message": "This item can be inside at most 1 Design Set.",
"childId": 24913,
"parentTypeId": 1171,
"maxParents": 1
}
6. Installation and modification lines
An installation line sits on the bid itself; a modification sits under the line it modifies. Both are
find-or-create plus members, like a line:
From line-install-foc.json — status 201:
{
"resource_id": 24919,
"created": true
}
A modification sub-line carries its own name and price. It has no field pointing at the price-book modification it
came from, and it cannot take that modification's identifier (sku): the price book's identifiers are unique, so the
attempt is 409 unique_key_taken (unique-key-taken-409.json). Keep the link in your own system.
7. Freeze bottom-up
GET /v1/state-transitions?resourceId=$SET&fromStatusId=<its statusId> lists the moves; each has a transitionId.
The checklist for one move:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/resources/<setId>/requirements?transitionId=<freezeTransitionId>"
From requirements-read.json — status 200:
{
"requirements": [
{
"requirementId": 43,
"name": "R-SET-FREEZE — all lines frozen",
"blocking": "hard",
"satisfied": false,
"missing": [
"All lines frozen"
],
"leaves": [
{
"leaf": "members",
"label": "All lines frozen",
"caption": "0/5 members at required status",
"satisfied": false,
"attestable": false,
"actionLabel": null,
"actor": null,
"actorRoles": [],
"payloadFields": [],
"blockedByActor": false,
"satisfiedEver": false,
"mayEmitUnvocab": false,
"memberTotal": 5,
"memberSatisfied": 0
}
]
},
{
"requirementId": 44,
"name": "Contents compatible",
"blocking": "hard",
"satisfied": true,
"missing": [],
"leaves": [
{
"leaf": "subtree_compat",
"label": "No design set settings conflicts",
"caption": "everything inside fits",
"satisfied": true,
"attestable": false,
"actionLabel": null,
"actor": null,
"actorRoles": [],
"payloadFields": [],
"blockedByActor": false,
"satisfiedEver": false,
"mayEmitUnvocab": false,
"violations": [],
"violationCount": 0,
"subtreeCompatVacuous": false
}
]
}
]
}
A set freezes only when every line in it is frozen; asked too early, the answer is 422 with this same checklist
(patch-status-422-requirements.json). So freeze each line first — the modification under it, then the line:
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"toStatusId":<frozenStatusId>}' "$BASE/v1/resources/<lineId>/status"
From patch-status-200.json — status 200:
{
"resourceId": 24913,
"fromStatusId": 1318,
"statusId": 1319
}
Then the installation line and the set. Freezing records a snapshot of the line (resource.captured, see
events.md; events-resource-captured.json). A frozen line is read-only, and there is no way back to
Draft: asking is 409 (patch-status-409-invalid-transition.json).
8. Submit
The bid's own moves from Draft:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/state-transitions?resourceId=<bidId>&fromStatusId=<draftStatusId>"
From bid-state-transitions.json — status 200:
{
"transitions": [
{
"transitionId": 818,
"fromStatusId": 280,
"toStatusId": 1314,
"actionLabel": "Submit",
"autoAdvance": false,
"toStatusName": "Sent",
"toStatusPreventsUsage": false,
"toStatusIsAllocating": false,
"toStatusSubjectReadOnly": true
},
{
"transitionId": 1271,
"fromStatusId": 280,
"toStatusId": 282,
"actionLabel": "Accept internally",
"autoAdvance": false,
"toStatusName": "Accepted",
"toStatusPreventsUsage": false,
"toStatusIsAllocating": false,
"toStatusSubjectReadOnly": true
}
]
}
Record the totals your application computed — the bid.sent attestation, shown in the
Quickstart. It is recorded once: a second is 409
(attestation-409-not-repeatable.json). Then check Submit's checklist:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/resources/<bidId>/requirements?transitionId=<submitTransitionId>"
From bid-requirements-submit.json — status 200:
{
"requirements": [
{
"requirementId": 45,
"name": "R-BID-SEND-M — all design sets & install lines frozen",
"blocking": "hard",
"satisfied": true,
"missing": [],
"leaves": [
{
"leaf": "members",
"label": "All design sets frozen",
"caption": "1/1 members at required status",
"satisfied": true,
"attestable": false,
"actionLabel": null,
"actor": null,
"actorRoles": [],
"payloadFields": [],
"blockedByActor": false,
"satisfiedEver": false,
"mayEmitUnvocab": false,
"memberTotal": 1,
"memberSatisfied": 1
},
{
"leaf": "members",
"label": "All install lines frozen",
"caption": "1/1 members at required status",
"satisfied": true,
"attestable": false,
"actionLabel": null,
"actor": null,
"actorRoles": [],
"payloadFields": [],
"blockedByActor": false,
"satisfiedEver": false,
"mayEmitUnvocab": false,
"memberTotal": 1,
"memberSatisfied": 1
}
]
},
{
"requirementId": 47,
"name": "R-BID-SEND-LEVEL — price level set",
"blocking": "hard",
"satisfied": true,
"missing": [],
"leaves": [
{
"leaf": "edge",
"label": "Priced at connection to a Price Level",
"caption": "present",
"satisfied": true,
"attestable": false,
"actionLabel": null,
"actor": null,
"actorRoles": [],
"payloadFields": [],
"blockedByActor": false,
"satisfiedEver": false,
"mayEmitUnvocab": false
}
]
},
{
"requirementId": 46,
"name": "R-BID-SEND-E — bid totals recorded",
"blocking": "hard",
"satisfied": true,
"missing": [],
"leaves": [
{
"leaf": "evidence",
"label": "Bid totals recorded (bid.sent)",
"caption": "recorded",
"satisfied": true,
"attestable": true,
"actionLabel": "Send bid",
"eventType": "bid.sent",
"payloadKey": "total_cents",
"payloadPredicate": {
"exists": true
},
"actor": null,
"actorRoles": [],
"payloadFields": [
{
"key": "subtotal_cents",
"kind": "number",
"label": "subtotal_cents",
"required": false
},
{
"key": "discount_cents",
"kind": "number",
"label": "discount_cents",
"required": false
},
{
"key": "delivery_cents",
"kind": "number",
"label": "delivery_cents",
"required": false
},
{
"key": "tax_cents",
"kind": "number",
"label": "tax_cents",
"required": false
},
{
"key": "install_cents",
"kind": "number",
"label": "install_cents",
"required": false
},
{
"key": "total_cents",
"kind": "number",
"label": "total_cents",
"required": false
},
{
"key": "tax_rate_bp",
"kind": "number",
"label": "tax_rate_bp",
"required": false
}
],
"repeatable": false,
"blockedByActor": false,
"satisfiedEver": false,
"mayEmitUnvocab": false
}
]
},
{
"requirementId": 48,
"name": "R-BID-SEND-TERMS — payment terms set",
"blocking": "hard",
"satisfied": true,
"missing": [],
"leaves": [
{
"leaf": "edge",
"label": "Pays on connection to a Payment Terms",
"caption": "present",
"satisfied": true,
"attestable": false,
"actionLabel": null,
"actor": null,
"actorRoles": [],
"payloadFields": [],
"blockedByActor": false,
"satisfiedEver": false,
"mayEmitUnvocab": false
}
]
}
]
}
Submit checks that the bid holds at least one design set, that every design set and install line is frozen, that the totals are recorded, and that the bid's level and terms are filled. Then move it:
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"toStatusId":<sentStatusId>}' "$BASE/v1/resources/<bidId>/status"
From bid-status-submit-sent.json — status 200:
{
"resourceId": 24911,
"fromStatusId": 280,
"statusId": 1314
}
A sent bid is read-only:
From status-read-only-409.json — status 409:
{
"error": "status_read_only",
"message": "Harbor Homes — kitchen is Sent, a read-only status — its properties cannot be changed.",
"resourceId": 24911,
"statusId": 1314
}
GET /v1/resources/<bidId>/advancement answers the bid, its moves from Sent and each move's checklist in one call —
see endpoints.md.
9. Accept
The customer's acceptance is the bid.accepted attestation:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"eventType":"bid.accepted","payload":{}}' "$BASE/v1/resources/<bidId>/attestations"
From bid-attest-accepted.json — status 201:
{
"eventUlid": "01M5FWMHYZ4Y68TMB3N8AJKJBQ",
"eventType": "bid.accepted",
"resourceId": 24911,
"actorResourceId": 24907
}
The bid then moves to Accepted on its own, a moment later. Its history shows who did what — the move is ReSync's, the acceptance is your key's:
From bid-advance-accepted.json — status 200:
{
"items": [
{
"event_ulid": "01M5FWMJ1MDV7XZC4K10MH8AHM",
"event_type": "status.changed",
"occurred_at": "2026-10-21T18:47:34.709Z",
"actor": {
"resource_id": 5,
"name": "Popular Design Mill & Cabinet, Inc."
},
"display": {
"from_status_id": 1314,
"to_status_id": 282,
"from_status_name": "Sent",
"to_status_name": "Accepted",
"text": "Sent → Accepted"
},
"producer_id": "advance.worker"
},
{
"event_ulid": "01M5FWMHYZ4Y68TMB3N8AJKJBQ",
"event_type": "bid.accepted",
"occurred_at": "2026-10-21T18:47:34.623Z",
"actor": {
"resource_id": 24907,
"name": "bid-app-docs"
},
"display": {
"attested_action_label": "Accept bid"
},
"producer_id": "bid-app-docs"
},
{
"event_ulid": "01M5FWMHNVHG3M0ZEG8ZNM1HF5",
"event_type": "status.changed",
"occurred_at": "2026-10-21T18:47:34.331Z",
"actor": {
"resource_id": 24907,
"name": "bid-app-docs"
},
"display": {
"from_status_id": 280,
"to_status_id": 1314,
"from_status_name": "Draft",
"to_status_name": "Sent",
"text": "Draft → Sent"
},
"producer_id": "bid-app-docs"
}
],
"nextCursor": "01M5FWMHNVHG3M0ZEG8ZNM1HF5",
"afterCursor": "c1MTMwMTMxOS4yMzc2NA"
}
10. Convert
Convert's checklist asks for one thing — the bid's customer:
From bid-requirements-convert.json — status 200:
{
"requirements": [
{
"requirementId": 1531,
"name": "R-CONVERT-EDGE — customer edge present",
"blocking": "hard",
"satisfied": true,
"missing": [],
"leaves": [
{
"leaf": "edge",
"label": "Bid for connection to a Customer",
"caption": "present",
"satisfied": true,
"attestable": false,
"actionLabel": null,
"actor": null,
"actorRoles": [],
"payloadFields": [],
"blockedByActor": false,
"satisfiedEver": false,
"mayEmitUnvocab": false
}
]
}
]
}
Converting is one call. The move to Converted records the conversion itself — the advancement read names it under
that move's records, { "eventType": "bid.converted", "label": "Convert to order" } (bid-advancement-accepted.json):
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"toStatusId":<convertedStatusId>}' "$BASE/v1/resources/<bidId>/status"
From bid-status-convert.json — status 200:
{
"resourceId": 24911,
"fromStatusId": 282,
"statusId": 1315
}
Do not post bid.converted afterwards. It is already on the bid, so a second one is refused and nothing changes:
From bid-attest-converted-409.json — status 409:
{
"error": "attestation_not_repeatable",
"message": "Convert to order is already recorded for Harbor Homes — kitchen in its current status.",
"resourceId": 24911,
"eventType": "bid.converted"
}
11. The order
ReSync creates the order a moment after the move to Converted. Read the bid until its derivatives lists it
(get-derivatives-after-convert.json), or find it by name:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/resources?typeKey=cab_order&q=Harbor%20Homes&limit=5"
From list-orders-q.json — status 200:
{
"resources": [
{
"resourceId": "4714",
"name": "Order — Harbor Homes — 2026-10-26",
"identifier": null,
"ownerResourceId": 4,
"resourceTypeId": "28",
"resourceTypeName": "Order",
"isSystem": false,
"createdAt": "2026-10-27T04:13:31.366Z",
"statusId": 30,
"statusName": "Received",
"statusColor": "#64748b",
"archivedAt": null,
"icon": "box"
}
],
"nextCursor": null
}
The order is named once, when it is created — "Order — " plus the bid's customer and the date — and keeps that name. Read it:
From order-get.json — status 200:
{
"resourceId": "4714",
"resourceTypeId": "28",
"ownerResourceId": "4",
"name": "Order — Harbor Homes — 2026-10-26",
"identifier": null,
"properties": {},
"statusId": "30",
"isVerified": false,
"createdAt": "2026-10-27T04:13:31.366Z",
"archivedAt": null,
"derivedFromTypeId": null,
"resourceTypeName": "Order",
"propertySchema": {},
"declaredPropertySchema": {},
"orgName": "Popular Design Mill & Cabinet, Inc.",
"ownerOrgId": "3",
"canvasMode": null,
"statusName": "Received",
"statusColorCode": "#64748b",
"icon": "box",
"lifecycleId": 7,
"canvasId": null,
"derivedFrom": [
{
"resourceId": 4704,
"name": "Harbor Homes — kitchen",
"typeName": "Bid"
}
],
"derivatives": [],
"derivedKeys": [],
"edges": [],
"otherEdges": [],
"confirmedKeys": [],
"readOnly": null
}
What to notice. derivedFrom points back at the bid. The order is your organization's, and your key may read it
but not change it. Its history shows ReSync created it:
From order-events.json — status 200:
{
"items": [
{
"event_ulid": "01M5XS0DVMVEXBT6DYWBKX33BA",
"event_type": "interaction.created",
"occurred_at": "2026-10-27T04:13:31.380Z",
"actor": {
"resource_id": 4,
"name": "Popular Design Mill & Cabinet, Inc."
},
"display": {
"sourcePortId": null,
"targetPortId": null,
"interactionId": 860,
"sourceResourceId": 4714,
"targetResourceId": 4704,
"interactionTypeId": 11,
"text": "connected Order — Harbor Homes — 2026-10-26 — derives from → Harbor Homes — kitchen",
"interactionTypeName": "derives_from",
"forwardLabel": "derives from",
"reverseLabel": "source of",
"sourceName": "Order — Harbor Homes — 2026-10-26",
"targetName": "Harbor Homes — kitchen"
},
"producer_id": "consequence.worker"
},
{
"event_ulid": "01M5XS0DVBRCV8RQXC1D839G7H",
"event_type": "status.changed",
"occurred_at": "2026-10-27T04:13:31.372Z",
"actor": {
"resource_id": 4,
"name": "Popular Design Mill & Cabinet, Inc."
},
"display": {
"from_status_id": null,
"to_status_id": 30,
"from_status_name": null,
"to_status_name": "Received",
"text": "set to Received"
},
"producer_id": "consequence.worker"
},
{
"event_ulid": "01M5XS0DVA3MBY3T6JXJ1SB0WT",
"event_type": "resource.created",
"occurred_at": "2026-10-27T04:13:31.370Z",
"actor": {
"resource_id": 4,
"name": "Popular Design Mill & Cabinet, Inc."
},
"display": {
"name": "Order — Harbor Homes — 2026-10-26",
"resource_type_id": 28,
"derived_from_resource_id": 4704,
"consequence_rule_id": 1,
"triggered_by_event_ulid": "01M5XS0DSKHFD0S5ZVYBXZE4RP",
"text": "✳ created — Order"
},
"producer_id": "consequence.worker"
}
],
"nextCursor": null,
"afterCursor": "c1MTg2NTMxNS40Njky"
}
The order starts empty — the bid's lines are not copied into it:
From order-members-empty.json — status 200:
[]
12. Accepted internally, by a named person
Some bids are accepted inside your organization rather than by the customer: bid.accepted_internally, naming the
person who approved it in actorResourceId. That person must belong to your organization; anyone else is
403 actor_not_readable (attestation-403-actor-not-readable.json), and nothing is recorded.
Like Convert, accepting internally needs the bid's customer field (§2) — for a bid sold inside your organization,
the internal customer. Without it the move to Accepted is 422, naming "R-ACCEPT-INTERNAL-EDGE — customer edge present"
among its requirements, and the bid stays Draft.
From bid-attest-accepted-internally.json — status 201:
{
"eventUlid": "01M5FWMM2YF3QKXSW8RHBE4XJS",
"eventType": "bid.accepted_internally",
"resourceId": 24922,
"actorResourceId": 24905
}
Things to know
- Every resource you create through
find-or-createbinds one identifier in your external system — whether it came alone or inside a bigger body. A bid with one design set and five lines is seven identifiers; the customer is one more. - A second
find-or-createwith the same identifier changes nothing. It answers200with the existing resource and ignores thenameandpropertiesyou sent. To change a resource,PATCHit. A body withchildrenreplays the same way, resource by resource; a field that already points somewhere else is refused, not changed. - A
PATCHmerges every key you send, declared or not. A property the type does not declare is stored as sent, without a check. Send only the keys the type declares (itspropertySchema, Quickstart). - Saving a new bid is one request; the rest of its life is step by step.
find-or-createwithfieldsandchildrenwrites the bid, its design set, every line and their fields at once (above; up to 250 resources per request). Saved step by step instead, each line is about 8 requests — create, put it in its set, set its prices, and its product field. Either way the bid's later moves (freezes, Submit, accept, convert) are one request each, and each line's freeze is one request per line. - The first sync is about 66 requests. Reading the price book, the price levels, the payment terms, the design
choices, the customers and the reference lists at
limit=100takes about 66 requests — about 5% of a 1,200-per-minute window, or a fifth of the default 300. - The rate limit is 300 requests per minute per key by default, counted in a fixed one-minute window that starts
with your first request. ReSync can set a different limit for your key — ask your administrator; your key's own
limit is in every response's
x-ratelimit-limit. On429, waitretry-afterseconds and continue. - Member rows carry no status. Reading a tree's statuses costs one
GETper child. - A freeze or a Submit records a snapshot (
resource.captured). Your key sees the snapshot's own property values; the settings and the product it photographed are kept on the record but are not passed to a key. - Who approves. An attestation or status move is attributed to your key's own service identity unless you name a
person:
actorResourceIdmay name any person of your organization; someone outside it is403 actor_not_readable. - Identifiers are unique per external system across every organization, not only yours. Prefix them with something that is yours.
- Freezing is one-way, and a sent bid is final. Nothing leaves Frozen (
409 Invalid status transition). A revision is a new bid; linking it to the one it replaces is not yet available to keys (interaction-400-ports-required.json). - The order is created empty. Its lines are not copied from the bid; read them from the bid.
- A modification sub-line does not point at its price-book modification. It carries its own name and price; keep the link in your own system.
- Compatibility is answered per field. Legal-targets gives each candidate's verdict and reasons; a key has no bulk read of the compatibility rules.
Where to go next
- endpoints.md — every door, with parameters and errors.
- errors.md — every error code and what to do about it.
- events.md — what each event in a resource's history means.
- conventions.md — the rules that apply everywhere, and what you cannot do yet.