CORA
The document-review agent in the Fortress integration
CORA reviews the compliance documents of an application once every adult applicant has signed. It nudges each applicant to upload what is missing, checks each uploaded document, and writes the result back to Fortress. Fortress keeps the application moving and tells CORA when to look.
The bundle model
- Definitions upfront.
GET /compliance-documents/manifestreturns every document type with its definition (fields, mappings, rules) before any upload, independent of any application. Definitions are versioned per document type (definitionVersion), so CORA builds one Blueprint per version. The bundle carries each document type’sdefinitionVersion, not its content. Fortress tells Fission Labs when a definition changes; there is no event for it. - One read of the application’s data.
GET /applications/{applicationId}/fullreturns the application from Fortress’ own database (no search index, no sync delay): application, household, every live applicant with customer, addresses, jobs, spouse, minor record,incomes[]andassets[]lines, deductions, and (withsensitive: read) the encrypted sensitive values; only the values the taxonomy compares documents against. The sections follow the API key’s grants. The manifest’s mappings name the path in this response that each document value is compared against. - One read of the documents.
GET /compliance-documents/applications/{applicationId}/bundlereturns requirements → document types → documents, with each document’s status, extracted fields and review result, and each document type’sdocumentTypeKeyanddefinitionVersion(its label, taxonomy domain, recognition text and definition content only withinclude; the manifest is the dictionary for them). The bundle serves the current definition version for each document type. The version can change during an application when a definition is updated; comparedefinitionVersionto pick the Blueprint; adefinitionVersionwith no Blueprint: re-read the manifest. A document type with no approved definition hasdefinitionVersion: null. - CORA writes document status. Every status CORA sets goes through
POST /documents/{documentId}/results, with or without extracted fields and review results. - Fortress derives requirement status. Fortress derives the status of each requirement from its documents every time the bundle is read. CORA never writes it.
- Events tell CORA when to look. What CORA does depends on the event. See Events.
application.signed,requirement.upload_received,requirement.upload_removed,compliance_review.closed: re-read the bundle.compliance_review.opened: stop writing untilcompliance_review.closed; meanwhile the cancel route and the Results API return409 COMPLIANCE_REVIEW_OPEN.- The bundle’s
complianceReviewcarries the open session, ornullwhen none is open (a closed review is gone from the bundle;compliance_review.closedreports the close): check it on every read, and use it to recover a missedcompliance_review.openedorcompliance_review.closed. agent.state_changed: pause when the agent is disabled, resume when it is enabled again. Also re-readGET /agents/cora/stateand apply the newcadencePolicyfrom the next touchpoint.message.received: read the applicant’s message.application.completed: stop all work on the application.
The Review Flow walks through one application from signature to completion. The API Reference lists every route CORA calls.
Statuses
Document status. Set on each document; who writes each one:
Requirement status. Fortress derives it from the requirement’s non-rejected documents (REJECTED documents are history only and never count) and the documents Fortress asks the applicant for. The rules read the statuses the bundle shows, so a stored REJECTED document that an open manual compliance review covers shows WAITING_FOR_COMPLIANCE and counts until the review closes. The first rule that matches wins, blockers first:
AWAITING_UPLOAD: no non-rejected document counts, or a document Fortress asks this applicant for, and that is not optional (for example the government ID for an adult’s identity), has none.WAITING_FOR_COMPLIANCE: a counting document isWAITING_FOR_COMPLIANCE, whether CORA set it or an open manual compliance review covers it. While a review is open, the bundle shows the documents it covers asWAITING_FOR_COMPLIANCEwithstatusSetBy: fortress_user, an earlier humanAPPROVEDorREJECTEDincluded; closing the review shows the stored statuses again.WAITING_FOR_APPLICANT: a document isWAITING_FOR_APPLICANT.UPLOADED: a document isNOT_YET_REVIEWED.IN_REVIEW: a document isEXTRACTINGorEVALUATING_RULES.VALIDATED: every non-rejected document isVALIDATED_BY_CORAorAPPROVED, and every required document type (required: true) has one.
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. Fortress is still aligning the requirement status precedence with Fission Labs (CORA).
Application status. There is no separate CORA status field on the application. Fortress can derive an application-level CORA status from the requirement statuses of the application.
When Fortress refuses a write
Fortress can refuse a CORA write with 409, first match wins: DOCUMENT_REMOVED, COMPLIANCE_REVIEW_OPEN (a manual compliance review is open; the cancel route answers it too), STATUS_SET_BY_USER (a Fortress user set the document’s status), or DOCUMENT_ALREADY_VALIDATED (the document is already VALIDATED_BY_CORA). Nothing is written. CORA must handle it: stop, re-read the bundle and continue from the state Fortress returns. A document a human set (statusSetBy: fortress_user) is final for CORA, except the temporary WAITING_FOR_COMPLIANCE the bundle shows while a manual compliance review is open (complianceReview set): after compliance_review.closed the stored status shows again.
Authentication
Every call sends the organization’s API key in the x-api-key header. One key maps to one organization, server to server only. 401 without an x-api-key (gateway) or when Fortress refuses a key that passed it; 403 when the key is unknown or lacks the permission. See Authentication.
Events are not authenticated by the API key: each delivery is signed with the X-Fortress-* headers. See Webhooks.
Cadence policy
The timing of CORA’s nudges comes from cadencePolicy on GET /agents/cora/state?propertyId=, not from fixed values in CORA. It is the property’s stored policy, or the default (the baseline seed) when the property never set one or has no CORA configuration. isFallback is read-only: true for that default, false for a stored policy. Staff set it in Fortress; there is no partner write. CORA reads it on startup and again on each agent.state_changed, and applies a change from its next touchpoint.
touchpoints[]:offset(daysand/orhours, strictly increasing),channels(SMS,CALL,SITE_TASK) and an optionalpurpose. At aSITE_TASKtouchpoint CORA stops outreach and raises no task.quietHours(optional):start,end(HH:mm) and an IANAtimezone. A touchpoint inside quiet hours is deferred toend.expireAfterLastTouchpoint(daysand/orhours, total above 0): how long after the last touchpoint, without any applicant interaction, CORA waits before it cancels withCADENCE_EXPIRED. Always returned; a stored policy that did not set it reads as 7 days.
The touchpoints are the whole cadence. The scope is fixed. There is one cadence per household. The clock starts at application.signed. An applicant response or upload (requirement.upload_received) does not stop the cadence; it restarts the clock at offset 0. Each touchpoint sends one message per household that covers all outstanding requirements.
The rest of CORA’s outreach behaviour is fixed, not part of the policy:
- CORA stops the cadence when the application is complete (every requirement
VALIDATED) or canceled, denied or abandoned. - When a channel fails (for example an invalid phone number), CORA may move on to the next channel or touchpoint.
- CORA runs the cadence whenever the agent is enabled (
isEnabled). The policy has no on/off of its own.
Conventions
- All routes are under
https://api.fortresstech.io/v1. - Field names are camelCase. Ids of Fortress resources (organization, property, household, application, applicant, requirement, document type (
documentTypeId), document, staff user (actorId), session) are UUIDs.runId,checkId,ruleId,mappingIdandapplicationVersionare plain strings. - Omitted,
nulland[]mean different things. A property left out with the bundle’sexcludeparameter is omitted, not returned empty.nullmeans there is no value, for exampledefinitionVersion: nullfor a document type with no approved definition.[]is an empty list, for examplereasonCodes: []when every check passes, ordocuments: []for a requirement with no upload yet. - Every event shares the same envelope (
eventId,eventType,occurredAt,data). See The event envelope.
More in Conventions.
Still aligning with Fission Labs (CORA)
- The cadence policy: its shape, the seed values, and re-reading it on
agent.state_changed(contract v0.20 namesagent.property_state_changed). - Where the policy is served: on
GET /agents/cora/state, not on the dedicatedGET /properties/{propertyId}/agents/cora/cadencePolicyof contract v0.20. - A property with no stored policy: Fortress serves the default with
isFallback: true, where contract v0.20 answersnulland back-fills the seed on existing rows. isFallbackandexpireAfterLastTouchpoint, which contract v0.20 does not define.- The cadence clock starts at
application.signed(contract v0.20 namesapplication.activated, an event Fortress does not send). - When outreach stops is fixed, not a policy field (contract v0.20 has a configurable
stopOn). consolidateRequests,invalidChannelAccelerationandisActivefrom contract v0.20 are fixed behaviour, not policy fields (isActiveis the agent’sisEnabled).- No
deadlineCompression: the touchpoints define the whole cadence and no deadline is exposed to CORA, so contract v0.20’sdeadlineCompressionis dropped. - No daily possible-cancellation touchpoint: CORA sends only the touchpoints the policy lists, then cancels with
CADENCE_EXPIREDafterexpireAfterLastTouchpoint.
Planned (TBD)
Not defined in these docs yet:
- Engagement read endpoint (MVP, not in the first wave). One read that tells CORA whether to engage for an application, resolving the global, property and household levels in one call.
agent.state_changedcarriesPROPERTYscope today, andGET /agents/{agentKey}/statereads the property switch, so the engagement read covers only the household level. - Touchpoint metadata (post-MVP). Recording the metadata of each touchpoint CORA sends, through a route separate from
POST /messages.

