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 (scenarios 1–9) show the same household at each stage.
Happy path
0. Before any application. CORA has read the document type definitions (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 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 returns every requirement as AWAITING_UPLOAD (scenario 1).
3. CORA reads Maria’s information. Read application (GET /applications/{applicationId}), Read household (GET /households/{householdId}) and List customers (GET /customers) give her name, contact details, consent and language.
4. CORA nudges Maria. It sends Touchpoint 1 (Introduction) with 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, not from fixed values. Replies arrive as message.received.
5. Documents arrive. Maria uploads her government ID and a pay stub. Each upload sends 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 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 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 (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 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), then POST /applications/{applicationId}/cancel with reason: CADENCE_EXPIRED. Fortress cancels the application and sends application.completed with status: CANCELED. CORA stops.
B. Maria asks to cancel. At any point Maria replies to a touchpoint. Fortress sends message.received; CORA reads the message (Read message, GET /messages/{messageId}) and its intent is to cancel. CORA sends the final message, then POST …/cancel 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 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) and waits for her new upload. Maria uploads a new one and Fortress sends 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 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 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), and her reply arrives as message.received (read it with 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. 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 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: 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 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.

