Quickstart
The five calls every integration makes first, whatever it goes on to build: who your key is, which types it may touch, the vocabulary it may use, how to read a list, and how to record that something happened. Every response below is a real capture from a running server — nothing here is illustrative. The resources in the captures belong to a test organization; your ids, names and price levels will differ.
| # | Door | Call |
|---|---|---|
| 1 | Self — who am I, and what may I touch? | GET /v1/api-keys/self |
| 2 | Types — map type codes to ids, read what a type declares | GET /v1/resource-types, GET /v1/resource-types/:id |
| 3 | Vocabulary — the kinds of connection and the events you may record | GET /v1/interaction-types, GET /v1/attestation-event-types |
| 4 | Read — a list, with each row's values | GET /v1/resources?typeKey=…&include=properties |
| 5 | Attest — record that something happened | POST /v1/resources/:id/attestations |
Then A bid, end to end puts them to work: one whole bid, from an empty start to a converted order. examples.http has one runnable block per door. The latest copy of these pages lives at docs.limnal.dev.
Set up
export BASE=https://api.limnal.dev
export TOKEN=... # the key your administrator gave you, shown once at creation
Every request carries the key as a bearer token, and nothing else:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/..."
In code, one small wrapper is enough. This one never throws on an HTTP status — every answer, including a refusal,
comes back as { status, json, header }, so a step can decide what a 409 means for it — and it keeps the response
headers, because the rate-limit headers are worth watching (see Things to know):
const BASE = process.env.RESYNC_BASE! // https://api.limnal.dev
const TOKEN = process.env.RESYNC_TOKEN! // from your secret store — never a command-line argument, never logged
type Answer = { status: number; json: any; text: string; header: (name: string) => string | null }
async function api(method: string, path: string, body?: unknown): Promise<Answer> {
let res: Response
try {
res = await fetch(`${BASE}${path}`, {
method,
headers: { authorization: `Bearer ${TOKEN}`, ...(body != null ? { 'content-type': 'application/json' } : {}) },
body: body != null ? JSON.stringify(body) : undefined,
})
} catch (e) {
return { status: 0, json: { error: 'network', message: (e as Error).message }, text: '', header: () => null }
}
const text = await res.text()
let json: any = null
try { json = text ? JSON.parse(text) : null } catch { json = text }
return { status: res.status, json, text, header: (name) => res.headers.get(name) }
}
1. Who am I, and what may I touch?
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/api-keys/self"
From self.json — status 200:
{
"apiKeyId": 783,
"name": "bid-app-docs",
"scopes": {
"read_types": [
"cab_pricing_level",
"cab_payment_terms",
"cab_order",
"cab_catalog_product",
"cab_cat_accessory",
"cab_cat_base",
"cab_cat_molding",
"cab_cat_office",
"cab_cat_utility",
"cab_cat_vanity",
"cab_cat_wall",
"cab_fam_base_sink",
"cab_fam_bookcase",
"cab_fam_crown_molding",
"cab_fam_filler",
"cab_fam_lazy_susan",
"cab_fam_oven_cabinet",
"cab_fam_panel",
"cab_fam_post",
"cab_fam_utility_cabinet",
"cab_fam_vanity_combo",
"cab_fam_wall_cabinet",
"cab_fam_wall_drawer",
"cab_fam_wall_pantry_pullout_filler",
"cab_fam_wall_stacked",
"door_style_lower",
"door_style_upper",
"drawer",
"finish",
"front",
"hinge",
"interior",
"sheen",
"slide",
"species"
],
"operate_types": [
"cab_customer",
"cab_bid",
"cab_design_set",
"cab_line",
"cab_install",
"cab_adjustment",
"cab_modification"
],
"find_or_create_types": [
"cab_customer",
"cab_bid",
"cab_design_set",
"cab_line",
"cab_install",
"cab_adjustment",
"cab_modification"
],
"created_owner_resource_id": 5
},
"actsAt": null,
"owner": {
"resourceId": 5,
"name": "Popular Design Mill & Cabinet, Inc."
}
}
What to notice. owner is the organization your key belongs to — every resource you can reach is owned by it.
operate_types is what you may change; read_types is what you may additionally read; find_or_create_types is what
you may bring into existence. This key may create and change customers, bids, design sets and the four kinds of bid
line, and only read the price book, the price levels, the payment terms and the orders a converted bid produces. A type
code in none of the lists returns 403 everywhere, and that is working as intended.
Make this call at startup and log the result. When something later returns 403, this is the answer.
2. Map type codes to ids
Doors take a type's code (typeKey=cab_bid); a few answers carry its numeric id. GET /v1/resource-types lists
every type your key can read, each with its code and resourceTypeId — read it once and keep the code → id map. The
full list is long (resource-types-list.json in the fixture set); lifecycleId narrows it to the types that move
through one lifecycle — here, the bid's:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/resource-types?lifecycleId=<bidLifecycleId>"
From resource-types-list-by-lifecycle.json — status 200:
[
{
"resourceTypeId": "176",
"code": "cab_bid",
"name": "Bid",
"description": null,
"propertySchema": {
"tax_rate_bp": {
"type": "number",
"label": "Tax rate (basis points)",
"derivable": false
}
},
"parameterDefinitions": {},
"labelTemplate": "{name}",
"isSystem": false,
"canBeAllocated": false,
"isConsumable": false,
"parentResourceTypeId": "170",
"parentName": "Shop Document",
"ownerResourceId": 5,
"lifecycleId": "108",
"orgId": "5",
"orgName": "Popular Design Mill & Cabinet, Inc."
}
]
propertySchema is what the type declares about its values (derivable: false marks money ReSync never computes);
lifecycleId is the lifecycle its statuses come from. One type in full also says what it allows as a member — here, a
bid line can sit inside at most one design set:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/resource-types/<lineTypeId>"
From resource-type-by-id.json — status 200:
{
"resourceTypeId": "3425",
"code": "cab_line",
"name": "Bid Line",
"description": null,
"propertySchema": {
"qty": {
"type": "number",
"label": "Quantity"
},
"area": {
"type": "text",
"label": "Area"
},
"taxable": {
"type": "select",
"label": "Taxable",
"options": [
"yes",
"no"
]
},
"ext_cents": {
"type": "number",
"label": "Extended price (cents)",
"derivable": false
},
"shop_note": {
"type": "text",
"label": "Shop note"
},
"unit_cents": {
"type": "number",
"label": "Unit price (cents)",
"derivable": false
},
"customer_note": {
"type": "text",
"label": "Customer note"
},
"finish_interior": {
"type": "select",
"label": "Finished interior",
"options": [
"yes",
"no"
]
},
"list_overridden": {
"type": "select",
"label": "List overridden",
"options": [
"yes",
"no"
]
},
"override_list_cents": {
"type": "number",
"label": "Override list price (cents)",
"derivable": false
}
},
"nameTemplate": null,
"parameterDefinitions": {},
"expressionContext": {},
"labelTemplate": "{name}",
"isSystem": false,
"canBeAllocated": false,
"isConsumable": false,
"defaultIsSerialized": false,
"defaultIsLotTracked": false,
"parentResourceTypeId": "170",
"defaultUnitTypeId": null,
"ownerResourceId": "5",
"lifecycleId": "656",
"orgName": "Popular Design Mill & Cabinet, Inc.",
"ownerOrgId": "1",
"canvasId": null,
"containmentCaps": [
{
"containmentCapId": 1,
"parentTypeId": 1171,
"maxParents": 1
}
]
}
3. Read the vocabulary
Two lists name the words the rest of the API speaks. Interaction types are the kinds of connection between two
resources — a bid's customer is a bid_for connection, its price level priced_at:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/interaction-types"
From interaction-types-read.json — status 200:
{
"interactionTypes": [
{
"interactionTypeId": 1,
"name": "allocates",
"affects": "availability",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": true,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 6,
"name": "assigned_to",
"affects": "availability",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": true,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 13,
"name": "bid_for",
"affects": "none",
"isBidirectional": false,
"isSystem": false,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 7,
"name": "certified_in",
"affects": "none",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 9,
"name": "compatible_with",
"affects": "none",
"isBidirectional": true,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": false
},
{
"interactionTypeId": 2,
"name": "consumes",
"affects": "inventory",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": true,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 5,
"name": "depends_on",
"affects": "scheduling",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": true,
"connectable": true
},
{
"interactionTypeId": 11,
"name": "derives_from",
"affects": "none",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 12,
"name": "feeds",
"affects": "none",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 10,
"name": "incompatible_with",
"affects": "none",
"isBidirectional": true,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": false
},
{
"interactionTypeId": 8,
"name": "licensed_for",
"affects": "none",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 15,
"name": "pays_on",
"affects": "none",
"isBidirectional": false,
"isSystem": false,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 14,
"name": "priced_at",
"affects": "none",
"isBidirectional": false,
"isSystem": false,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 4,
"name": "produces",
"affects": "inventory",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": true,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 3,
"name": "reserves",
"affects": "availability",
"isBidirectional": false,
"isSystem": true,
"affectsInventory": false,
"affectsAvailability": true,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 17,
"name": "sells",
"affects": "none",
"isBidirectional": false,
"isSystem": false,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
},
{
"interactionTypeId": 16,
"name": "uses",
"affects": "none",
"isBidirectional": false,
"isSystem": false,
"affectsInventory": false,
"affectsAvailability": false,
"affectsScheduling": false,
"connectable": true
}
]
}
What to notice. connectable: false marks a kind you cannot draw yourself — compatible_with and
incompatible_with are compatibility markings, kept by your administrator. A field already knows its own kind (its
defaultInteractionTypeId, see the walkthrough); this list is for naming what you read back.
Attestation event types are the things you may record as having happened — a bid was sent, accepted, converted — each with the payload fields it declares:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/attestation-event-types"
The full answer is attestation-event-types-read.json in the fixture set — one row per event type your key can see.
Read it rather than caching it forever: a declaration can change, and the changelog says when.
4. Read a list
A list always needs a type. Add include=properties to get each row's values in the same request:
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/resources?typeKey=cab_pricing_level&limit=5&include=properties"
From list-include-properties.json — status 200:
{
"resources": [
{
"resourceId": "13806",
"name": "Dealer",
"identifier": null,
"ownerResourceId": 5,
"resourceTypeId": "1264",
"resourceTypeName": "Price Level",
"isSystem": false,
"createdAt": "2026-09-13T08:53:20.358Z",
"statusId": null,
"statusName": null,
"statusColor": null,
"archivedAt": null,
"icon": "box",
"properties": {
"name": "Dealer",
"pct_bp": 4536
},
"confirmedKeys": {}
},
{
"resourceId": "13807",
"name": "High-Volume Contractor",
"identifier": null,
"ownerResourceId": 5,
"resourceTypeId": "1264",
"resourceTypeName": "Price Level",
"isSystem": false,
"createdAt": "2026-09-13T08:53:22.255Z",
"statusId": null,
"statusName": null,
"statusColor": null,
"archivedAt": null,
"icon": "box",
"properties": {
"name": "High-Volume Contractor",
"pct_bp": 5025
},
"confirmedKeys": {}
},
{
"resourceId": "13808",
"name": "Contractor",
"identifier": null,
"ownerResourceId": 5,
"resourceTypeId": "1264",
"resourceTypeName": "Price Level",
"isSystem": false,
"createdAt": "2026-09-13T08:53:24.139Z",
"statusId": null,
"statusName": null,
"statusColor": null,
"archivedAt": null,
"icon": "box",
"properties": {
"name": "Contractor",
"pct_bp": 5678
},
"confirmedKeys": {}
},
{
"resourceId": "13809",
"name": "Homeowner",
"identifier": null,
"ownerResourceId": 5,
"resourceTypeId": "1264",
"resourceTypeName": "Price Level",
"isSystem": false,
"createdAt": "2026-09-13T08:53:26.009Z",
"statusId": null,
"statusName": null,
"statusColor": null,
"archivedAt": null,
"icon": "box",
"properties": {
"name": "Homeowner",
"pct_bp": 6937
},
"confirmedKeys": {}
},
{
"resourceId": "13810",
"name": "Internal",
"identifier": null,
"ownerResourceId": 5,
"resourceTypeId": "1264",
"resourceTypeName": "Price Level",
"isSystem": false,
"createdAt": "2026-09-13T08:53:27.881Z",
"statusId": null,
"statusName": null,
"statusColor": null,
"archivedAt": null,
"icon": "box",
"properties": {
"name": "Internal",
"pct_bp": 0
},
"confirmedKeys": {}
}
],
"nextCursor": "13810"
}
What to notice. resourceId is a string. nextCursor is an opaque token: pass it back as &cursor=… for the
next page, and stop when it comes back null. Payment terms are the same call with typeKey=cab_payment_terms. ReSync
stores these numbers; it never multiplies them — computing a price is your application's job.
5. Record what happened
An attestation records that something happened — here, that a bid was sent, with the totals your application computed — without changing the resource's own fields:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"eventType":"bid.sent","payload":{"subtotal_cents":155027,"discount_cents":0,"delivery_cents":0,"tax_cents":12790,"install_cents":50000,"total_cents":167817,"tax_rate_bp":825}}' \
"$BASE/v1/resources/$BID/attestations"
From bid-attest-sent.json — status 201:
{
"eventUlid": "01M5FWMHJY4Q8TB2KSHCS8Z9S9",
"eventType": "bid.sent",
"resourceId": 24911,
"actorResourceId": 24907
}
What to notice. The answer names the event (eventUlid) and who it is attributed to: your key's own service
identity, unless you name a person with actorResourceId. An event type that is not in your vocabulary, a payload that
misses a declared field, or a resource whose status does not allow the event is refused — see
errors.md. Some events are recorded once only; sending one again is 409.
Next: A bid, end to end
A bid, end to end — one customer, one bid, a design set with five lines, from Draft through Frozen, Sent, Accepted and Converted to the order ReSync creates. Every step quotes its capture.
Where to go next
- walkthrough-bid.md — a whole bid, door by door, and the things to know before you build one.
- auth-and-scopes.md — scopes, rate limits, request ids.
- endpoints.md — every door, with parameters and errors.
- errors.md — every error code and what to do about it.
- conventions.md — the rules that apply everywhere, and what you cannot do yet.