# API Report Card: FixGrid API v1 — FINAL (max-effort, three independent runs, resolved)

## Compiler's resolution (supersedes any single-run statement in the grading body below)
- Methodology version: 1.1
- Evaluating models: three independent graders on one frozen packet, at the highest effort each session offers — run A Claude Fable 5.1, run B Claude Opus 5.5, run D Claude Opus 5.5 (all session-inherited `max`; the session's effort was read as `max` before launch). Compiler: Claude Fable 5.1 (this session), which did not grade. A Grok run at `xhigh` exists for the operator's own use and is excluded from this result.
- Date run: 2026-09-28 (evidence access 14:37–14:56 UTC; grading 15:31–16:05 UTC)
- Evidence packet: fixgrid-2026-09-28 v2 FINAL (`../evidence/`; manifests, hashes, 89 live captures, one signed webhook delivery). Evidence-discovery mode for the runs: supplied final packet.
- Evidence tier: fully verified — controlled live (battery steps 1–8 all run; the notice and move-in writes deliberately not exercised and documentation-graded inside C1.1–C1.3, disclosed).
- Resolved marks: C1.1 partial · C1.2 no · C1.3 no · C1.4 partial · C2.1 yes · C2.2 yes · C2.3 yes · C2.4 yes · C2.5 yes · C2.6 yes · C2.7 partial · C2.8 yes · C2.9 no · C2.10 yes · C2.11 partial · C2.12 partial · C3.1 yes · C3.2 partial · C3.3 yes · C3.4 yes · C3.5 N-A · C4.1 partial · C4.2 yes · C4.3 partial · C4.4 yes · C5.1 yes · C5.3 yes.
- Category points: 3.75 / 15 · 7.9167 / 10 · 4.375 / 5 · 3.75 / 5 · 15.0 / 15.
- Raw 34.7917 / 50 · Normalized 69.58 / 100 · **Published numeric score 70 / 100 · Letter grade C-**.
- Overall verification coverage: 100% (26 of 26 applicable checks; C3.5 N-A). Gate satisfied; minimum battery complete.
- Agreement: 23 of 26 applicable checks unanimous across the three runs. Three checks split 2-vs-1 and were resolved against the evidence (full reasoning in `RUN_COMPARISON_MAX_2026-09-28.md`): C1.1 partial (work orders 0.5 for the missing field edits, scheduling and residents 0.5 as present-but-read-only → 52.4%); C1.3 no (reassignment excluded as a C1.2 write, the resident move-in/move-out row 0.5 because the move-out confirmation and revocation are not API actions → 44.4%); C2.2 yes (the three card-only webhook payloads are a separately documented, event-discriminated shape, not a field whose type varies within one schema).
- Residual disagreement and score effect (each dissenting reading alone): C1.1 → no gives 66 (D); C1.3 → partial gives 73 (C); C2.2 → partial gives 69 (D+). The published 70 sits 0.08 above the rounding line; C- is the resolved result, with D+ and C both defensible readings that the linked run reports let a reader audit.
- Grading body: the full report of run B (Claude Opus 5.5, max) follows verbatim. Its check marks equal the resolved marks. One item inside C1.1 differs from the resolution (B scores work orders 1.0 → 59.5%; the resolution scores 0.5 → 52.4%; the mark is partial either way), and its "Unresolved evaluator disagreements" line describes that single run only — this section governs.
- Unscored vendor observations added by the max runs: 8 of 398 live tickets carry `cancellation_reason_code: "duplicate"`, outside the documented seven-value vocabulary (captures 007–008); `OccupancyRecord.status` is lower-case while `Turn.status` is Title-Case for the same record; plus the earlier round's notes (API-created webhook subscriptions absent from the in-app panel; `completed_at` surviving a reopen; the undocumented `fixgrid-delivery-id` / `fixgrid-event-id` headers; one live 422 sentence differing from the documented one).

---

# API Report Card: FixGrid — FixGrid API v1

## Run metadata
- Methodology version: 1.1
- Evaluating model: Claude Opus 5.5 (1M context), model ID `claude-opus-5-5[1m]`, run as a Claude Code subagent
- Reasoning effort: session-inherited (operator set the session to max)
- Date run: 2026-09-28
- Provisional evidence-packet version or ID: fixgrid-2026-09-28 packet v1
- Final evidence-packet version or ID: fixgrid-2026-09-28 packet v2 FINAL
- Evidence-discovery mode: supplied final packet (no discovery repeated, no source added or removed, no URL fetched, no API called; the keys are revoked)
- Evidence tier: fully verified — controlled live
- Live-write method and safety: controlled live. Fixtures: the operator-designated unit 5834 "Unit 101" (Plaza Commons) in the operator's demonstration company FixGrid Plaza; one grader-created fixture ticket 3568 titled "APITEST-DELETE grader idempotency probe 5467073c"; webhook subscription 11 pointed at the operator's own local receiver through a Cloudflare quick tunnel. Operator authorization recorded (packet_manifest_v2_FINAL.md, "Live-test battery": [operator name withheld], in chat 2026-09-28, including permission to trip the rate limit and approval of the tunnel relay). Least privilege by scope: PM-Read (read) for reads, PM Write (write) for ticket writes, PM Hook (webhook_only) for subscription management (001–003). Cleanup verified in captures: 083 fixture ticket Cancelled / duplicate_ticket; 084 subscription deleted (204); 085 subscription list empty; 087 unit record identical field-for-field to the pre-test read 058; 088 the unit's open-ticket set identical to the pre-test set in 059 (ids 3456, 3460–3463, 3505, 3531); 089 re-delete returns 404. Disclosures: the API has no ticket delete (045 → 405), so the fixture ticket persists as a Cancelled record; one demo-ticket payload transited Cloudflare's quick-tunnel relay (operator-approved; the endpoint itself was the operator's receiver); the pre-execution dry-run plan is not itself preserved in the packet; key revocation is recorded as requested of the operator, not captured.
- Minimum live-test battery: complete — steps 1–8 all ran (captures listed below). No step is N-A. Two occupancy writes (POST /units/{unit_id}/notice and POST /units/{unit_id}/move-in) were deliberately not exercised because they create tenancy records that the API cannot reverse (an irreversible state change on live data under the protocol's hard exclusions); they are documentation-graded items inside C1.3 (and inside the residents/units write items of C1.2).
- Live tests performed (all 2026-09-28, 14:44:33Z–14:55:08Z):
  - Step 1 authenticate: 001–003 (three operator-minted keys; `/whoami` 200 echoing company "FixGrid Plaza" and each key's scope); 004–006 (401 `unauthorized` + `WWW-Authenticate: Bearer` for no key, a bad key, and a cookie-only request).
  - Step 2 read and page: 007–008 (all 398 tickets in two 200-row keyset pages, ids ascending, no duplicates, `next_cursor` null at the end); 009 (limit=999 clamped, applied limit 200); 010 (default 50); 011–018 (first page of properties, units, assets, vendors, inspections, turns, meters, webhooks).
  - Step 3 filtered / incremental queries: 026–027 (`updated_since` on tickets; every row returned in 026 has `updated_at` ≥ the cutoff and the id set equals the expected first 200 matches; 2027 cutoff → empty page); 028–031 (status, property_id, unit_id, ticket_number — all honored); 032 (assets `updated_since`); 033 (turns status); 034 (meter readings paging).
  - Step 4 deliberate errors: 035–052 (400, 401, 403, 404, 405, 409 and 422 variants; one error shape throughout).
  - Step 5 rate-limit and traceability signals: 050_rate_limit_429 (429 `rate_limited`, `Retry-After: 30`); 055 (a second key unaffected at the same second — per-key budgets); 056 (recovered 32 s later); response headers on all 79 captures (`rndr-id` and `CF-RAY` present; no documented request identifier; no ETag).
  - Step 6 create/update on the fixture: 058–059 (before state); 060 (create 201); 064 (status-in-body refused); 066–073 (In Progress; Completed refused without a note, then accepted with one; reopen; bad cancellation reason 422; extra `assignee_id` key 422; read-back).
  - Step 7 idempotency: 060 + 061 + 062 (same key twice → one ticket; `x-idempotent-replay: 1`; unit ticket count 11 → 12); 063 (same key, different body → 409 `idempotency_mismatch`); 069 (PATCH replay); 077–078 (webhook create replayed, including with the event list reordered).
  - Step 8 webhook: 076 (create 201, signing secret returned once); 079 (list); 080 (DELETE with a read key → 403); 081 (unknown id → 404); 082 (trigger PATCH); live/webhook_deliveries/1790607263438.json (one signed delivery received 14:54:23Z; HMAC verified valid as recorded in packet_manifest_v2_FINAL.md); 083–089 (cleanup and verification); receiver.log, tunnel.log.
  - Operations exercised live: 17 of the 26 documented operations. Not exercised: the six single-record fetches for properties, assets, vendors, inspections, turns and meters; notice; move-in; and `GET /openapi.json` as an API call (its body is in the packet as app_openapi.json).
- Live tests not possible: none. Not performed by design: the occupancy notice and move-in writes (above).
- Documentation-graded checks (baseline verified): none at check level — every write-path check (C1.2, C1.3, C2.4, C2.8) was exercised live. Documentation-graded items inside checks, disclosed: C1.2 residents and units/properties write items and C1.3 residents and units lifecycle items (notice, move-in); C2.4 idempotency on notice and move-in; C2.8 retry/parking ladder (the single delivery succeeded, so no retry was observable); C3.4 key revocation (not a battery step).

## Final evidence packet manifest
First-party documentation retrieved 2026-09-28 (packet/). All 37 packet files were re-hashed in this run and match packet_sha256.txt.
- https://developers.fixgrid.app/ — dev_index.html/.txt ("Start here")
- https://developers.fixgrid.app/authentication.html — dev_authentication.html/.txt
- https://developers.fixgrid.app/objects.html — dev_objects.html/.txt
- https://developers.fixgrid.app/errors.html — dev_errors.html/.txt
- https://developers.fixgrid.app/webhooks.html — dev_webhooks.html/.txt
- https://developers.fixgrid.app/limits.html — dev_limits.html/.txt ("Rate limits and versioning")
- https://developers.fixgrid.app/changelog.html — dev_changelog.html/.txt
- https://developers.fixgrid.app/reference.html — dev_reference.html/.txt
- https://developers.fixgrid.app/openapi.json — dev_openapi.json (OpenAPI 3.1; sha256 cebd90c4…ab50)
- https://app.fixgrid.app/api/v1/openapi.json — app_openapi.json (byte-identical to the above)
- https://developers.fixgrid.app/llms.txt — dev_llms.txt (1,515 bytes)
- https://developers.fixgrid.app/sitemap.xml — dev_sitemap.xml
- https://www.fixgrid.app/ — www_index.html/.txt
- https://www.fixgrid.app/pricing.html — www_pricing.html/.txt
- https://www.fixgrid.app/security.html — www_security.html/.txt
- https://www.fixgrid.app/terms.html — www_terms.html/.txt
- https://www.fixgrid.app/features.html — www_features.html/.txt
- https://www.fixgrid.app/llms.txt — www_llms.txt
- https://www.fixgrid.app/sitemap.xml — www_sitemap.xml
- Negative results preserved: developers.fixgrid.app/llms-full.txt → 404 (dev_llms-full.txt body "Not Found"); /status.html → 404 on both hosts (dev_status.*, www_status.*); status.fixgrid.app does not resolve (recorded in the manifests, not itself an artifact).
- Live artifacts (live/): 79 numbered request/response captures (001–018; 026–053 including two files numbered 050; 055–056; 058–073; 076–089 — numbers 019–025, 054, 057, 074 and 075 are not present), 7 sample_*.json rows, fixture.json, row_counts.json and webhook_deliveries/1790607263438.json — 89 JSON files in all; receiver.log; tunnel.log.
- Packet control files: packet_manifest_v1.md (URL manifest and the fixed coverage classification), packet_manifest_v2_FINAL.md (freeze note, controlled-verification notes, battery record), packet_sha256.txt.
- Packet-integrity notes (no mark affected): (1) packet_manifest_v2_FINAL.md says the reference carries curl examples for "all 22 operations"; the reference and OpenAPI in the packet show 26 operations across 23 paths, each with a curl example. (2) The manifest records a 16 s signature-timestamp skew at receipt; the preserved header `t=1790607262` (14:54:22Z) and the delivery's `received_at` 14:54:23Z (file id 1790607263438 ms) show about 1.4 s. The HMAC-validity observation is taken as recorded, as the brief instructs (the secret was destroyed). (3) row_counts.json records full walks of every resource for a typing scan, but only the pages listed above are preserved; this run's typing verdict rests on the preserved rows.

## Evidence-amendment log
- None in this run (independent grading of a supplied final packet). The packet's own controlled-verification pass (packet_manifest_v2_FINAL.md) added and removed no sources; its post-freeze housekeeping preserved raw copies of two sources already in the v1 manifest (www root → www_index.*, developer sitemap → dev_sitemap.xml) with hashes appended. No mark in this report depends on the housekeeping beyond context citations.

## API eligibility
- Qualifying API: yes
- API operator: FixGrid [developers.fixgrid.app "Start here" header "FixGrid API · v1"; openapi.json `info.title` "FixGrid API", `servers[0].url` https://app.fixgrid.app/api/v1; terms.html preamble — app.fixgrid.app "operated by FixGrid"]
- Access or credential issuer: FixGrid, issued through the customer's own Company Admin, who mints keys inside the FixGrid app on Integrations → Developers [developers.fixgrid.app "How you get one"; security.html "Access control" — "Company-scoped API keys are minted by a Company Admin"]
- Eligibility basis: First-party documentation defines a bearer-key REST API over the customer's FixGrid maintenance record — list and fetch for eight objects, ticket and occupancy writes, signed webhooks and subscription routes [developers.fixgrid.app "Start here" intro and "What is here"; reference.html "Operations: 26 operations across 23 paths"; openapi.json]. Live `/whoami` calls with three operator-minted keys returned the company and each key's scope [001–003].

## Context
- Software category: maintenance or operations tool (the rubric's default Maintenance/operations classification, fixed in packet_manifest_v1.md).
- What the API is for and its core objects and workflows: FixGrid is a multifamily maintenance system of record; its API lets a partner or an operator's own tools read the company's tickets (work orders), properties, units (with occupancy and readiness), assets, vendors (with COI compliance state), inspections, turns (move-out/move-in transition records) and utility meters, create tickets and change their status, feed notices to vacate and move-ins from another system, and receive signed webhooks for ten events. The core maintenance objects are therefore work orders, their status transitions and their assignment, with vendors, scheduling, residents and units/properties as supporting objects.

## Provider and property-management fit
- What this product is: a residential-native maintenance management system (CMMS) for multifamily operators — tickets, make-ready turns, inspections, preventive maintenance, assets, parts and compliance on one record [www.fixgrid.app FAQ "What is FixGrid?"; features.html FAQ "What does FixGrid do?"]
- Bank status, when relevant: N-A — FixGrid is not a bank and the packet documents no account, balance or payment service [terms.html §2 "Description of Service"]
- Who provides any bank account or regulated banking service: N-A [same]
- What the customer actually receives: a monthly SaaS subscription (Starter $49/mo base + $1.25/unit over 50; Professional $99/mo + $2.00/unit; Enterprise custom) with unlimited users, the mobile PWA, the resident portal, and the public API + signed webhooks on every plan [pricing.html plan cards and "On every plan"]
- Property-management fit: PM-specialized — multifamily property maintenance is the product's central purpose and multiple PM workflows are documented [www.fixgrid.app hero "Maintenance software for multifamily"; features.html]
- Documented PM-specific workflows: resident maintenance requests and the ticket lifecycle [features.html "Role-based portals"; objects.html "ticket"]; make-ready and turns with notice-to-vacate / move-in, unit occupancy and the QA-gated "Vacant Ready" state [features.html "Make-Ready & Turns"; objects.html "unit", "turn"]; inspections including NSPIRE readiness [www.fixgrid.app navigation; objects.html "inspection"]; regulated-systems compliance logs [features.html "Compliance & Safety"]; vendor COI compliance and hold [objects.html "vendor"; webhooks.html `vendor.hold`]; occupancy ingest from a PMS feed [objects.html "occupancy"]
- Trust or fiduciary workflow support, when relevant: N-A — the product does not hold, move or account for funds; no trust, client-fund, security-deposit or escrow workflow is documented or relevant [terms.html §2; pricing.html]
- Operational role and dependencies: FixGrid is the maintenance-operations record that runs alongside the operator's PMS, which keeps leasing, residents and accounting; occupancy can be pushed in through the API's notice and move-in routes [features.html FAQ "Is FixGrid a property-management system (PMS)?" — "No … it runs alongside the system you use for leasing and accounting"].

## Coverage classification (fixed before inspection)
Class and weight are copied verbatim from packet_manifest_v1.md (the rubric's default for Maintenance/operations, no deviation). The last column is this run's inspection result.

| Object or workflow | Class | Weight | Present / read-only / absent |
|---|---|---|---|
| Work orders | critical | 3 | Present — list/filter/fetch, create, status change; non-status fields not writable after creation |
| Status transitions | critical | 3 | Present — read and write (any status to any status) |
| Vendor/technician assignment | critical | 3 | Read-only (`assignee`, `assigned_vendor` on tickets; `assignee` on inspections) |
| Vendors | important | 2 | Read-only |
| Scheduling/appointments | important | 2 | Read-only schedule dates on inspections and turns; no appointment object |
| Residents | important | 2 | Present but limited — tenancy written in through move-in / notice; no resident read |
| Units/properties | important | 2 | Present — read, plus unit occupancy ingest; no property/unit record create or update |
| Estimates | optional | 1 | Absent |
| Invoices | optional | 1 | Absent |
| Owner approval | optional | 1 | Absent |
| Tags | optional | 1 | Absent |
| WORKFLOW: create a work order | critical | 3 | Present (live 060) |
| WORKFLOW: assign it | critical | 3 | Absent (no assignment write; live 072) |
| WORKFLOW: transition its status to completion | critical | 3 | Present (live 068) |

## Functional coverage map
- Core objects: work orders — present (critical, 3); status transitions — present (critical, 3); vendor/technician assignment — read-only (critical, 3); vendors — read-only (important, 2); scheduling/appointments — read-only schedule dates, no appointments (important, 2); residents — present but limited, write-in only (important, 2); units/properties — present (important, 2); estimates, invoices, owner approval, tags — absent (optional, 1 each).
- Primary operational workflows: create a work order — present, live; assign it — absent; transition its status to completion — present, live (completion requires a 5+-word note; cancellation requires one of seven reason codes).
- Principal lifecycle changes (one per classified object, inheriting its class and weight): work-order cancel/void; status change through hold, completion and reopen; assignment reassign/unassign; vendor hold/release/deactivate; reschedule or cancel an appointment/inspection; resident move-in and notice to vacate (move-out); unit status change (occupancy, Down/Model/Employee designation, readiness); estimate approve/reject; invoice approve/void; owner approve/reject; tag remove.
- How the sub-maps were built (judgment, stated so another grader can reproduce it): C1.1 gives an object 1.0 when its principal operations exist — the read, plus the principal write where maintenance work mutates the object (create and status for work orders; occupancy for units) — and 0.5 when it is materially read-only where writes are expected or lacks one principal operation; secondary missing writes (work-order field edits, property/unit record creation) are scored once, as partial writes in C1.2, not docked again in C1.1. C1.2 scores all 14 classification rows (11 objects + 3 workflows) on write capability. C1.3 uses one principal-lifecycle row per object. C1.4 uses one principal state-change row per critical and important object.

## Category 1: Functional Coverage and Usefulness: 3.8/15
- **C1.1 Object coverage: partial — weighted coverage = 59.5% (12.5 / 21); no critical object absent.** [judgment-sensitive]

  | Object | Class | W | Score | Evidence |
  |---|---|---|---|---|
  | Work orders (tickets) | critical | 3 | 1.0 | List with filters and keyset paging (openapi.json `paths./tickets.get`; live 007–008 walked all 398), fetch (`paths./tickets/{ticket_id}.get`; 073), create (`paths./tickets.post`; 060), status update / close / cancel / reopen (`paths./tickets/{ticket_id}.patch`; 066–083). Field edits, notes and photos are not writable — scored in C1.2. |
  | Status transitions | critical | 3 | 1.0 | PATCH `status` enum Open · In Progress · On Hold · Completed · Cancelled, "There is no transition graph — any status is reachable from any other" (objects.html "Writing a ticket"); status filter (028); `ticket.status_changed` event; live 066, 068, 070, 082, 083. |
  | Vendor/technician assignment | critical | 3 | 0.5 | Readable: `Ticket.assignee`, `Ticket.assigned_vendor`, `Inspection.assignee` (openapi.json `components.schemas.Ticket` / `Inspection`; 270 of the 398 live tickets carry an assignee, one an assigned vendor — 007–008). Not writable: POST body is unit_id, asset_id, title, description, priority, category; PATCH body is status, note, cancellation_reason_code, cancellation_notes (openapi.json request bodies); live 072 `assignee_id` → 422 `invalid_request`. Materially read-only where writes are operationally expected. |
  | Vendors | important | 2 | 0.5 | `GET /vendors`, `GET /vendors/{vendor_id}` (live 014: all 15), `last_compliance_state` (objects.html "vendor"), `vendor.hold` event. No create, update, hold or deactivate route (openapi.json paths). Read-only where vendor maintenance writes are expected. |
  | Scheduling/appointments | important | 2 | 0.5 | No appointment object and no scheduled date on tickets. Read-only schedule data only: `Inspection.scheduled_date` / `completed_date`, `Turn.move_out_date` / `move_in_date` / `projected_ready_on`, `Unit.projected_ready_on`, `Ticket.preferred_access_time` / `permission_to_enter`; `turn.schedule_changed` event. No scheduling write. |
  | Residents | important | 2 | 0.5 | No resident list or fetch. A resident tenancy is written by `POST /units/{unit_id}/move-in` (parties with first/last name, email, phone, role) and a departure by `POST /units/{unit_id}/notice` (reference.html "occupancy"; documentation-graded); tenancy records are readable as turns (dates, NTV reason, destination) but carry no resident identity, and `OccupancyRecord.parties` "Never carries email or phone" and is returned only on the write. Present, missing the read of resident identity/contact. |
  | Units/properties | important | 2 | 1.0 | `GET /properties`, `/properties/{property_id}`, `GET /units` (+ `property_id`), `/units/{unit_id}` with `occupancy`, `incoming_lease`, `ready`, `projected_ready_on` (objects.html "unit"; live 011, 012, 058); occupancy writes through notice / move-in (documentation-graded). |
  | Estimates | optional | 1 | 0.0 | Absent — no path or schema. |
  | Invoices | optional | 1 | 0.0 | Absent. |
  | Owner approval | optional | 1 | 0.0 | Absent. |
  | Tags | optional | 1 | 0.0 | Absent (category/subcategory are fixed fields, not tags). |

- **C1.2 Core operational actions: no — weighted coverage = 41.7% (12.5 / 30); a critical write workflow ("assign it") is absent.** Under a workflows-only reading the coverage is 66.7% (6 / 9), and the mark is still no under the rule's critical-absent clause.

  | Row | Class | W | Write | Evidence |
  |---|---|---|---|---|
  | Work orders (object) | critical | 3 | 0.5 | Create yes (060); after creation only status, closing note and cancellation fields are writable (openapi.json `paths./tickets/{ticket_id}.patch.requestBody`); no edit of title, description, priority, category, room or access fields, no comment or photo; `DELETE` → 405 (045). Partial write. |
  | Status transitions | critical | 3 | 1.0 | 066, 068, 070, 082, 083; business rules enforced (067 note required; 071 reason enum). |
  | Vendor/technician assignment | critical | 3 | 0.0 | No assignment field on any write; 072 → 422. |
  | Vendors | important | 2 | 0.0 | GET only. |
  | Scheduling/appointments | important | 2 | 0.0 | No scheduling write on any path. |
  | Residents | important | 2 | 0.5 | Tenancy create (move-in) and notice exist (documentation-graded); no party/contact update, no revoke or cancel. |
  | Units/properties | important | 2 | 0.5 | Unit occupancy changes through the two ingest routes (documentation-graded); no property/unit create, update or designation route. |
  | Estimates / Invoices / Owner approval / Tags | optional | 1 each | 0.0 | Absent. |
  | WF: create a work order | critical | 3 | 1.0 | `POST /tickets` (objects.html "Writing a ticket"); live 060, one record confirmed by 062. |
  | WF: assign it | critical | 3 | 0.0 | Absent — see the assignment row. |
  | WF: transition its status to completion | critical | 3 | 1.0 | Live 068 (Completed with a 5+-word note; `completed_at` set); 067 shows the rule. |

- **C1.3 Delete or lifecycle actions: no — weighted coverage = 38.1% (8 / 21); a critical lifecycle action (reassign/unassign) is absent.** [judgment-sensitive]

  | Principal lifecycle change | Parent class | W | Score | Evidence |
  |---|---|---|---|---|
  | Work order cancel / void, with reason | critical | 3 | 1.0 | PATCH to Cancelled with one of seven `cancellation_reason_code` values (objects.html "ticket", "Writing a ticket"); live 083 (`cancelled_at` set, `duplicate_ticket`). Hard delete is not offered (045 → 405); cancel is the documented void. |
  | Status change: in progress, hold, complete, reopen | critical | 3 | 1.0 | Live 066, 082, 068, 070. |
  | Assignment reassign / unassign / vendor accept-decline | critical | 3 | 0.0 | No assignment write of any kind (openapi.json request bodies; 072). |
  | Vendor hold / release / deactivate | important | 2 | 0.0 | `vendor.hold` is a system-raised event (webhooks.html "The events"); no vendor write route. |
  | Reschedule / cancel an appointment or inspection | important | 2 | 0.0 | Inspections and turns are read-only. |
  | Resident move-in and notice to vacate (move-out) | important | 2 | 0.5 | Move-in and notice routes exist (reference.html "occupancy"; documentation-graded, deliberately not exercised). No revoke-notice, cancel-move-in or confirm-move-out route; the confirmed move-out, which is what changes `unit_status` (webhooks.html `turn.status_changed`), is not an API action. |
  | Unit status change (occupancy, Down/Model/Employee, readiness) | important | 2 | 0.5 | Occupancy advances through move-in / notice; the designations are "sticky designations an operator sets" (objects.html "unit") and readiness is derived — neither is writable. |
  | Estimate approve/reject; invoice approve/void; owner approve/reject; tag remove | optional | 1 each | 0.0 | Absent. |

  Sensitivity: excluding the reassignment row as "an update, not a lifecycle change" still leaves 44.4% (8 / 18), which is a no; only a finer map that splits the work-order lifecycle into three critical rows and also excludes reassignment reaches partial (52.4%).

- **C1.4 Change notification: partial — push covers 58.8% (10 / 17) of the critical-plus-important state changes.**

  | State change | Class | W | Score | Mechanism |
  |---|---|---|---|---|
  | Work order created | critical | 3 | 1.0 | `ticket.created` (webhooks.html "The events"); subscribed in 076. |
  | Work order status changed (incl. completion, cancellation) | critical | 3 | 1.0 | `ticket.status_changed` with `change.from` / `change.to`; delivered live (1790607263438.json: Open → On Hold, `data` identical to the 082 PATCH response). |
  | Assignment changed | critical | 3 | 0.0 | No event (the ten keys in webhooks.html; `ticket.status_changed` carries status only). |
  | Vendor status change | important | 2 | 0.5 | `vendor.hold` only; no event for release or deactivation. |
  | Schedule change | important | 2 | 0.5 | `turn.schedule_changed` (projected ready date) and the overdue signals `inspection.stale`, `pm.stale`, `turn.task.stale`; nothing when an inspection or appointment is scheduled, moved or completed. |
  | Resident move-in / notice / move-out | important | 2 | 0.5 | `turn.status_changed` fires only when the stored `unit_status` changes (confirmed move-out, lease start) — explicitly "not the notice being posted" (webhooks.html). |
  | Unit/property status change | important | 2 | 0.5 | `turn.status_changed` on `unit_status` changes; "Setting a designation does not fire it"; `ready` / `occupancy` field changes are not evented. |

  Polling complement: `updated_since` exists on tickets, assets, turns and meters and was honored live (026, 027, 032), but not on units, properties, vendors or inspections (objects.html "Filters"). Push cannot reach 85% here because assignment change (weight 3 of 17) has no event.

Score math: earned 1.0 of 4 applicable checks (0.5 + 0 + 0 + 0.5); unrounded fraction = 0.25; category points = 3.75 → 3.8/15; verification coverage = 4/4 = 100%.

What this means for you: you can pull the whole maintenance record into your own tools, create work orders, move them through their status lifecycle (including a clean close and a coded cancel), and feed move-ins and notices from your PMS. You cannot assign or dispatch work to a technician or vendor, edit a work order after it is filed, manage vendors or appointments, or handle estimates, invoices, owner approvals or tags through the API — those stay inside the FixGrid app. Webhooks tell you about new and changed tickets and unit turns, but not about assignment changes.

## Category 2: API Design, Reliability, and Operability: 7.9/10
- **C2.1 Modern API conventions: yes.** Resource-oriented REST over JSON: 26 operations on 23 paths using GET, POST, PATCH and DELETE (reference.html "Operations"; openapi.json `servers` https://app.fixgrid.app/api/v1); a wrong verb gets 405 with an `Allow` header (errors.html `method_not_allowed`; live 044 and 045, `allow: GET, PATCH`). Live GET 007–034, POST 060 and 076, PATCH 066–083, DELETE 084.
- **C2.2 Consistent typing: yes.** [judgment-sensitive] Every schema in openapi.json `components.schemas` types every field (nullable as type arrays; enums for ticket status/priority, asset status, turn status, inspection status/result, unit occupancy, vendor compliance state, NTV and destination codes; `format` date, date-time, uri). Rendered examples are type-consistent (index `/whoami`; webhooks.html envelope, turn payload and 201 body; errors.html). Live: this run type-checked every preserved row against the published schemas — 1,158 list rows across nine schemas (all 398 tickets, 8 properties, 15 vendors, 85 inspections, 15 meters; 200 of 774 units; 200 of 5,388 assets; 232 of 708 turns; 5 readings), 15 single-object responses, the one webhook-subscription list row and the webhook delivery's `data` — with 0 type, enum, format, missing-key or undocumented-key deviations. Noted, not counted as type inconsistencies: the three card-only webhook events carry `data.property` / `data.unit` as label strings in a separately documented inline dict discriminated by `event` (webhooks.html "Three keys carry a small inline dict"); `OccupancyRecord.status` is documented as lower-case "active" while `Turn.status` is Title-Case (a value-casing, not a type, difference).
- **C2.3 Structured errors: yes.** One shape, `{"error":{"code","message"}}`; "code is stable and safe to branch on", nineteen codes each mapped to an HTTP status (errors.html "The contract", "The nineteen codes"; openapi.json `components.schemas.Error`). Live: all 30 error captures use that shape with a populated code and correct status — e.g. 035 400 `invalid_cursor`, 037 422 `invalid_request`, 004 401 `unauthorized` + `WWW-Authenticate`, 046 403 `insufficient_scope`, 042 404 `not_found`, 044 405 + `Allow`, 063 409 `idempotency_mismatch`, 050_rate_limit_429 429 `rate_limited`. Limitations noted: `invalid_request` covers many 400/422 situations (the docs ask you to branch on status and sentence); two live message sentences differ from the documented ones (064, 063) with the code unchanged.
- **C2.4 Duplicate prevention: yes.** Every consequential write requires an `Idempotency-Key` — ticket create, ticket status, notice, move-in, webhook create (errors.html `idempotency_key_required`; reference.html parameter tables). Live: 048 refuses a create without the key (400); 060 + 061 with the same key produced one ticket (3568; replay flagged `x-idempotent-replay: 1`; unit ticket count 11 → 12 in 059/062); 063 changed body with the same key → 409; 069 PATCH replay; 077–078 webhook create replayed (same subscription 11, secret omitted, reordered events treated as the same request as documented). Delete is naturally idempotent (089 → 404). Notice / move-in idempotency is documentation-graded.
- **C2.5 Graceful handling under load: yes.** A documented 429 with `Retry-After` as "An integer number of seconds" and a stated allowance of 120 per minute per key (limits.html "The limit", "On 429"; errors.html `rate_limited`; openapi.json `x-rate-limit` and a 429 response on every keyed operation). Live: 050_rate_limit_429 (429, `retry-after: 30`); 055 a second key unaffected; 056 recovered after 32 s. (The spec does not declare the `Retry-After` header object; the guides do.)
- **C2.6 Pagination for large collections: yes.** Keyset cursors, rows "ascending by id", `next_cursor` null at the end, inserts during a walk "do not shift or duplicate rows", default 50 / maximum 200 with the clamp documented (objects.html "The list envelope", "Paging"). Live: 007 + 008 traversed all 398 tickets with no duplicates; 009 limit=999 applied as 200; 010 default 50; 034 readings paging. There is no total count; the next-page token satisfies the check.
- **C2.7 Bulk or incremental export: partial.** [judgment-sensitive] No bulk, export or async-job operation exists among the 26 operations (openapi.json paths). Full datasets can be walked in 200-row pages, and `updated_since` supports incremental sync on tickets, assets, turns and meters (honored live: 026, 027, 032), but not on units, properties, vendors or inspections ("/properties and /vendors take limit and cursor only"; inspections "There is no updated_since"; units take `property_id` only — objects.html "Filters"). An account export exists only by contacting support (terms.html §7).
- **C2.8 Webhook security and delivery reliability: yes.** Signed: `FixGrid-Signature` with an HMAC-SHA256 over `<timestamp>.<body>`, Python and Node verification code and a 5-minute tolerance (webhooks.html "Verifying the signature"). Retry policy: first attempt on the hourly sweep, retries at 1 · 2 · 4 · 8 · 24 h, then parked, records kept 30 days, parked deliveries retryable by the admin (webhooks.html "Delivery, retries, and giving up"). Consumer guidance: dedupe on `event_id`, at-least-once delivery, timestamp tolerance against replay, per-object ordering by `occurred_at` (webhooks.html "The envelope", "Your endpoint"). Live: subscription 076; one delivery (1790607263438.json) with the signature header and the seven-key envelope in the documented order; HMAC verified valid as recorded in packet_manifest_v2_FINAL.md. Retries were not observable (the one delivery succeeded).
- **C2.9 Concurrency and conflict control: no.** [judgment-sensitive] No ETag / If-Match or record version (`schema_version` is the payload-shape version — limits.html "schema_version is not the API version"); PATCH has no precondition and "any status is reachable from any other" (objects.html "Writing a ticket"); a search of all developer-host sources finds no ETag, If-Match, concurrency or conflict guidance; no live response carries an ETag (79 of 79). The only 409s are idempotency-key collisions (`idempotency_in_progress`, `idempotency_mismatch`), which prevent duplicate application of the same request — credited in C2.4 — but do nothing against two integrations overwriting each other's change.
- **C2.10 Versioning and backward compatibility: yes.** Version in the path, `/api/v1`, "If we ever publish a v2, v1 keeps answering"; explicit breaking and non-breaking lists; breaking changes "get 90 days' notice" (limits.html "Versioning", "These are breaking…", "These are not breaking…"); corroborated by changelog.html "Deprecation" (announced at least ninety days ahead, old behaviour keeps working). (This evidence is not reused in C4.4.)
- **C2.11 Request traceability: partial.** No request or correlation identifier is documented anywhere in the packet; the documented support path is "contact support with the time and the path" (errors.html `internal_error`). Live responses all carry `rndr-id` and `CF-RAY` (79 of 79) — hosting/CDN identifiers that FixGrid does not document or tie to support.
- **C2.12 Service availability and status transparency: partial.** No public status page (/status.html is 404 on both hosts — dev_status.*, www_status.*; status.fixgrid.app does not resolve per the manifests). An SLA exists only inside Enterprise contracts ("Dedicated CSM + written SLAs"; "SLA-backed uptime — Written availability commitments" — pricing.html Enterprise card and "Why Enterprise costs what it does"); standard terms disclaim uninterrupted service (terms.html §10).

Score math: earned 9.5 of 12 applicable checks (8 yes, 3 partial at 0.5, 1 no; no N-A); unrounded fraction = 0.791667; category points = 7.916667 → 7.9/10; verification coverage = 12/12 = 100%.

What this means for you: the engineering is solid for automation and AI agents — every captured record matched its published type, errors carry stable codes, an idempotency key really did stop a duplicate ticket, throttling tells you exactly how long to wait, paging is clean, the version contract promises 90 days' notice, and webhooks are signed and retried. The gaps: nothing stops two tools from silently overwriting each other's status change, there is no documented request ID to quote to support, there is no status page (a written SLA only on Enterprise), and there is no bulk export or changed-since filter for units, properties, vendors or inspections.

## Category 3: Access Control and Safe Automation: 4.4/5
- **C3.1 Read-only credentials: yes.** The `read` scope "opens every list and fetch" and nothing else (authentication.html "Scopes"). Live: 001 shows scope `read`; the read key is refused on a ticket create (046 → 403) and on a subscription delete (080 → 403).
- **C3.2 Scoped credentials: partial.** [judgment-sensitive] Three fixed scopes — `read`, `write` (every write plus read and webhooks) and `webhook_only` (subscriptions only) — "fixed at mint" (authentication.html "Scopes"; developers.fixgrid.app "What it carries"). Every key is company-wide (developers.fixgrid.app "What the key is"); a key cannot be limited to one property, one resource type or one action (e.g. create tickets but not record occupancy). Live: 047 a `webhook_only` key is refused on `/tickets` (403). Broad scoping only.
- **C3.3 Multiple keys: yes.** Keys are created and labelled per integration (developers.fixgrid.app "How you get one"), and the rate limit is counted per key "so two integrations at the same customer never spend each other's budget" (limits.html "The limit"). Live: three distinct keys in use (001–003); 055 one key's throttle left another unaffected.
- **C3.4 Rotation and revocation: yes.** The Company Admin can "Revoke it on the same tab at any time", and a lost key is replaced by minting a new one and revoking the old (developers.fixgrid.app "How you get one", "If you have lost a value"); a revoked key answers 401 `key_revoked` (errors.html). Self-serve for the account owner; documentation-graded (revocation was not captured).
- **C3.5 Test and production isolation: N-A.** No sandbox or separate test environment is documented (keys begin `fg_live_`; no sandbox, test-key or staging material anywhere in the packet). The battery ran on the operator's demonstration company in production.

Score math: earned 3.5 of 4 applicable checks (C3.5 N-A excluded); unrounded fraction = 0.875; category points = 4.375 → 4.4/5; verification coverage = 4/4 = 100% (N-A excluded from the denominator).

What this means for you: you can give each integration or AI agent its own key, make it read-only, and revoke it yourself in the app. You cannot narrow a key to one property or one kind of record — a `write` key can create tickets, change statuses and record move-ins across the whole company — and there is no sandbox, so testing happens in a real (or demo) company.

## Category 4: Documentation and AI-Agent Readiness: 3.8/5
- **C4.1 Complete self-serve reference: partial.** Coverage is complete — all 26 operations with parameters, request bodies, every response code with its error sentences, and full field tables, plus authentication, error and webhook guides (reference.html; authentication.html; errors.html; webhooks.html). Worked examples are thin for the core endpoints: every operation has a curl request, but the write examples are generated placeholders (reference.html POST /tickets: `"unit_id":0 … "priority":"string","category":"string"`, values that would be refused), and there are no response examples in the reference or the OpenAPI document (zero `example` keys). Rendered response JSON exists only for `/whoami`, the list-envelope skeleton, error bodies and webhook payloads — not for listing, fetching, creating or updating a ticket, unit, turn or vendor. Documentation-versus-live contradictions observed: the status-in-create refusal reads "The server sets status; omit it." live (064) versus the documented "This endpoint always creates a ticket with status 'Open'. Remove `status` from the request body."; the create mismatch reads "different ticket request" live (063) versus the documented "different ticket payload"; webhook events are documented as "Stored sorted and deduplicated" but were echoed and listed in request order (076, 079); authentication.html says no-store applies to "Every response on the namespace", but the 404-path and 405 responses carried no Cache-Control (043, 044, 045). The site footer's claim that every error sentence is copied from the running product is therefore not fully borne out.
- **C4.2 Reliable machine-consumable integration path: yes.** A public OpenAPI 3.1 document at developers.fixgrid.app/openapi.json and app.fixgrid.app/api/v1/openapi.json (byte-identical, no key needed) covers all 26 operations with typed schemas, enums, required fields and the bearer security scheme; this run found 0 deviations between it and the live data. No official SDK and no MCP server (developers.fixgrid.app warns never to put a key in a Claude/MCP connector); one strong mechanism suffices. Limitations: no `operationId`s (generators must synthesize names), no examples, `Retry-After` not declared as a response header.
- **C4.3 AI-readable documentation: partial.** developers.fixgrid.app/llms.txt (1,515 bytes) is an index of links with one-line summaries (dev_llms.txt); llms-full.txt is 404 (dev_llms-full.txt "Not Found"); there is no per-endpoint Markdown or plain-text corpus. The marketing llms.txt carries a "Developer API" section of links only (www_llms.txt). Index-only.
- **C4.4 Kept current: yes.** [judgment-sensitive] The changelog records every change with dates from v1 on 2026-09-08 through 2026-09-24, naming the objects and routes affected ("Every change to the API, dated. Additive changes land here on the day they ship"); the reference is "Generated from openapi.json — the same document the API serves" and "The reference and this page both change with the same publish" (reference.html; changelog.html "Watching this page"); sitemap lastmod dates run 2026-09-12 to 2026-09-24 (dev_sitemap.xml); the two OpenAPI copies are identical and matched live data on 2026-09-28. Blemish: the 2026-09-12 Turn entry is marked "Documented here 2026-09-24" — a 12-day lag against the same-day promise, disclosed by the changelog itself; there is no email list.

Score math: earned 3.0 of 4 applicable checks; unrounded fraction = 0.75; category points = 3.75 → 3.8/5; verification coverage = 4/4 = 100%.

What this means for you: a developer or an AI coding tool can generate a working client straight from the OpenAPI file, which matched the live API exactly, and the changelog is dated and current. The reference lacks realistic request and response examples for the everyday calls, there is no full-text llms file for AI tools, and a few documented error sentences do not match what the API actually returns — build against the error codes, not the words.

## Category 5: Accessibility and Cost: 15.0/15
- **C5.1 Self-serve API key: yes.** "A Company Admin mints the key inside FixGrid" on Integrations → Developers; "You do not apply to FixGrid for a key" (developers.fixgrid.app "How you get one", "Talk to us"); security.html "Access control". Live: the three keys used were minted by the operator (packet_manifest_v2_FINAL.md; 001–003).
- **C5.3 Not commercially gated: yes.** [judgment-sensitive] "Included on every plan. Starter, Professional, and Enterprise. No partner fee." (developers.fixgrid.app "What it carries"); "Public API + signed webhooks — Included on every plan … No partner fee, no API add-on." and the "On every plan" list (pricing.html); the cheapest plan is Starter at $49/mo. The API documentation shows no plan gating of any endpoint or scope and no plan-related error code. Note: some product modules whose data sits behind API objects — Grid Auriga vendor management, the full asset registry, utilities — are Professional-tier product features (pricing.html "Compare plans"), so a Starter company would have less data to read; this is product packaging, not an API entitlement.

Score math: earned 2 of 2 applicable checks; unrounded fraction = 1.0; category points = 15.0/15; verification coverage = 2/2 = 100%.

What this means for you: every plan, including the $49/month Starter, includes the API and webhooks with no add-on fee, and your own admin creates keys in the app without a sales call.

## Total
- Raw: 34.79 / 50 (3.75 + 7.916667 + 4.375 + 3.75 + 15.0 = 34.791667, from unrounded category values)
- Normalized before rounding: 69.58 / 100 (34.791667 ÷ 50 × 100 = 69.583333)
- Published numeric score: 70 / 100
- Letter grade: C-
- Evidence tier: fully verified — controlled live
- Overall verification coverage: 100% — 26 of 26 applicable checks verified (C3.5 N-A excluded; no unverified checks). Gate: no category Unable to verify (every category at 100%); overall ≥ 80% — passed; minimum battery complete — passed.
- Partial-result flag: no.
- Unresolved evaluator disagreements: none recorded in this run (independent run; other runs not consulted). The score sits close to the C-/D+ boundary, so the judgment-sensitive marks matter; each alone would move it as follows: C1.1 partial→no −3.75 (65.8 → 66, D); C1.2 no→partial +3.75 (73.3 → 73, C); C1.3 no→partial +3.75 (73.3 → 73, C); C2.2 yes→partial −0.83 (68.8 → 69, D+); C2.7 partial→yes +0.83 (70.4 → 70, C-); C2.9 no→partial +0.83 (70.4 → 70, C-); C3.2 partial→yes +1.25 (70.8 → 71, C-); C4.4 yes→partial −1.25 (68.3 → 68, D+); C5.3 yes→partial −7.5 (62.1 → 62, D-).

## Bottom line for a property manager
FixGrid's API lets you pull your whole maintenance record — work orders, units with occupancy and make-ready status, turns, assets, vendors with insurance compliance, inspections and meters — create work orders and move them to done or cancelled, feed move-ins and notices from your PMS, and get signed alerts when tickets or unit turns change. Its biggest strength is build quality: typed and accurately published schemas, stable error codes, duplicate-proof writes, clear throttling, a written versioning promise, and full access on every plan with keys your own admin creates. Its biggest limitation is reach: you cannot assign or dispatch work, edit a work order after filing, manage vendors or appointments, or handle estimates, invoices, approvals or tags through the API, and there is no sandbox, status page or fine-grained key scoping. FixGrid is a maintenance system, not a PMS or a bank; it holds no funds and documents no trust or deposit workflows, so you still need your PMS for leasing, residents and accounting and your bank for money movement.
