scroll # Portal API Reference (Day 382) Working noteson the `/api/things` surface, grounded in whatwas actually exercised during tonight's ColorChain migration — not a full endpointcatalog. File:line refs are`Portal/rust/src/server/at_routes.rs` unlessnoted. ## Core Things endpoints | Endpoint |Handler | Notes | |---|---|---| | `GET/api/things?sapphire=X&ring=Y&being=Z&limit=N&cursor=...`| `api_things_list` (3519) | Per[[project_things_api_filtering]] — only`sapphire`/`ring`/`being`/`limit`/`cursor`actually filter; `onyx`/`ruby.*` query paramsare silently ignored, filter thoseclient-side. | | `GET /api/things/:id` |`api_things_get` (3535) | Full Thing incl.`gem_values`. This is what `xo read <uuid>`resolves to under the hood. | | `GET/api/things/by-name/:name` |`api_things_by_name` (3567) | Lookup by`onyx`. | | `GET /api/things/by-type/:type` |`api_things_by_type` (3622) | Lookup by`sapphire`. | | `GET /api/things/by-gem` |`api_things_by_gem` (4220) | Generic gem-valuequery — what `△ gem` and `△ thing` MCPtools likely call through. | | `PATCH/api/things/:id/gems` |`api_things_patch_gems` (4290) | **The editpath.** See below — this is the one thatactually moved tonight's migration. | | `POST/api/things/ingest` | `api_things_ingest`(4370) | Always INSERTs a new, unrelated Thing— no upsert, no dedup (per[[project_thing_box_thing|Thing Box]] notes— caused a real duplicate-asset incidentpreviously). Don't use this to edit; use thePATCH path. | | `GET/api/things/:id/snapshots` |`api_things_snapshots` (4343) | Versionhistory — every `PATCH .../gems` callauto-snapshots the prior `gem_values` first(4302-4310), so edits carry real lineage. | ##`PATCH /api/things/:id/gems` — themechanism, and its real gotcha ```json {"gem_values": { "ruby": {...}, "topaz": {...}}, "merge": true } ``` - **`merge: true`**(default) runs `gem_values = gem_values || $1`(4314-4319) — **Postgres JSONB `||` is ashallow, top-level merge.** Any gem-slot keyyou include in the payload (`ruby`, `topaz`,`diamond`, etc.) **replaces that slot's entirevalue**, not just the sub-fields you mention.Tonight's migration learned this the hard way:patching `diamond: {swatches: [...]}` withoutfirst including the existing`crown`/`ring`/`summoner`/`amulet` keyssilently wiped them on 24 Color Things — hadto re-fetch current state and re-patch witheverything included. **Always fetch thecurrent `gem_values` for a slot beforepatching it, and send the full slot back withyour changes merged in, not just the newkeys.** - **`merge: false`** replaces`gem_values` wholesale (4320-4324) — evenmore destructive, full overwrite. -Auto-snapshot happens regardless (4302-4310)via `db::snapshot_thing`, so a botched patchis always recoverable from history — butrecovery isn't automatic, you still have tonotice and re-fetch/re-apply. ## Auth —softer than expected `api_things_patch_gems`has **no Authorization/Bearer check in thehandler itself** (confirmed by reading4290-4336 — no token validation, no rolecheck). The 403s hit during tonight'smigration were **Cloudflare's bot-management(error code 1010)**, not application auth —triggered by Python's default `urllib`User-Agent. Switching the write loop from`python urllib` to plain `curl` resolved itwith **zero auth header needed**; adding`Authorization: Bearer <token>` (via `△my_token`) made no observable difference oncethe UA issue was fixed. This is worth treatingas a real finding, not just a workaround: thelive PATCH-gems route currently has **noapp-level write authorization** — anyone whocan get past Cloudflare's bot check can mutateany Thing's `gem_values`. Cross-reference[[project_fid_priority_knob]]'s siblingsegment `token-hardening` (FID, already on thespiral, currently 5/5 gaps open) — thisfinding belongs there, not just in this note.## Palette-specific endpoints (tonight'sactual subject) | Endpoint | Handler | Notes ||---|---|---| | `GET /api/palette` |`api_palette_active` (7100) | Active palettename from `config::get_str("active-palette")`,defaults to `hexcraft-dark`, delegates to`api_palette_by_name`. | | `GET/api/palette/:name` | `api_palette_by_name`(7109) | Resolves Color→Swatch chain. **Bug,not fixed tonight:** the swatch query(7146-7148) has no palette-membership filter— it returns every `sapphire=swatch` Thingregardless of `:name`. Two Palette Thingsexist (`hexcraft-dark`, `timepiece-palette`)but only the metadata block differs per name;the swatch set is identical either way. Thisis gap `ccr-004` on the `color-chain-rebirth`segment. | | | | **Fixed tonight:** theresolver was reading `hex`/`rgb` from `ruby`(`Gem::Mark`) (7129-7137), which broke whenthe Color migration moved those fields to`topaz` (`Gem::Data`) per the locked schema.Patched to read `topaz` instead — rebuilt,restarted `portal-hexlet` via pm2, verifiedlive. | ## Source pointers -`Portal/rust/src/server/at_routes.rs:3519-4520`(core Things CRUD), `:4290-4336`(`api_things_patch_gems`), `:7099-7193`(palette). -`Portal/rust/src/things/theme.rs:18-86` —the TUI-side equivalent resolver, same`ruby`→`topaz` fix applied there too (Day382, alongside the API fix, in[[Color-Chain-Rebirth.00382]]).