The full reference.
Every field, parameter, error and limit of the triage API. One photo in, one answer out. The Developers page is the short version.
One photo in
"tier": "EMERGENCY",
"trade_required": "Appliance Technician",
"safety_hazard": true
A resident-reporting app, a maintenance inbox, your own work-order tool. You send the photo. One fixed reply comes back: what it is, whether a fault is visible, one of four urgency words, which trade, a line for the resident, and a link to tell us what it turned out to be.
A demo key is free, and the key is emailed to you as soon as you ask. A direct Buildium connection is built and proven only in our own Buildium test account, needs Buildium Premium, and isn't open to manager accounts yet (once signed in, /connect shows what it would read and write). For any other PMS or CMMS, your developers map our reply to your work-order fields. The field notes below are written for that.
That's set in code, not by the model. The one exception comes first: if the photo isn't of a building at all (a person, a pet, a vehicle), the answer is P4, not urgent. How urgency works ›
Make a request
Base64 it (JPEG, PNG or WebP, 1 KB to 3 MB) and put it in the image field of a JSON body.
Key in the x-triage-key header. ?config=fast reads the photo three times, ?config=deep five. &extended=1 adds the rest of the reply.
Read core, keep diagnosis_id, and when you learn what it really was, send that to callback_url.
All three send the same request and get the same reply. The two clients take the photo as bytes, base64 or a data URL, send your key as x-triage-key, and turn a refusal into an error carrying the status, our error code, its sentence and the Retry-After seconds. To use fixRAgent inside Claude, ChatGPT, Cursor or Gemini, set it up for your AI assistant.
A real reply
- One gas furnace and air handler, captured 13 Sep 2026. Three of three reads found no fault, so the tier is
WHENEVER. Read five times, five of five agreed. - config_id names the setup that answered, and
config_source.fallback_used: falsesays it was the measured one (when it isn't). - agreement counts matching reads. It covers the fault verdict only. Reads can agree and still be wrong, and it says nothing about the urgency, the trade or the parts.
- The two calls took 4.2 s (three reads) and 4.7 s (five reads), from one machine, one call each. That's what was measured. It is not a service level.
Fast or deep
What production served on the one call measured, 13 Sep 2026 at 13:36:18Z. agreement.n on your reply says what yours got.
The same photo read five times, independently. Can take a second round when the first is undecided, and says when it did.
How the reads become one answer
- Fact by fact, not a single read picked out. The fault verdict is the answer most reads gave; a tie reads no fault.
- Wiring and water are voted the same way, and a tie reads false, so the EMERGENCY floor never fires on a coin toss.
- Each of the four graded readings is the most common value. A tie takes the lower one, except a fuel-gas, carbon-monoxide or exposed-conductor tie, which rounds up.
- Urgency is then worked out once, in code, from those voted facts. The wording comes from the read whose own urgency matches.
- A failed read counts against agreement. It's in
n, never ink, so the engine that fails most can't look the most consistent. - Decided when
k >= ceil(n/2)(stamped on every reply asagreement.vote_rule). A tie is never decided. Undecided takes a second round of the same size, with both rounds inagreement.roundsand a sentence inwarnings.
A Triage Profile prints agreement as a count of reads, never as a percentage.
Try it without a key
A mock server replays the real replies on this page, in full, so you can wire up a client before your key arrives. config=fast gets the three-read reply, config=deep the five-read one.
The same requests and examples (Collection v2.1). Import it, then point baseUrl at the mock or at https://fixragent.com. It's made from the captured replies, so it can't say something the API didn't.
Points the same requests at the real API. Import it beside the collection, put your key in the secret key variable, switch it on, and the fast request runs for real.
JavaScript and Python
Two thin clients, each made from openapi.json so the types can't drift from the API. Download the file and import it; that's the install today. The same files become the @fixragent/triage npm package and the fixragent-triage PyPI package, and those install lines land here with the first release. Source: sdk/, MIT licence.
await triage(photo, { key, config, extended, problemText, reporter, lang })triage(photo, key=, config=, extended=, problem_text=, reporter=, lang=)await reportOutcome({ callbackUrl, outcome: 'fixed' })report_outcome('fixed', callback_url=…)FixragentError with status, code, message, retryAfterFixragentError with status, code, message, retry_afterindex.d.tstypes.py (TypedDict)api, sends source:sdk-jschannel api, sends source:sdk-pythonbaseUrl: 'https://b1442b0b-4a7a-4135-ab4d-9c4b9c1ab0d2.mock.pstmn.io'base_url='https://b1442b0b-4a7a-4135-ab4d-9c4b9c1ab0d2.mock.pstmn.io'Both clients replay the captured replies on this page in their own tests, and the JavaScript client has been checked against the mock byte for byte.
Other ways in
Paste one tag where the box should sit. Change your-site to a short name; every Triage Profile made from your page carries it, so they count as yours. The box sets no cookies on your site.
The fourth way in, for an agent. Claude, ChatGPT, Cursor or Gemini can send a photo and read the answer for you. With a key, each photo counts against the key's 60 a day, and every call against the same 20 an hour per address. With no key, it reads up to 3 photos per connection in a rolling 24 hours, then answers with a labelled SAMPLE instead of reading yours.
Set it up ›The Triage Profile the deep reply makes, the reply behind it, and the pilot offer, on one printable page. It shows no photo: the photo behind it is held under a licence that covers processing only.
Open it ›The three guards
For callers without a key of their own, who also have a day limit per address (keyless_address_day). A key is judged by the key, never by the address it calls from.
Each demo key. Resets at 00:00 UTC. A key also has a per-minute allowance of 20 (key_burst_limited), counted separately on each of our servers that answers it, not across all of them together.
A daily spending brake protects the service. It's checked before your photo is read, so hitting it costs nothing. A key with its own cap is bounded by that cap instead.
budget_exhaustedWhen one stops you, the reply says which, in a sentence, with a Retry-After worked out from the clock, not a round number. The 503 also carries retry_after_seconds and resets_at.
Keys with a usage plan
So a campaign can send hundreds of photos in minutes without failing and without a surprise bill.
The per-address hour limit doesn't apply. Your key gets its own per-minute allowance, so all your photos can come from one server. It is counted separately on each of our servers that answers, not across all of them; the hard cap is the one counted across the whole service.
key_burst_limited · Retry-After in secondsA fixed number of assessments per window (your pilot period or a calendar month). Past it, nothing runs and nothing is billed. Set with us in writing; it can be raised the same day.
key_cap_reached · usage block · Retry-After to the resetEvery answer carries your usage. GET /api/usage with the same key returns it without sending a photo.
Send an Idempotency-Key (8–128 characters) and a repeat within 24 hours gets the stored answer. Without one, the same photo, problem text, language and setup from the same key within 10 minutes counts the same. A retry sent while the first is still running can run twice; it's billed once.
Billing, busy engine, and checking your total
503 engine_busy with Retry-After, not a 502. Nothing is billed. Retry with the same photo or Idempotency-Key and it's never charged twice./api/usage answers for pilot keys with a usage cap, and for an issued key with its own count: engine runs since 00:00 UTC against the 60 a day. A wrong, dead or missing key gets 401 demo_key_required.
How urgency is set
The server sets both, in code. The model only gives observations: four graded readings and two yes-or-no facts from the photo.
severity P1severity P2severity P3severity P4- The EMERGENCY floor (criterion 1). Visible wiring, or water near anything electrical, is EMERGENCY. By definition.
criterion_1_firedtells you when that's what happened. - Checked first: not a building. When
is_building_assetis false (a person, a pet, a vehicle) the answer is P4, not urgent. - Unsure doesn't fire it. Where either fact is uncertain, the decision table decides instead, or every dim basement photo would be an emergency. Low-voltage wiring (doorbell, thermostat, ethernet) never fires it; it's noted in
fault_summary. - Never dropped for lack of a named fault. A photo can show an exposed conductor with no nameable fault, and that's still an emergency.
- What the model proposed is kept, not used. It's in
provenance.model_proposed_severity, andseverity_overwrittenrecords the overwrite.
Parameters
configquery · fast | deepWhich setup answers. fast reads the photo three times, deep five. The setup comes from a file shipped beside the API and pinned to the prompt, schema and model the code would serve. config_source names the file, config_id the setup.extended block. Left out, it comes back null.lang in the body wins; with neither, the browser's Accept-Language decides (es-* gives Spanish); the default is English. Spanish ›When the measured setup can't answer, and how the reply says so
candidates_file_absent), unreadable (candidates_file_unparseable), missing the setup you asked for (config_not_in_candidates_file), disagrees with the code on the model, prompt or schema, rubric, engine version, read count or temperature (candidate_pin_mismatch, with mismatch listing which), or the multi-read part can't load (vote_module_unavailable).config_source.fallback_used is true with fallback_reason; a sentence in config_source.warning; the same in the top-level warnings; a config_id starting fallback-; and an X-Triage-Config-Fallback header.config_id as the authority.Request body
data:image/...;base64, prefix is accepted and stripped. At least 1 KB, at most 3 MB (3,145,728 bytes) once decoded.configstringSame as the query parameter; the query wins.false and we keep no copy of the photo, only its fingerprint. Left out, the cleaned photo is kept with its Triage Profile (what happens to the photo).true makes the request stricter about training; nothing can turn training on. The header x-triage-do-not-train: 1 does the same.Spanish
fault_summary, photo_subject, asset_type, trade_required, resident_explanation, resident_why_it_matters, resident_what_happens_next, root_cause, failure_mechanism and the rest of the written fields.
Every key, value and taxonomy id. The four urgency words on the wire. And hazard_detail, resident_do_not and every *_reason field stay in English, so a safety line is always the engine's own sentence, never a machine translation.
Send lang=es on the address or "lang": "es" in the body. A browser that prefers Spanish gets the same, and so does the Triage Profile on the home page.
How to tell which language came back
The reply carries a language object: requested, served (en or es), source (body, query, header or default) and hazard_text, which reads the language the hazard line really came back in. That check is there because on 14 Sep 2026, nine of twelve hazard reads under the first wording came back in Spanish. When it says not-en, the Triage Profile holds that line back and prints a fixed sentence instead; your client should decide the same way. Read served before assuming the language: an unknown value is answered in English.
The reply, block by block
X-Triage-Schema header, and every 200 repeats it in schema_version (today triage-response-v1@1.1.0). Within v1 nothing required is removed or renamed, no type changes, no value is taken away. New optional fields can appear, so ignore ones you don't know. A breaking change ships as v2 at a new address, and v1 keeps answering.Errors
Every error is a code and a sentence written for a person. Never a stack trace, never an internal code. Every status comes from one table.
retry_after_seconds, and a retry_after_basis naming the clock: rate_limited (ip_rate_window), demo_key_quota and budget_exhausted (utc_day), key_cap_reached (cap_window), key_burst_limited (key_minute_window) and engine_busy (engine_backoff). It's worked out, never rounded: ask 30 minutes into a closed one-hour window and it says 1800, not 3600. The others send none and never promise a retry.Five of them, as they arrive
What happens to the photo
EXIF, XMP, IPTC and the other carriers that hold GPS are removed before the photo is hashed, sent to the model, or stored.
The photo is turned away (415 image_unsupported or 500 metadata_strip_failed). There's no pass-through, because a photo that still carries GPS looks the same as a clean one.
The cleaned photo is kept privately with its Triage Profile, so whoever holds the profile's link sees it too. Send "keep_photo": false and only its fingerprint, provenance.photo_sha256, is kept. Google, our model provider, keeps what it's sent (prompt, reply and photo) for 55 days.
What we keep ourselves, and for how long, is in the privacy policy.
Tell us how it turned out
Every reply comes with a diagnosis_id and a callback_url. When you find out what it really was, send it back. No key needed.
Every field you can send back, and the replies
callback_url address is for people and logs; the API doesn't read it. Not a uuid: 400 outcome_orphan_refused.triage_link. Anything else: 400 reported_via_invalid. Nothing is quietly changed to an allowed value.- Merge, not replace. Only the fields you send are updated, so a second, fuller report never wipes the first, and a report sent at the same moment is merged, not dropped.
- Replies.
200 { ok: true, outcome: {…} }when it lands.400withoutcome_orphan_refused,outcome_label_invalid,reported_via_invalid,fixed_by_invalidortrade_invalid, each listing what's allowed.409 triage_lane_migration_pendingif our side isn't ready for that lane yet; the write is refused, never downgraded.500is a plain internal error. - Not for you:
GET /api/outcome?email=&diagnosis_ids=is the app's own scan log. It returns nothing for an id the account doesn't own.
Terms and help
The API terms, the terms of service and the privacy policy. In one line: this is an educational triage, not professional advice. Gas, water near electrics and exposed wiring are licensed-trade jobs.
Write to support@fixragent.com. A founder reads every message and replies within one business day. Help has the common questions in plain words.
Legal questions and deletion requests go to legal@fixragent.com, or use Delete my data.
This page is made from openapi.json, which is made from the API's own settings and checked against it. The samples are the API's own replies.
