application/problem+json:
code, never on title or detail — those are prose and may
be reworded. code is stable. Every response, success or failure, also
carries a Request-Id header; quote it when you contact support.
type links directly to that code’s entry on this page, so the URL in an
error always takes you to its explanation.Authentication (401)
missing_api_key
No Authorization header on the request.
Fix: Send Authorization: Bearer ck_live_... (or ck_sandbox_...).
invalid_api_key
The header is malformed, or the key isn’t one we recognize. Also returned
when the scheme isn’t Bearer, or the key doesn’t start with ck_live_ /
ck_sandbox_.
Fix: Check the key was copied whole — they’re shown once at issuance.
revoked_api_key
The key was revoked.
Fix: Use a current key, or ask us to issue a replacement.
expired_api_key
The key passed its expiry date.
Fix: Ask us for a fresh key.
partner_disabled
The account itself is disabled — no key will work.
Fix: Contact api@trycactus.com.
Access (403)
The key authenticated, but can’t reach this product. The code tells you who can unblock it. See Product access for which endpoints belong to which product.product_not_enabled
Your account isn’t enabled for that product.
Fix: Contact api@trycactus.com — only Cactus
can add a product to an account.
missing_scope
Your account has the product, but the key you used was issued without it.
Fix: Use a key that carries the scope. Ask us if you’re not sure which
of your keys does.
Request (400)
invalid_request
Malformed body or parameters. The errors[] array lists each offending
field with a field, code, and message.
Fix: Correct the fields named in errors[].
Not found (404)
not_found
No such resource — or it belongs to another account. The two are
deliberately indistinguishable, so one account can’t probe another’s ids.
Fix: Check the id, and that it was created with the same account.
Method not allowed (405)
method_not_allowed
The path exists but not for that HTTP method.
Fix: Check the method against the API reference.
Conflict (409)
The resource exists but isn’t in a state that allows this call. All of these become retryable once the underlying condition changes.document_not_uploaded
The document was registered, but the file bytes never arrived.
Fix: PUT the file to the upload URL from POST /v1/documents before
referencing it.
document_in_use
The document is attached to an extraction or underwriting still in flight.
Fix: Wait for that job to reach a terminal status, then retry.
extraction_not_completed
You asked for the result before the job finished.
Fix: Poll the extraction (or use ?wait=) until status is
completed. Better still, register a webhook endpoint.
underwriting_not_completed
You asked for the Excel model before the run finished.
Fix: Poll the underwriting (or use ?wait=) until status is
completed, or register a webhook endpoint.
model_unavailable
The run completed but we couldn’t store its workbook. The report itself is
unaffected — report.url and results are there.
Fix: Send us the request_id; we can re-export the model.
Rejected at acceptance (422)
Validation failures. None of these are billed — a document is only charged once it passes acceptance.document_not_found
A referenced document id doesn’t exist.
Fix: Check the id from the POST /v1/documents response.
document_deleted
A referenced document has been deleted.
Fix: Re-upload it.
duplicate_document
The same document id appears more than once in one request.
Fix: De-duplicate document_ids.
document_over_size_cap
Over 500 pages or 100 MB. Rejected outright and not charged.
Fix: Split the file and submit the parts separately.
document_type_unresolvable
We couldn’t determine the document’s type.
Fix: Set declared_type explicitly. See
Preparing documents.
unsupported_asset_class
There’s no underwriting model for that asset class yet. The message lists
the supported ones.
Fix: Use a supported asset class, or ask us about the one you need.
asset_class_unsupported
The same thing, on POST /v1/underwritings. Underwriting covers fewer
asset classes than extraction reads: multifamily, self-storage,
industrial-outdoor-storage, industrial, office, retail, hotel.
Fix: Use one of those, or ask us about the one you need.
document_role_required
A document’s declared_type implies no underwriting role, so we can’t tell
what it’s for.
Fix: Set role on it. A seller’s financial model is the usual case —
it has no declared type of its own, so name it financial_model.
duplicate_document_role
Two documents claim the same role. A run reads at most one document per
role, so there’s no way to choose between them.
Fix: Send one document per role, or re-role the second as supporting.
document_format_unsupported
The file’s format can’t be read for the role it was given — an offering
memorandum must be a PDF, and a financial model must be a spreadsheet.
Fix: Check the role’s accepted formats in the API reference and either
convert the file or change its role.
end_user_ref_required
Your account requires end_user_ref on every run, so each of your users’
analyses lands in its own isolated workspace.
Fix: Pass end_user_ref — your own opaque identifier for that user.
rate_unavailable
No rate-card line exists for that document type on your account.
Fix: Contact api@trycactus.com — this is a
configuration gap on our side.
Run failures
These aren’t HTTP errors. An underwriting that ends badly reachesstatus: "failed" with an error.code, and anything billed is refunded in full.
underwriting_needs_review
The run reached a point it couldn’t resolve on its own and needs a human
analyst. There’s no report and no workbook.
Fix: Nothing to retry blindly — the usual cause is a document the
pipeline couldn’t reconcile. Send us the underwriting id and we’ll tell you
what stopped it.
underwriting_timeout
The run exceeded the 4-hour ceiling.
Fix: Retry. If it happens twice on the same documents, send us the ids.
underwriting_not_publishable
The run finished but its result couldn’t be released — the evidence behind
it didn’t clear our publication checks.
Fix: Send us the underwriting id; the blocking reasons are recorded on
our side.
Rate limit (429)
rate_limited
Too many requests.
Fix: Wait the number of seconds in the Retry-After header, then retry.
Server (500)
internal_error
Something failed on our side.
Fix: Retry. If it persists, send us the request_id from the response.
Jobs that fail after being billed are refunded in full, automatically.
A failed extraction or underwriting never costs you anything.