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