- DATA-MODEL.md: storage engine (stdlib sqlite3 + schema.sql, no ORM/migrations), pragmas, per-test isolation. - API.md: revision coupling, async job model, PUT check order, server-derived ready. - PROMPTS.md: no prompt files ship; the DB is the only prompt source.
10 KiB
App API contract — client ↔ FastAPI
Status: SPEC · 2026-09-18.
Base path /api/v1. Health at /api/health. All JSON unless noted. Timestamps UTC ISO-8601.
The outbound WooCommerce payload is owned by docs/contracts/WOO-API.md.
Requirements impact
| Requirement | Impact on this design |
|---|---|
| APP-5 | slab status values and transitions |
| APP-20 | client ids, UTC timestamps, client_rev/server_rev |
| APP-25 | FastAPI is the only Woo and inference caller |
| APP-30 | /api/v1 base path, /api/health |
| APP-35 | error envelope, full lists, JSON |
| APP-40 | server stores client numbers, never recomputes |
| APP-45 | health body and non-200 condition |
| APP-50 | upstream Woo 4xx mapped into the envelope |
| CAPTURE-15, CAPTURE-165 | offline and U2Net are out of scope |
| CAPTURE-25, CAPTURE-30, CAPTURE-115, CAPTURE-130 | calibration choice and batch photos |
| CAPTURE-70, CAPTURE-80, CAPTURE-85, CAPTURE-90 | length/thickness units and derived numbers |
| CAPTURE-95, CAPTURE-105, CAPTURE-135, CAPTURE-141, CAPTURE-145, CAPTURE-150, CAPTURE-155 | PNG output, upload, preview/controls |
| METADATA-5, METADATA-10, METADATA-15, METADATA-30 | taxonomy cardinality and scoping |
| METADATA-35, METADATA-40, METADATA-45, METADATA-50, METADATA-55 | Call 1 / Call 2 |
| METADATA-60, METADATA-65, METADATA-70, METADATA-75, METADATA-100, METADATA-105, METADATA-110, METADATA-115 | context, constraints, fallback, prompts, failure |
| OPS-55 | no client auth |
| PUBLISH-5, PUBLISH-10, PUBLISH-15, PUBLISH-20 | create sequence, draft rule, duplicate, stale |
| PUBLISH-25, PUBLISH-30, PUBLISH-31, PUBLISH-35, PUBLISH-40, PUBLISH-45, PUBLISH-50, PUBLISH-55, PUBLISH-60 | create outcomes, images, payload |
| REVIEW-5, REVIEW-10, REVIEW-15, REVIEW-20, REVIEW-25, REVIEW-30, REVIEW-40, REVIEW-45, REVIEW-50 | ready set, fields, limits |
| SETTINGS-5, SETTINGS-10, SETTINGS-15, SETTINGS-20, SETTINGS-25, SETTINGS-30 | env URL, credentials, crypto |
| SETTINGS-35, SETTINGS-40, SETTINGS-45, SETTINGS-50, SETTINGS-51, SETTINGS-55, SETTINGS-60, SETTINGS-65, SETTINGS-75, SETTINGS-80, SETTINGS-85, SETTINGS-90 | rates, tests, taxonomy, flags |
1. Conventions
- Auth: no client auth; the app relies on the trusted LAN/Tailscale network plus HTTPS.
- Envelope: every error is
{ "error": { "code": string, "message": string, "detail": object|null } }. - Lists: returned in full, no pagination.
- Revisions: create/update carry
client_rev; the server echoesserver_rev; a mismatch is409 revision_conflict. A background inference write bumpsserver_rev; the client adopts the polledserver_revbefore its next write. - Async work: inference and publish run through one in-process job queue with a single consumer; a status transition is compare-and-set inside one write transaction, so a poll never observes a half-applied transition. Tests run the queue inline so contract tests are deterministic.
- IDs and time: slab ids are client-generated UUIDv4; timestamps are UTC ISO-8601.
- Photos:
multipart/form-datawithdata(JSON),files[](original images), andmeta[]({ "seq": n }). One photo is the calibration photo, chosen by the owner. - Secrets: never returned; only a configured boolean.
- Error display: raw status codes, stack traces, and internal codes never reach the owner.
2. Error codes
| Status | Code | Meaning |
|---|---|---|
| 400 | validation |
payload failed validation |
| 404 | not_found |
unknown slab, photo, or setting |
| 409 | revision_conflict |
client_rev/server_rev mismatch; detail.current_server_rev |
| 409 | duplicate_sku |
SKU exists in Woo under any status |
| 422 | unpublishable |
slab not publishable or missing a required field |
| 422 | stale_field |
a picked taxonomy id vanished; returns current options |
| 502 | woo_error |
WooCommerce rejected the request; detail carries the Woo body |
| 500 | internal |
unexpected |
There is no 412 validation_module_stale path.
3. Health and meta
GET /api/health→{ "status": "ok", "version": string, "woo_reachable": bool }. Non-200 when the database or image volume is not writable.GET /api/v1/meta→{ "version": string }.
4. Slabs
POST /api/v1/slabs
Multipart create of a calibrated draft. Returns 201 with the slab at status: "calibrated".
Stores the client-sent derived numbers and never recomputes them. Auto-runs Call 1 only when
inference_enabled is true; inferred values persist only at confidence ≥ 0.7. 409 duplicate_sku
if the SKU already exists.
SlabCreate fields:
| Field | Type | Notes |
|---|---|---|
id |
uuid | client-generated |
sku |
string | 3–24, alphanumeric and hyphen |
length_in, thickness_in |
number | 1/8 inch steps |
sqft, bdft, width_min_in, width_max_in, width_avg_in |
number | client-computed |
species_id |
int | one species leaf |
wood_category_ids |
int[] | one or more |
edge_type_term_id |
int | one |
figure_term_ids |
int[] | one or more |
grade_term_id |
int | exactly one |
thickness_term_id |
int | round-up band |
moisture_term_id |
int | default kiln-dried |
fig_tag_ids |
int[] | optional |
feat_tag_ids |
int[] | optional |
price |
number | 2 decimals |
price_source |
string | recommendation | override |
title, short_description, description |
string|null | prose |
content_source |
string | manual | llm | template |
client_rev |
int | optimistic concurrency |
Grade, edge, thickness, and moisture are singular; figure and the tag lists are arrays.
GET /api/v1/slabs
Returns [Slab], most recent first.
GET /api/v1/slabs/{id}
Returns the Slab. Also the poll endpoint for inference and publish. The client backs off
(1s, 2s, 4s, cap 10s). On publish success the terminal response carries woo_product_id; the
working row is then discarded, so a later GET may return 404, which the client treats as
completion.
PUT /api/v1/slabs/{id}
Partial update of editable fields plus client_rev. Stores client-sent derived fields and does
not recompute them. Checks run in order: 409 revision_conflict, then 409 duplicate_sku (SKU
field actions edit_sku / open_existing), then 422 stale_field (current options included).
status becomes ready only when the full required set is present; it is server-derived and a
client never sets it.
DELETE /api/v1/slabs/{id}
Soft delete of unpublished drafts only (sets deleted_at); 204. Deleted rows are excluded from
all GET responses. Published slabs cannot be deleted from the app.
5. Photos
GET /api/v1/slabs/{id}/photos/{pid}/original→image/*while retained.GET /api/v1/slabs/{id}/photos/{pid}/processed→image/*(transparent PNG) while retained.- After publish both return
410 Gone. POST /api/v1/slabs/{id}/u2net/{pid}is reserved post-POC and is not exposed.
6. Inference
POST /api/v1/slabs/{id}/infer-taxon
Runs or retries Call 1. Sends the clean processed photos at 1024 px long edge or less, the synced
taxonomy, and the user metadata. Returns 202; the client polls. A failure or timeout follows the
runtime failure rule (recoverable offers Retry, unrecoverable explains).
POST /api/v1/slabs/{id}/infer-content
Runs Call 2 on user action only. Fresh request carrying the full Call 1 context; no provider
thread id. Requires test-inference ok. Returns 202; the client polls. The model writes prose
only and never touches dimensions.
Call 1 result fields: species_id, species_confidence, wood_category_ids,
edge_type_term_id, figure_term_ids, figure_confidence, grade_term_id, grade_confidence,
character, inclusions, voids, checks. Free-text observations map to synced feat-* tags
only.
Call 2 result fields: title, short_description, description.
7. Publish
POST /api/v1/slabs/{id}/publish
Preconditions are the full ready set, else 422 unpublishable with field keys. Sequence: validate
the ready set; look up the SKU and stop if present; refresh taxonomy when the last sync is over
one hour old; resolve categories, tags, and attributes; upload the processed PNGs to Woo media;
POST /products per docs/contracts/WOO-API.md; finalize. SLAB-UAT-* always creates a draft.
Returns 202; the client polls.
- Success:
woo_product_idset and returned on the terminal poll, photos purged, the local working slab row discarded, device draft cleared. Nothing is deleted from WooCommerce. - Failure: attempt marked failed, photos retained,
sync_logwritten, plain message on the affected field or action, retry from the same screen. - Partial failure (media uploaded, product POST failed): orphaned media left, logged, manual cleanup note surfaced.
8. Taxonomy
GET /api/v1/admin/taxonomy→ grouped categories, attributes with terms, and tags, plustaxonomy_anchorandtaxonomy_last_sync_at. Only fig-* and feat-* tag families are kept. These lists are the only legal picker options.POST /api/v1/admin/taxonomy/sync→ pulls from Woo, rebuilds the cache in one transaction, setstaxonomy_last_sync_at, bumpstaxonomy_anchoronly when the stored-subset hash changes, and on failure returns502while keeping the previous cache.
9. Pricing
GET /api/v1/admin/pricing→[PriceRule]POST /api/v1/admin/pricing→201PUT /api/v1/admin/pricing/{id}→200DELETE /api/v1/admin/pricing/{id}→204
One flat price_per_bdft per species leaf, no tiers.
10. Settings
GET /api/v1/settings→SettingsViewwith secrets masked.PUT /api/v1/settings— partial update.woo_base_urlis not writable; the server rejects the write. Woo credentials are write-only and stored AES-GCM. Inference base URL and key are encrypted; flags, brand voice, and geo context are settings. Secrets are never echoed.POST /api/v1/settings/test-woo→{ "ok": bool, "detail": string, "taxonomy_anchor": string, "taxonomy_last_sync_at": string }. Fails closed on missing credentials or non-HTTPS; an auth rejection is distinct from an unreachable store; on success always resyncs.POST /api/v1/settings/test-inference→{ "ok": bool, "mode": "stateful"|"stateless"|"fail", "vision_capable": bool, "detail": string }.failblocks Call 2 only.
11. Removed from v1
- The shared validation module and its
412path. - Provider thread ids and
previous_response_id(Call 2 is a fresh full-context call). - The U2Net control; the endpoint is reserved only.
- Offline/IndexedDB sync.