Skip to main content
Errors are RFC 9457 problem documents, served as application/problem+json:
Branch on 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 reaches status: "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.