AI-Agent Guide
One page a coding agent needs to build a correct Shop Commander integration — paste it into your agent's context.
Paste this page into a coding agent, together with the shop's API key held in an environment variable, and it has what it needs.
Spec: https://shopcommander.com/developers/openapi.json (OpenAPI 3.1, exact and versioned).
Base URL: https://api.shopcommander.com/v1.
Auth: header Authorization: Bearer $SC_API_KEY (keys start sc_live_). No cookies, no query-string keys.
First call: GET /v1/shop → timezone, currency, capabilities (feature flags). Adapt to them.
Rules
- Ids are typed strings (
cus_…,veh_…,appt_…,ro_…,job_…,inv_…,pay_…,item_…,task_…). Never construct or parse them. - Money is a decimal string with a
currency. Use decimal arithmetic.nullon a money field means "not visible to your scopes", not zero. - Absolute request instants require RFC3339
Zor an explicit offset; naive datetimes are rejected. Appointments are wall-clockdate+ exactHH:MMin the shop's IANAtimezone; DST gaps are rejected and folds requireutc_offset. Date-only/business-date values areYYYY-MM-DD. - Lists:
{object:"list", data, has_more, next_cursor}; page withcursor=,limit≤ 200. Sync withupdated_after+sort=updated_at, overlapping by a minute and de-duplicating onid. - PATCH presence matters. Omitted means unchanged; explicit
nullclears only documented nullable fields;false,0,[], and""are values. Unknown fields are rejected. - Retries: use a fresh
Idempotency-Keyfor every command. It is required for consequential creates/actions, replayable for seven days, and reserved for 30. Same key + different operation/target/body →409. - Concurrency: read
version, sendexpected_versionon updates;409 version_conflict→ re-read and merge. - Errors: always
{"error": {type, code, message, request_id, param?, details?, blockers?}}. Branch oncode; body validation is422 validation_failed. A409withblockers[]is a business rule — showblockers[].message/resolutionto a person; do not retry. Checkavailable_actionson appointments and jobs before acting. - Rate limits: 300/min sustained, 60/10 s burst, 60 writes/min per integration; obey
Retry-Afteron429. - Webhooks: at-least-once, unordered, thin payloads. Verify
ShopCommander-Signature(t=…,v1=HMAC-SHA256(secret, "{t}.{raw_body}"), reject if|now−t|>300 s, accept if anyv1matches), de-duplicate on eventid, thenGETthe object. Respond2xxwithin 10 s. - Out of scope by design: customer authorization of estimates, customer messaging, financial writes (posting invoices / recording payments), employee/HR data. Do not try to emulate them.
- Tenancy: one key = one shop. A record from another shop is a
404.
Resources (v1)
shop · customers (+ /archive) · vehicles · appointments (+ /availability, /cancel, /check-in) · repair-orders (+ paginated /jobs) · jobs (+ paginated /parts and /labor, /start, /complete, /unable-to-complete) · technicians · canned-jobs · inventory-items · invoices · payments · tasks (+ /complete) · external-id (PUT/DELETE on customers/vehicles/repair-orders/tasks) · webhook-endpoints (+ /rotate-secret, /test; secrets are shown only on create/rotate) · events.
Scopes gate each family: shop.read, customers.read|write, vehicles.read|write, appointments.read|write, repair_orders.read|write, jobs.actions, pricing.read, inventory.read, inventory.cost.read, canned_jobs.read, technicians.read, invoices.read, payments.read, tasks.read|write, webhooks.manage, external_refs.write. Write does not imply read; full-resource mutations require both. external_refs.write also requires the target resource's read scope. A missing scope is 403 insufficient_scope with the complete requirement in details.
Minimal client skeleton (Python)
import os, time, uuid, requests
BASE = "https://api.shopcommander.com/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['SC_API_KEY']}"
def call(method, path, *, json=None, params=None, idem=None):
headers = {"Idempotency-Key": idem} if idem else {}
for attempt in range(5):
r = S.request(method, BASE + path, json=json, params=params, headers=headers, timeout=30)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "1"))); continue
if r.status_code >= 500:
time.sleep(2 ** attempt); continue
if r.status_code >= 400:
err = r.json()["error"]
raise RuntimeError(f"{err['code']}: {err['message']} (request {err['request_id']}) blockers={err.get('blockers')}")
return r.json()
raise RuntimeError("gave up")
def each(path, **params):
cursor = None
while True:
page = call("GET", path, params={**params, "limit": 200, **({"cursor": cursor} if cursor else {})})
yield from page["data"]
if not page["has_more"]:
return
cursor = page["next_cursor"]
shop = call("GET", "/shop")
appt = call("POST", "/appointments", idem=str(uuid.uuid4()),
json={"customer_id": "cus_…", "vehicle_id": "veh_…", "date": "2026-09-14", "time": "09:30"})