> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.fortresstech.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.fortresstech.io/_mcp/server.

# CORA review flow

Maria Lopez (applicant `6e5d4c3b-…`) and James Lopez share application `9d8c7b6a-5e4f-4d3c-9b2a-1e0f9d8c7b6a`. Maria has two requirements: income (her pay stubs) and identity (a government ID). The [bundle examples](/api-reference/compliance-documents/get-application-bundle) (scenarios 1–9) show the same household at each stage.

| Item                 | Id                             |
| -------------------- | ------------------------------ |
| Income requirement   | `78b21158-…`                   |
| Identity requirement | `40d30ff4-…`                   |
| Pay stub Sep 12      | `998fa0b8-…` (run `run_9101A`) |
| Pay stub Sep 26      | `17bad985-…` (run `run_9102A`) |
| Government ID        | `8067d1fa-…` (run `run_9103A`) |

## Happy path

**0. Before any application.** CORA has read the [document type definitions](/api-reference/compliance-documents/get-compliance-documents-manifest) (`GET /compliance-documents/manifest`) and built one Blueprint per document type and `definitionVersion`. Fortress tells Fission Labs when a definition changes.

**1. Maria signs.** James signed earlier; Maria is the last adult. Fortress sends [`application.signed`](/webhooks/webhook-events/webhooks/application-signed) with `lastSignerApplicantId` = Maria. The bundle serves the current definition version for each document type. The version can change during an application when a definition is updated; compare `definitionVersion` to pick the Blueprint; a `definitionVersion` with no Blueprint: re-read the manifest. A document type with no approved definition has `definitionVersion: null`.

**2. CORA reads where things stand.** [`GET /compliance-documents/applications/{applicationId}/bundle?exclude=requirements.documentTypes`](/api-reference/compliance-documents/get-application-bundle) returns every requirement as `AWAITING_UPLOAD` (scenario 1).

**3. CORA reads Maria's information.** [Read application](/api-reference/applications/read-application) (`GET /applications/{applicationId}`), [Read household](/api-reference/households/read-household) (`GET /households/{householdId}`) and [List customers](/api-reference/customers/list-customers) (`GET /customers`) give her name, contact details, consent and language.

**4. CORA nudges Maria.** It sends Touchpoint 1 (Introduction) with [Send message](/api-reference/messaging/send-message) (`POST /messages`), listing what is missing. Names come from the bundle and the manifest: the requirement's `documentTypes` with `required: true` and no non-rejected document (`documents` empty or all `REJECTED`; the default read, with the document details excluded), named by the manifest `label` of each `documentTypeKey` (or by the bundle's own `label` with `include=requirements.documentTypes.label`). Each requirement lists the documents Fortress asks this applicant for (the same cards the applicant sees in MyFortress, optional ones with `required: false`), plus the type of any other document uploaded to it. While a requirement is `AWAITING_UPLOAD`, CORA keeps following the Nudging flow (Get Started, Momentum, …). The timing and channels of each touchpoint come from `cadencePolicy` on [`GET /agents/cora/state`](/api-reference/agents/get-agent-state), not from fixed values. Replies arrive as [`message.received`](/webhooks/webhook-events/webhooks/message-received).

**5. Documents arrive.** Maria uploads her government ID and a pay stub. Each upload sends [`requirement.upload_received`](/webhooks/webhook-events/webhooks/requirement-upload-received) with its `requirementId` and `documentId`; each document starts at `NOT_YET_REVIEWED`. After the first stub both of her requirements are `UPLOADED` (scenario 2).

**6. CORA checks that everything is uploaded.** On each `requirement.upload_received`, CORA re-reads the [bundle](/api-reference/compliance-documents/get-application-bundle) and looks at Maria's requirements only (`applicantId` = Maria). Once none is `AWAITING_UPLOAD` (from scenario 2, after her first stub), CORA starts the Document validation flow for Maria; a later stub (scenario 3) joins the review. The status follows the applicant's current answers and can change between events; re-read when in doubt.

**7. CORA tells Fortress it started.** For the first pay stub: [`POST /documents/{documentId}/results`](/api-reference/documents/post-document-results) with `status: EXTRACTING` and `reviewResult.runId: run_9101A` (example "1. Pickup"). The document is `EXTRACTING`; the income requirement becomes `IN_REVIEW`. CORA gets the file with [Read document download URL](/api-reference/documents/read-document-download-url) (`GET /documents/{documentId}/downloadUrl`) and takes the document type's `documentTypeId` and `definitionVersion` from the bundle (`requirements[].documentTypes[]`). The definition (fields, mappings, rules) is the manifest's for that `documentTypeId` + `definitionVersion`: the Blueprint from step 0. A `definitionVersion` with no Blueprint: re-read the manifest.

**8. Extraction results.** CORA runs the wrong item, legibility and completeness checks, then extracts the fields. `POST …/results` with `status: EVALUATING_RULES`, `extractedFields` and `reviewResult.quality` (example "2. Extraction"), same `runId`.

**9. Evaluation results.** CORA loads the application data (step 3) for the mappings, then runs the required fields, deterministic, fraud and conformance checks. Everything passes: `POST …/results` with `status: VALIDATED_BY_CORA`, `reasonCodes: []`, `reviewResult.checks` and `computed` (same shape as example "3. Evaluated rules", with every verdict `PASS`). Same `runId`, so it merges with step 8.

**10. CORA repeats steps 7–9** for the second pay stub (`run_9102A`) and the government ID (`run_9103A`).

**11. Fortress derives the statuses.** Fortress derives the requirement status each time the bundle is read. CORA acts on each document's status; the requirement status is the summary (the most urgent state first). A requirement can be `WAITING_FOR_COMPLIANCE` while one of its documents is `WAITING_FOR_APPLICANT`, so nudge and pick up work per document. Here: once her stubs are `VALIDATED_BY_CORA`, the income requirement is `VALIDATED`; once the government ID is, the identity requirement is `VALIDATED`. All of Maria's requirements are `VALIDATED`. `…/bundle?hideDocumentTypesOnValidatedRequirement=true` now returns her requirements without their document types.

**12. The application moves on.** When James's requirements are `VALIDATED` too (CORA runs the same steps for him), every requirement of the application is `VALIDATED` (scenario 7). Fortress moves the application to its next step. When it reaches a final status, Fortress sends [`application.completed`](/webhooks/webhook-events/webhooks/application-completed) and CORA stops.

## Unhappy paths

Each path starts from a step of the happy path and ends by rejoining it, or by stopping.

**A. Nudging expires.** After step 4 Maria uploads nothing. CORA keeps sending the touchpoints the property's `cadencePolicy` lists, at their offsets, and nothing in between; outreach ends at the last touchpoint (or at a `SITE_TASK` one). After longer than the property's `cadencePolicy.expireAfterLastTouchpoint` (7 days by default) without interaction after the last touchpoint, CORA sends the final message ([Send message](/api-reference/messaging/send-message)), then [`POST /applications/{applicationId}/cancel`](/api-reference/applications/cancel-application) with `reason: CADENCE_EXPIRED`. Fortress cancels the application and sends [`application.completed`](/webhooks/webhook-events/webhooks/application-completed) with `status: CANCELED`. CORA stops.

**B. Maria asks to cancel.** At any point Maria replies to a touchpoint. Fortress sends [`message.received`](/webhooks/webhook-events/webhooks/message-received); CORA reads the message ([Read message](/api-reference/messaging/read-message), `GET /messages/{messageId}`) and its intent is to cancel. CORA sends the final message, then [`POST …/cancel`](/api-reference/applications/cancel-application) with `reason: APPLICANT_REQUESTED`. Same end as A. If the reply is not a cancellation, the days-without-interaction counter resets and nudging continues.

**C. Extraction fails, re-upload needed.** At step 8 the government ID photo is too blurry to read. CORA rejects it: [`POST …/results`](/api-reference/documents/post-document-results) with `status: REJECTED`, `reasonCodes: [UNREADABLE]` and `reviewResult.quality.legibility: fail` (no `extractedFields`). The rejected document stays in the bundle as history only and no longer counts, so Fortress derives the identity requirement as `AWAITING_UPLOAD`. CORA asks Maria for a clearer copy ([Send message](/api-reference/messaging/send-message)) and waits for her new upload. Maria uploads a new one and Fortress sends [`requirement.upload_received`](/webhooks/webhook-events/webhooks/requirement-upload-received) for it. The requirement is `UPLOADED`, and CORA reviews the new document from step 7 with a new `runId`.

**D. Evaluation fails, re-upload needed.** At step 9 the Sep 12 pay stub is older than 120 days: rule R5 fails with `onFail: reupload`. CORA rejects it: [`POST …/results`](/api-reference/documents/post-document-results) with `status: REJECTED`, `reasonCodes: [R5]` and the R5 check as `FLAG` with its `applicantMessage` (example "3. Evaluated rules"). The rejected stub no longer counts; the requirement goes back to `AWAITING_UPLOAD` when nothing else counts. CORA sends Maria the `applicantMessage` and waits for her new upload. As in C, Maria uploads a recent stub (`requirement.upload_received`), the requirement is `UPLOADED`, and CORA reviews it from step 7 with a new `runId`.

**E. Evaluation partly fails, clarification needed.** At step 9 the date of birth on the government ID does not match the application: mapping GM2 fails with `onFail: clarification`. CORA writes [`POST …/results`](/api-reference/documents/post-document-results) with `status: WAITING_FOR_APPLICANT` and `reasonCodes: [GM2]`: CORA is waiting for Maria's clarification. Fortress derives the identity requirement as `WAITING_FOR_APPLICANT`. The conversation happens in the message thread: CORA sends her the mapping's `applicantMessage` ([Send message](/api-reference/messaging/send-message)), and her reply arrives as [`message.received`](/webhooks/webhook-events/webhooks/message-received) (read it with [Read message](/api-reference/messaging/read-message)). The clarification stays in the message thread in Fortress, where compliance reads it. When the clarification is ready for compliance, CORA writes `POST …/results` with `status: WAITING_FOR_COMPLIANCE` and `reasonCodes: [GM2]`, and stops working on that document. Fortress derives the identity requirement as `WAITING_FOR_COMPLIANCE`. Compliance decides, as in F. CORA carries on with Maria's other documents.

**F. A human takes over mid-process.** While CORA is evaluating the Sep 26 stub (`EVALUATING_RULES`), staff open a manual compliance review: Fortress sends [`compliance_review.opened`](/webhooks/webhook-events/webhooks/compliance-review-opened). While it is open, the bundle shows the documents it covers as `WAITING_FOR_COMPLIANCE` with `statusSetBy: fortress_user`, and their requirements as `WAITING_FOR_COMPLIANCE` (unless a required document type has no document, which keeps `AWAITING_UPLOAD`), whoever set the stored status: a human can always take over, an earlier human `APPROVED` or `REJECTED` included. These statuses are not final decisions: they last only while the review is open. CORA's next write gets `409 COMPLIANCE_REVIEW_OPEN`: Fortress refused it (the cancel route and the Results API both enforce it). CORA stops, and waits for the review to close. Staff close the review with a decision: [`compliance_review.closed`](/webhooks/webhook-events/webhooks/compliance-review-closed) carries it. `APPROVED` sets every covered document `APPROVED` (for example the stub and the government ID from E), `DENIED` sets them `REJECTED`, both with `statusSetBy: fortress_user`; `NONE` changes no document, which shows its stored status again. `decidedDocumentIds` lists the documents a decision set; `APPROVED` and `DENIED` clear their `reasonCodes` and keep CORA's `extractedFields` and `reviewResult` as history. After `DENIED` the covered documents no longer count: a requirement goes back to `AWAITING_UPLOAD` when nothing else counts or a required document type is left without a counting document, and CORA nudges the applicant for a new upload as in C. CORA re-reads the [bundle](/api-reference/compliance-documents/get-application-bundle): documents a human set (`statusSetBy: fortress_user`) are final for CORA, requirements whose documents are all `VALIDATED_BY_CORA` or `APPROVED` are `VALIDATED`, and any document still `NOT_YET_REVIEWED` or left mid-run is picked up again at step 7 with a new `runId`.

## Rejoining the happy path

Whenever Fortress refuses a write with `409` (`DOCUMENT_REMOVED`, `COMPLIANCE_REVIEW_OPEN`, `STATUS_SET_BY_USER` or `DOCUMENT_ALREADY_VALIDATED` on the Results API; `COMPLIANCE_REVIEW_OPEN` on the cancel route too), CORA stops, re-reads the [bundle](/api-reference/compliance-documents/get-application-bundle) and continues from the state Fortress returns.

In every case Fortress keeps deriving the requirement statuses. Once all of Maria's requirements are `VALIDATED` (by CORA, by compliance, or both), she is at step 11.