Most error responses follow this shape:Documentation Index
Fetch the complete documentation index at: https://teardowns.aero/docs/llms.txt
Use this file to discover all available pages before exploring further.
unknown_vocabulary errors include valid_values so you can correct
the value without reading docs. Validation errors from Pydantic
(unknown body keys, wrong types) follow FastAPI’s standard 422 shape.
Branch on error_code, surface message to humans, log the rest.
Two exceptions to the envelope shape
Some endpoints return a plain string indetail rather than the
{ error_code, message } object. Today this applies to:
-
404 Not Foundon the by-id endpoints (GET /teardowns/{id},PATCH /teardowns/{id},DELETE /teardowns/{id}, and the matching/sales-lease-exchange/{id}routes). The body is literally:or:There is noerror_codeormessagefield branch on the HTTP status404instead. -
400from the transition endpoint when the state-machine rejects a move (e.g. trying tocompletea teardown that is still inactive_starting). The body is a plain string from the state-machine layer:Same pattern: noerror_code, branch on the400status and theactionyou sent.
{ error_code, message, ... } envelope is the rule.
Quick remediation matrix
| Status | error_code | Why | Fix |
|---|---|---|---|
| 401 | missing_or_malformed_authorization | Authorization header missing, empty, or doesn’t start with Bearer tdao_live_ | Send the API key as Authorization: Bearer tdao_live_…. JWTs from the web app are not accepted here. |
| 400 | invalid_organization_header | X-Organization-Id missing or not a UUID | Send the org’s UUID from the API Access page. |
| 401 | invalid_api_key | Key not found, revoked, or rotated | Mint a fresh key on the Settings page. Old value is dead. |
| 401 | api_key_expired | Key passed its expires_at | Mint a new key, optionally without an expiry. |
| 403 | organization_mismatch | The key belongs to a different org than the X-Organization-Id header | Always send the org id that goes with the key. Every occurrence is treated as suspicious and alerted on. |
| 403 | org_inactive | The org’s account_status is inactive | Contact Teardowns.aero support. |
| 403 | org_churned | The org’s account_status is churned | Contact Teardowns.aero support. |
| 403 | subscription_required | Subscription is past_due / canceled / expired / rejected / pending payment / pending approval | Resolve billing. The subscription_status field is included in the error body. |
| 403 | api_access_disabled | API access is turned off for this org | Ask Teardowns.aero support to re-enable. Existing keys resume working, no rotation needed. |
| 403 | api_key_creator_revoked | The user who minted the key is no longer active in this org | Have another eligible member mint a replacement key. |
| 403 | insufficient_capability | The minter doesn’t hold the capability the endpoint requires | Have an org admin grant the capability, or mint the key as someone who already has it. |
| 404 | (plain-string body, no error_code) | The resource doesn’t exist OR belongs to a different org | Confirm the id is yours. We deliberately do not distinguish never reveal foreign-org id existence. See the “Two exceptions” note above for the exact body shape. |
| 422 | invalid_status | The status field is not one of Starting / In process / Completed | Use one of the three exact values. Case-sensitive. |
| 422 | vocab_required | A required vocabulary field is missing for the chosen asset_type | Send aircraft_type for aircraft, engine_model for engine, etc. |
| 400 | unknown_vocabulary | The vocabulary name doesn’t match any active row | The error body includes valid_values pick one of those. Match is case-insensitive. |
| 422 | invalid_audience | An audience value is not in the allowed set | Allowed: Airline / Lessor / OEM / MRO / Distributor / Others. Case-insensitive. |
| 400 | file_too_large | Upload exceeds 50 MB | Split the file or compress. |
| 400 | invalid_content_type | Upload MIME isn’t in the accepted list | Accepted: PDF / XLSX / XLS / DOCX / DOC / CSV / JPEG / PNG / WEBP / GIF. |
| 502 | storage_unavailable | Supabase Storage upstream failed | Retry with backoff. Each retry uploads a new file with a fresh UUID prefix no corruption risk. |
| 413 | payload_too_large | JSON body exceeded the 10 MB cap | Use the document upload endpoint for files, not the JSON endpoint. |
| 429 | too_many_requests | A rate-limit bucket overflowed | Check Retry-After header. Back off. See rate-limiting. |
| 422 | various Pydantic shapes | Unknown body key, wrong type, etc. | The response body lists the bad field. Strict mode = no silent drops. |
Detailed explanations
401 invalid_api_key same response, three causes
401 invalid_api_key same response, three causes
We return this same code whether the key doesn’t exist, was revoked,
or matched a fingerprint but failed status checks. The single
response prevents an attacker from probing the keyspace.From your side: if you minted the key recently and it suddenly stops
working, the most likely cause is that the minter was deactivated
(which auto-revokes their keys within an hour, but the 401 starts
immediately). Less likely: the key was rotated by another org admin.
403 organization_mismatch always investigate
403 organization_mismatch always investigate
The API key already implies an org. Sending a different org id in
X-Organization-Id is, by construction, never a legitimate user
error either the wrong value got pasted into your ERP config, or
something more interesting is happening. We alert on every
occurrence.Fix: re-paste the Organization ID from the Settings → API Access
page where you minted the key.403 api_access_disabled vs subscription_required
403 api_access_disabled vs subscription_required
Two different switches:
api_access_disabled: API access was turned off for your org specifically. Has nothing to do with billing.subscription_required: your subscription is past_due / canceled / expired / not yet active. Resolve billing.
400 unknown_vocabulary self-correcting error
400 unknown_vocabulary self-correcting error
The full list of valid values is in the response body under
Match is case-insensitive
valid_values:a320-200 and A320-200 both work.
Whitespace at the edges is stripped.422 invalid_status exact strings only
422 invalid_status exact strings only
The
status field accepts exactly three values:StartingIn processCompleted
starting, in progress, STARTING all return 422.
We picked strict matching so the OpenAPI docs show the three values
as a real enum IDE autocomplete and linters can see them.422 with no error_code Pydantic validation
422 with no error_code Pydantic validation
Validation errors from the schema layer use FastAPI’s standard 422
shape. Example for an unknown body key:
loc tells you exactly which key was bad. Common offenders:
registration (use tail_number), location_country (use
country), estimated_teardown_date (use start_date),
aircraft_type_id (use the name, not the UUID).429 too_many_requests when to back off
429 too_many_requests when to back off
The response includes a
Retry-After header in seconds. Wait at
least that long before retrying. Exponential backoff on top is good
practice the limit window resets gradually.If you’re hitting bucket A (the 600 req/min per-key limit) regularly,
your ERP is probably retrying too aggressively. Mint a second key
for batch jobs that need their own budget.500 / 502 retry, then ping support
500 / 502 retry, then ping support
500 is an unexpected server-side bug. 502 storage_unavailable is
a Supabase Storage upstream issue.For both: retry the request with backoff. If it persists for more
than a minute, capture the request id (returned in the
x-request-id response header) and email support@teardowns.aero
with the id + a description.What we don’t return
A few common shapes you might expect that the API deliberately doesn’t use:- Plain text errors. Every error has a structured JSON body. No bare strings. No HTML pages.
- Different shapes for different error families. The shape is always
{ "detail": { "error_code", "message", ...optional fields } }(for business errors) or FastAPI’s default 422 shape (for schema errors). success: true/falseenvelopes. Successes return the resource directly. Branch on HTTP status, not a wrapper field.

