1. Register and upload each document
For each file, callPOST /v1/documents with its filename, declared_type,
and exact byte_size. Save the returned id, then PUT the file’s raw
bytes to upload.url, using the returned upload headers. Registration alone
does not upload the file. See the upload walkthrough.
Repeat for the rent roll, T-12, and any other documents. You do not need
to call /v1/extractions first; underwriting reads the documents itself.
Already uploaded documents can be reused in later runs.
2. Start the underwriting
Senddocuments as objects containing a document_id and its role.
There is no document_ids field. Each role corresponds to a document slot
on the app’s setup page.
At least one document is required, with at most one file per role and no
file selected twice. Rent rolls, T-12s, and supporting documents accept PDF,
XLS, XLSX, and XLSM; financial models accept spreadsheets only; offering
memoranda accept PDF only. Occupancy Report and Management Summary remain
coming-soon slots in the app; they are not separate API roles.
Prefer explicit roles. If omitted, the API infers a role from the uploaded
document’s
declared_type. To upload a financial model, register it as
other_standard or other_complex, then select role: "financial_model"
here. The document’s declared type and its role in a particular run are
separate fields.
Return targets are not exposed by this API. Use options.delivery: "private"
for an embedded integration: end_user_ref is required, notification_email
is optional, and completion creates no public link and sends no email.
The default, public_link, requires notification_email and creates and
emails a public report link. Supply a stable opaque end_user_ref to isolate
each customer’s analyses. Reuse the same idempotency key when retrying the
same underwriting request; change it for a new run.
3. Wait for the result
The response is202 Accepted with an uw_... ID. Poll
GET /v1/underwritings/{id} using poll_after_seconds, or receive
underwriting.completed / underwriting.failed through a
webhook. Check both terminal statuses.
On completion, the response includes structured results and
report.excel_url; report.url is included only for public-link delivery. Call GET /v1/underwritings/{id}/model for a fresh
workbook download URL. On failure, inspect error for the reason.
Sandbox keys return example results without running the AI workflow or
sending email. Use a live key to validate actual document processing.
Customer ownership and access
Reuse the sameend_user_ref for every underwriting belonging to a customer.
Each run receives a separate uw_... ID. Store both in your backend:
References are scoped to your partner account, so another partner using the
same reference does not share your customer’s organization. If a customer
is a company with several users sharing analyses, use a stable customer
account identifier and enforce individual users’ permissions in your app.
end_user_ref and the underwriting ID are identifiers, not credentials.
Your API key authorizes access to your partner’s underwritings, including
those belonging to different customers. Keep it on your server. Derive the
customer reference from your authenticated session and verify a run’s
ownership before returning it to a browser. The list filter is a convenience;
it does not authenticate an end user. The single-run GET endpoint currently
checks partner ownership, not a caller-supplied end-user reference.
For integrations serving multiple customers, ask Cactus to enable
requires_end_user_ref on your partner account. This rejects starts without
a customer reference; it does not replace authorization when viewing a run.
Embed a private report
Your backend authenticates the customer and checks their permission to view this underwriting. It then requests a session with your server-side API key:underwriting scope. Sandbox example underwritings
have no underlying app report and cannot be embedded.
The 201 response contains id, underwriting_id, url,
launch_expires_at, and session_ttl_seconds (900). Return only the launch
URL and session ID to your authorized browser, with Cache-Control: no-store.
Set the iframe’s src to that URL immediately:
end_user_ref
from your authenticated session rather than trusting browser input. The
registered origin restricts framing; it does not authenticate a customer.
This is the app’s eight-chapter report in a read-only presentation. Editing,
AI follow-up, and file downloads are not exposed in the iframe. Your backend
can still use the authenticated model endpoint if you want to provide a
separate download. A new session reads the current report, following any
completed revisions in the same report family; an already open frame keeps
its loaded snapshot.
A page reload requires a new launch URL because the browser credential is
held only in memory. Session creation deliberately does not replay an
idempotent response: each successful request creates a new single-use ticket.
Keep an authorized viewer’s report open
The report can stay open indefinitely while your application continues to validate the viewer. Your backend renews its access lease every five minutes:200 with id and expires_at. The same iframe and token
keep working; no navigation, report reload, or scroll reset occurs. The frame
observes the renewed deadline on its normal access checks. Renewal cannot
revive a revoked session or extend an unused launch ticket.
Implement a small authenticated route in your backend which:
- Reads the currently signed-in customer and rechecks their permission to view the underwriting. Never trust a browser-supplied customer reference.
- Verifies that the requested embed session belongs to that customer/run.
- Calls the renewal endpoint using your server-side Cactus key and the configured parent origin.
204. Every subsequent access is rejected. The frame checks
access every 15 seconds and clears the report on revocation or expiry
(browser background throttling can delay the visual update). Disabling the
partner, revoking/expiring the originating key, removing its scope or the
registered origin also invalidates access. Revocation cannot erase data a
viewer has already copied or captured.
Existing report.url links remain public bearer links, readable by anyone
holding them until sharing is revoked. Creating or revoking an embed session
does not disable an existing public link. Use private delivery when starting
new underwritings to avoid creating one.
Test in the admin console
Open API Console → Embedded report, choose a completed underwriting with an end-user reference, and select Open embedded report. The console calls the public session API from its server and loads the returned URL in a real iframe. Revoke session leaves the frame open so you can see its access check clear the report. Sessions renew through the console’s authenticated server action while the page remains open, without reloading the iframe. Changing the customer reference demonstrates the ownership check (404). New underwritings in the console default to private delivery.Shared results components
The embed composesBrokerSharedReport / SharedDealJourneyWorkspace from
the same eight production chapter components used by the signed-in result
workspace. Chapter layouts, charts, tables, and presentation fixes therefore
carry through after deployment. It does not copy a separate set of chapters.
Owner-only controls (editing, Sage, sharing, history) remain outside this
composition. New data fields need to be explicitly added to the safe shared
report projection before the embed can display them.