Errors
Every error carries a JSON body with a stable error code (machine-readable) and a human-readable detail sentence. Some codes add extra fields — the per-code reference below documents them.
error slugs as a public API surface. New slugs ship additively; the ones below won't change shape without a v2.Error body shape
All non-2xx responses use the same envelope:
{ "error": "<machine-readable error code>", "detail": "<one-sentence human-readable explanation>", // Optional fields per error code — see the reference below. "field": "filter.signals[2]", "reason": "unknown signal name", "upgrade_url": "https://terminal.house/api-keys?action=upgrade", "retry_after_seconds": 17, "quota_resets_at": "2026-06-12T00:00:00Z"}
The full set of optional fields varies by error code. The common ones — upgrade_url, retry_after_seconds, quota_resets_at — are documented per code below.
Status code reference
| Code | Slug | When |
|---|---|---|
| 400 | invalid_filter | Filter DSL failed validation. Body includes field and reason. |
| 400 | invalid_request | Required field missing, malformed JSON, or schema mismatch. |
| 401 | unauthorized | API key missing, malformed, expired, or revoked. |
| 402 | quota_exceeded | Monthly lead-views exhausted. Upgrade tier or wait for reset. Free operations (watchlists, pipeline, feedback) still work. |
| 402 | trial_card_required | Trial card missing or declined. Re-enter a card via Stripe Checkout to resume access. |
| 403 | forbidden_tier | Endpoint requires a higher tier. Starter keys hitting MCP / webhooks / CSV return this. Body carries an upgrade-prompt URL. |
| 403 | forbidden_seat | Member role lacks permission for this operation (e.g. a member trying to issue an API key on a Team tenant — admin-only). |
| 404 | not_found | Folio, watchlist, member, or API-key ID does not exist. |
| 409 | conflict | Watchlist name already in use; duplicate key label; pipeline state already at the requested value. |
| 422 | unprocessable | Request well-formed but semantically invalid (e.g. inline filter omits both recipe and filter). |
| 423 | account_paused | Subscription canceled; tenant in 90-day data-preservation window. Reactivate to resume. |
| 429 | rate_limited | Too many requests per minute. Honor Retry-After header. See Rate limits. |
| 500 | internal_error | Unexpected server-side fault. Logged + Sentry-paged. Safe to retry with exponential back-off. |
| 503 | upstream_unavailable | A county source is degraded. Cached data is still served when possible (cache_freshness_hours in the body indicates how stale). |
402 — quota_exceeded
You've hit your monthly lead-view cap. Reads return 402 until your billing-anniversary reset or a tier upgrade. Free operations (watchlists, pipeline, feedback) keep working.
HTTP/1.1 402 Payment RequiredContent-Type: application/json { "error": "quota_exceeded", "detail": "Operator tier monthly lead-views exhausted. Upgrade or wait for reset.", "quota_resets_at": "2026-06-12T00:00:00Z", "upgrade_url": "https://terminal.house/api-keys?action=upgrade"}
423 — account_paused
Your subscription was canceled (during-trial cancel or post-trial cancel). The tenant is paused, all surfaces are frozen, and data is preserved for 90 days. After that, an automated cleanup deletes account data per the GDPR-aligned retention policy.
HTTP/1.1 423 LockedContent-Type: application/json { "error": "account_paused", "detail": "Subscription canceled. Reactivate to resume API access. Data preserved 90 days.", "paused_at": "2026-04-29T14:02:31Z", "data_purge_at": "2026-07-28T14:02:31Z", "reactivate_url": "https://terminal.house/api-keys?action=reactivate"}
Re-subscribing within 90 days restores access at the chosen tier — same data, same watchlists, no second trial.
429 — rate_limited
You crossed the per-minute rate ceiling. The Retry-After header is the seconds to wait. Repeated immediate retries extend the cool-down — use full jitter.
HTTP/1.1 429 Too Many RequestsRetry-After: 17Content-Type: application/json { "error": "rate_limited", "detail": "Too many requests per minute. Honor Retry-After header.", "retry_after_seconds": 17}
5xx — upstream_unavailable
A county data source (Miami-Dade Property Appraiser, Broward, Palm Beach, Sunbiz, etc.) is degraded. We serve cached data when we can — cache_freshness_hours in the body tells you how stale.
HTTP/1.1 503 Service UnavailableContent-Type: application/json { "error": "upstream_unavailable", "detail": "Miami-Dade Property Appraiser is degraded. Cached data served when available.", "source": "fl-mdc-property-appraiser", "cache_freshness_hours": 18}
Track upstream source health at status.terminal.house.