Loan Boarding
Originating a loan through ORCA. The workflow covers borrower onboarding (personal and commercial), loan origination with collateral inline for secured loans, attaching additional collateral after origination, linking an externally managed escrow account for mortgages, and uploading loan documentation.
Business context
Lending platforms (LOS, point-of-sale lending tools, BNPL providers, mortgage originators) need to push originated loans into the financial institution's Core for servicing. Each Core has its own loan record format, collateral model, and rate-structure conventions; building a bespoke boarding bridge per Core is expensive and forces the lender to know each Core intimately.
ORCA exposes /loans as a single normalized REST resource. The fintech submits one ISO-20022-aligned LoanRequest and ORCA translates into the target Core. The result is a fully boarded, static loan account that the Core can then service: accrue interest, post payments, generate statements.
Scope
In-scope for this use case (Project 1 — Loan Boarding):
- Borrower search & creation —
Person(personal loans) andOrganization(commercial loans) - Loan origination — personal, installment, mortgage, home equity, commercial, line of credit, revolving credit, lease, etc.
- Inline collateral for secured loans (real estate, vehicles, securities, equipment, guarantees, etc.) including liens and identifiers
- Additional collateral added after origination via
PATCH /loans/{loanId} - Escrow account linkage — link an externally managed escrow account to the primary loan via
relatedAccounts[] - Mortgage-specific attributes — HMDA reporting, census tract, CBSA code, FHA fields
- Interest structure — fixed, variable, tiered, step-up rates via
interests[] - Loan-specific classifiers —
LoanPurpose,RiskRating,LendingRelationType - Loan documentation — promissory notes, deeds, appraisals, title docs, closing disclosures, UCC filings via the Documents API
- Custom data — flex fields via
supplementaryData
Out-of-scope (handled by other projects or outside ORCA):
- Loan funding / disbursement — Project 2 (Loan Funding). ORCA boards the static loan record; actual money movement (GL entries, ACH, wire) happens via a separate project.
- Document archival to cold storage — Project 3 (Document Movement). The Documents API handles upload and linkage; cold-storage archival from the LOS is a separate pipeline.
- Loan underwriting and credit decisioning — happens upstream in the LOS before boarding
- Escrow analysis, RESPA reporting, escrow disbursement — handled by the servicer's mortgage servicing system, not by ORCA
- Modifications to the ORCA specification — the spec is fixed
Architecture context
ORCA exposes a unified /loans resource. Collateral is embedded in the loan request body (not a separate endpoint); additional collateral is added via merge patch. Escrow accounts are typically opened in the servicer's back-office or sub-ledger system and linked into ORCA via the loan's relatedAccounts[]. ORCA itself is stateless — the Core is the system of record for loan, collateral, and balance data.
sequenceDiagram
autonumber
participant FT as Fintech
participant ORCA as ORCA API
participant Core as FI Core
FT->>ORCA: GET /persons OR /organizations
ORCA->>Core: Borrower lookup
Core-->>ORCA: Match / no match
ORCA-->>FT: 200 [Descriptor[]]
alt Borrower not found
FT->>ORCA: POST /persons OR /organizations
ORCA->>Core: Create borrower
Core-->>ORCA: customerId
ORCA-->>FT: 202 {partyId}
end
Note over FT,Core: Step 2: Originate the loan<br/>(secured loans include collateral inline in the body)
FT->>ORCA: POST /loans (LoanRequest with contract.collateral)
ORCA->>Core: Originate loan + register collateral
Core-->>ORCA: loanAccountNumber, collateralIds
ORCA-->>FT: 202 {loanId, contract.collateral.collaterals[]}
opt Add or update collateral later
FT->>ORCA: PATCH /loans/{loanId} (merge-patch contract.collateral)
ORCA->>Core: Update collateral records
Core-->>ORCA: Updated
ORCA-->>FT: 200 {Loan}
end
opt Link an externally managed escrow account
Note over FT: Escrow account exists in the servicer's<br/>back-office / sub-ledger system
FT->>ORCA: PATCH /loans/{loanId} (add relatedAccounts[] entry)
ORCA->>Core: Persist link
Core-->>ORCA: Updated
ORCA-->>FT: 200 {Loan}
end
opt Servicing reads
FT->>ORCA: GET /loans/{loanId}/balances
FT->>ORCA: GET /loans/{loanId}/transactions
end
opt Documentation (alternative path — can run in parallel)
FT->>ORCA: POST /documents (with linkages[] → loan)
ORCA->>Core: Store + index document
Core-->>ORCA: documentId, processing status
ORCA-->>FT: 201 {Document}
FT->>ORCA: GET /documents?resourceType=loan&resourceId=...
FT->>ORCA: GET /documents/{id}/download
end
Project decoupling
The full Loan Boarding capability is structured into three projects:
- Project 1 — Loan Boarding (this page): borrower, loan record, collateral, escrow linkage.
- Project 2 — Loan Funding: disbursement of principal via GL entries, ACH, or wire transfers.
- Project 3 — Document Movement: archival of loan documentation from the LOS into long-term storage.
This page documents Project 1. Funding and document archival are referenced where relevant.
Endpoints used
| Step | Method | Path | Purpose |
|---|---|---|---|
| 1a | GET |
/persons |
Search for a borrower (personal loan) |
| 1a | POST |
/persons |
Create a borrower (personal loan) |
| 1b | GET |
/organizations |
Search for a borrower (commercial loan) |
| 1b | POST |
/organizations |
Create a borrower (commercial loan) |
| 2 | POST |
/loans |
Originate the loan (with inline collateral for secured loans) |
| 2 | GET |
/loans/{loanId} |
Retrieve loan detail |
| 3 | PATCH |
/loans/{loanId} |
Add or update collateral / link escrow / general updates |
| 4 | GET |
/loans/{loanId}/balances |
Retrieve current balances |
| 4 | GET |
/loans/{loanId}/transactions |
Retrieve transaction history |
| 5 | POST |
/documents |
Upload loan documentation (Documents API) |
| 5 | GET |
/documents |
List documents linked to a loan |
| 5 | GET |
/documents/{id} |
Retrieve document metadata |
| 5 | GET |
/documents/{id}/download |
Download or get a signed URL |
| 5 | PATCH |
/documents/{id} |
Update document metadata |
| 5 | POST |
/documents/{id}/share |
Create a shareable URL |
| 5 | POST |
/documents/export |
Start a bulk export job |
What is NOT in the spec
Earlier drafts of the proposal referenced POST /loans/{loanId}/collateral and POST /loans/{loanId}/escrow as standalone endpoints. Neither exists in the current ORCA spec. Collateral is inline; escrow is linked via relatedAccounts[].
Common headers
| Header | Purpose |
|---|---|
idempotencyId |
Critical for loan operations to prevent duplicate originations. |
servicerId |
Identifies the servicing institution. |
Core-Validation-Mode |
ENFORCING (default), PERMISSIVE, or DISABLED. Controls whether ORCA pre-validates against the Core's policy before forwarding. |
Step 1 — Search or create the borrower
Identical to Account Opening. Search first, create only if no match.
For personal loans, the borrower is a Person → use personId.
For commercial loans, the borrower is an Organization → use organizationId.
The resulting identifier is used as partyId in the loan's parties[] array with partyAccountRole: "Borrower".
Step 2 — Originate the loan
POST /loans → 202 Accepted with Loan in the body. For secured loans, the collateral is sent inline as part of the request body — there is no separate collateral endpoint.
Loan vs. account
In ORCA, a loan is a specialized account exposed at /loans rather than /accounts. It shares many fields with deposit accounts — parties[], ownershipType, productId, currency, relatedAccounts[], supplementaryData — but adds loan-specific structures: contract, interests[], mortgage, recourseType, lendingRelationType, securedLoanIndicator.
Key request fields (LoanRequest)
| Field | Required | Description |
|---|---|---|
loanType |
yes | Personal, InstallmentLoan, MortgageLoan, HomeEquityLoan, CommercialLoan, LineOfCredit, RevolvingCredit, Lease, LoanInProcess, EscrowConstruction, Escrow. |
ownershipType |
yes | SingleOwnerAccount, JointAccount, BusinessAccount. |
parties[] |
yes | At minimum one entry with partyAccountRole: "Borrower". |
productId |
conditional | Core-defined loan product. |
securedLoanIndicator |
conditional | true for secured loans. Pair with contract.collateral. |
currency |
no | Defaults to USD. |
contract |
recommended | effectiveDate, maturityDate, principal amount, term, collateral (inline, for secured loans), interests[], payment schedule. |
interests[] |
recommended | Top-level interest calculations — typically use contract.interests instead. |
mortgage |
conditional | Mortgage-specific fields (HMDA, FHA, census tract, CBSA code). |
recourseType |
no | FullRecourse, PartialRecourse, NoRecourse, FullRepurchase, etc. |
lendingRelationType |
no | Direct, Dealer, Indirect, BoughtParticipation. |
relatedAccounts[] |
no | Links to other accounts (escrow, funding source, etc.). |
codes[] |
no | Loan-specific classifiers — LoanPurpose, RiskRating. |
supplementaryData |
no | Flex fields. |
Inline collateral structure
For secured loans, contract.collateral is a LoanCollateral object:
contract:
collateral:
totalValue: "<total market value across all pledged assets>"
loanToValueRatio: "<computed LTV>"
requiredPledge: "<minimum required value>"
collaterals[]: # array of Collateral objects, one per asset
- collateralType: "<ResidentialBuilding | Automobile | ...>"
value: "..."
netAppraisedValue: "..."
lastAppraisedDate: "..."
liens[]: [...]
postalAddress: {...} # for real estate
vehicleInformation: {...} # for vehicles
identifiers[]: [...]
status: "Secured"
ISO 20022 — Loans as accounts
Many Core systems represent loans as a particular accountType. ORCA preserves that lineage but exposes loans at their own endpoint family for clarity.
Party-account roles for loans
Borrower, CoBorrower, Guarantor, Cosigner. The borrower role is required.
Personal installment loan example (unsecured)
POST /loans HTTP/1.1
Host: api.portx.io
Authorization: Bearer <jwt>
Content-Type: application/json
idempotencyId: a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d
servicerId: bank-001
{
"loanType": "InstallmentLoan",
"ownershipType": "SingleOwnerAccount",
"productId": "PROD-LOAN-PERSONAL-36M",
"currency": "USD",
"securedLoanIndicator": false,
"parties": [
{
"partyId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"partyAccountRole": "Borrower",
"name": "Jane M Doe"
}
],
"contract": {
"contractType": "Regular",
"amount": "15000.00",
"effectiveDate": "2026-06-01",
"maturityDate": "2029-06-01",
"term": { "unit": "Months", "value": 36 },
"interests": [
{
"name": "RegularRate",
"rateType": "Fixed",
"dayCountBasis": "Actual365",
"rates": [
{ "rateName": "Actual", "percentage": "8.50" }
]
}
]
},
"lendingRelationType": "Direct",
"codes": [
{ "codeType": "LoanPurpose", "value": "DebtConsolidation" },
{ "codeType": "RiskRating", "value": "A-Grade" }
],
"supplementaryData": {
"losReferenceId": "LOS-2026-04823",
"underwriterId": "uw-145"
}
}
{
"loanId": "loan-a8b9c0d1-2e3f-4a5b-6c7d-8e9f0a1b2c3d",
"accountNumber": "LN-1000234567",
"loanType": "InstallmentLoan",
"status": "Approve",
"ownershipType": "SingleOwnerAccount",
"openDate": "2026-06-01",
"contract": {
"amount": "15000.00",
"effectiveDate": "2026-06-01",
"maturityDate": "2029-06-01"
}
}
Mortgage example with inline collateral (secured)
{
"loanType": "MortgageLoan",
"ownershipType": "JointAccount",
"productId": "PROD-LOAN-MORTGAGE-30Y",
"currency": "USD",
"securedLoanIndicator": true,
"parties": [
{
"partyId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"partyAccountRole": "Borrower",
"name": "Jane M Doe"
},
{
"partyId": "person-8a4b9d03-5c2f-4e3a-8b1d-2c3d4e5f6a71",
"partyAccountRole": "CoBorrower",
"name": "John Q Smith"
}
],
"contract": {
"contractType": "Regular",
"amount": "425000.00",
"effectiveDate": "2026-06-15",
"maturityDate": "2056-06-15",
"term": { "unit": "Years", "value": 30 },
"interests": [
{
"name": "RegularRate",
"rateType": "Fixed",
"dayCountBasis": "Actual360",
"rates": [
{ "rateName": "Actual", "percentage": "6.875" }
]
}
],
"collateral": {
"totalValue": "475000.00",
"loanToValueRatio": "0.89",
"requiredPledge": "425000.00",
"collaterals": [
{
"collateralType": "ResidentialBuilding",
"value": "475000.00",
"netAppraisedValue": "470000.00",
"lastAppraisedDate": "2026-05-12",
"postalAddress": {
"addressType": "Residential",
"streetName": "Oak Ridge Dr",
"buildingNumber": "8821",
"townName": "Round Rock",
"countrySubDivision": "TX",
"postCode": "78664",
"country": "US"
},
"liens": [
{ "name": "FirstMortgage", "amount": "425000.00" }
],
"identifiers": [
{ "schemeName": "CollateralNumber", "number": "COL-2026-0001" }
],
"status": "Secured"
}
]
}
},
"mortgage": {
"censusTract": "0203.04",
"cBSACode": "12420",
"hMDAReportingIndicator": true,
"hMDAOccupationStatus": "PrincipalResidence",
"hMDAPropertyType": "OneToFourFamily",
"propertyPurpose": "Purchase"
},
"recourseType": "FullRecourse",
"lendingRelationType": "Direct",
"codes": [
{ "codeType": "LoanPurpose", "value": "Purchase" }
]
}
{
"loanId": "loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e",
"accountNumber": "LN-1000234568",
"loanType": "MortgageLoan",
"status": "Approve",
"ownershipType": "JointAccount",
"contract": {
"amount": "425000.00",
"collateral": {
"collaterals": [
{
"collateralId": "col-c0d1e2f3-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"collateralType": "ResidentialBuilding",
"status": "Secured"
}
]
}
}
}
Auto loan example (secured by vehicle)
{
"loanType": "InstallmentLoan",
"ownershipType": "SingleOwnerAccount",
"productId": "PROD-LOAN-AUTO-60M",
"currency": "USD",
"securedLoanIndicator": true,
"parties": [
{
"partyId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"partyAccountRole": "Borrower",
"name": "Jane M Doe"
}
],
"contract": {
"contractType": "Regular",
"amount": "28000.00",
"effectiveDate": "2026-06-20",
"maturityDate": "2031-06-20",
"term": { "unit": "Months", "value": 60 },
"interests": [
{
"name": "RegularRate",
"rateType": "Fixed",
"dayCountBasis": "Actual365",
"rates": [{ "rateName": "Actual", "percentage": "7.25" }]
}
],
"collateral": {
"totalValue": "32500.00",
"loanToValueRatio": "0.86",
"collaterals": [
{
"collateralType": "Automobile",
"value": "32500.00",
"netAppraisedValue": "31800.00",
"lastAppraisedDate": "2026-05-15",
"vehicleInformation": {
"vin": "1HGCM82633A123456",
"make": "Honda",
"model": "Accord",
"year": 2024,
"color": "Silver"
},
"identifiers": [
{ "schemeName": "CollateralNumber", "number": "COL-2026-0002" }
],
"status": "Secured"
}
]
}
},
"lendingRelationType": "Dealer",
"codes": [
{ "codeType": "LoanPurpose", "value": "VehiclePurchase" }
]
}
Loan statuses
LoanStatus follows the loan's lifecycle:
Pending → Form → Review → Approve / Denied / Withdrawn → Funded → Active → (PaymentPastDue | Foreclosure | Bankruptcy | WriteDown | WrittenOff)
ORCA returns Approve immediately after origination; the transition to Funded happens once Loan Funding (Project 2) disburses the principal.
Step 3 — Update the loan (add collateral, link escrow, edit attributes)
PATCH /loans/{loanId} with application/merge-patch+json. Used for three distinct purposes:
- Adding or updating collateral records after origination
- Linking an externally managed escrow account
- Editing loan attributes (risk rating, flex fields, etc.)
JSON Merge Patch
RFC 7396. The body contains only the fields you want to change. For arrays (like contract.collateral.collaterals[] or relatedAccounts[]), the patch replaces the entire array — include both existing and new entries when adding, or you'll lose what's already there.
3.1 Add an additional collateral record
When a borrower pledges an additional asset after origination (e.g., adding a second vehicle to a cross-collateralized loan), patch the loan with the full updated collaterals[] array.
PATCH /loans/loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e HTTP/1.1
Content-Type: application/merge-patch+json
idempotencyId: 2c3d4e5f-6a7b-8c9d-0e1f-2a3b4c5d6e7f
{
"contract": {
"collateral": {
"totalValue": "508000.00",
"loanToValueRatio": "0.84",
"collaterals": [
{
"collateralId": "col-c0d1e2f3-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"collateralType": "ResidentialBuilding",
"value": "475000.00",
"status": "Secured"
},
{
"collateralType": "Automobile",
"value": "33000.00",
"netAppraisedValue": "32000.00",
"lastAppraisedDate": "2026-08-10",
"vehicleInformation": {
"vin": "5YJ3E1EA7KF317001",
"make": "Tesla",
"model": "Model 3",
"year": 2025
},
"identifiers": [
{ "schemeName": "CollateralNumber", "number": "COL-2026-0003" }
],
"status": "Secured"
}
]
}
}
}
{
"loanId": "loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e",
"status": "Active",
"contract": {
"collateral": {
"totalValue": "508000.00",
"collaterals": [
{ "collateralId": "col-c0d1e2f3-4a5b-6c7d-8e9f-0a1b2c3d4e5f" },
{ "collateralId": "col-d1e2f3a4-5b6c-7d8e-9f0a-1b2c3d4e5f60" }
]
}
}
}
3.2 Link an externally managed escrow account
The current ORCA spec does not model mortgage escrow accounts natively (no Escrow schema, no /escrow endpoint). In practice, mortgage escrow is managed in the servicer's back-office or sub-ledger system — that system controls collection, holds funds, performs annual escrow analysis, and disburses to tax assessors and insurance carriers.
ORCA's role is to expose the link between the primary loan and the externally managed escrow account, so consumers (statements, dashboards, payoff calculations) can find it. Use the relatedAccounts[] field with accountRelationType: "EscrowAccount".
Workflow
- The escrow account is opened in the servicer's back-office system. It receives a stable identifier (which becomes the
accountIdin ORCA's view). - Patch the primary loan to add a
relatedAccounts[]entry pointing at the escrow account.
PATCH /loans/loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e HTTP/1.1
Content-Type: application/merge-patch+json
{
"relatedAccounts": [
{
"accountId": "esc-back-office-887623",
"accountNumber": "ESC-1000234568-01",
"accountType": "Escrow",
"accountRelationType": "EscrowAccount",
"name": "Doe Mortgage Escrow"
}
]
}
{
"loanId": "loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e",
"relatedAccounts": [
{
"accountId": "esc-back-office-887623",
"accountRelationType": "EscrowAccount",
"accountType": "Escrow"
}
]
}
Patch replaces the array
relatedAccounts[] is replaced entirely by the patch body. If the loan already has other linked accounts (e.g. a FundingSource), include them in the patch too.
3.3 Update other loan attributes
Same PATCH endpoint, different fields:
{
"codes": [
{ "codeType": "RiskRating", "value": "B-Grade" }
],
"supplementaryData": {
"annualReviewDate": "2027-06-15"
}
}
Step 4 — Servicing reads
4.1 Retrieve loan balances
GET /loans/{loanId}/balances
{
"loanId": "loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e",
"balances": [
{ "name": "PrincipalBalance", "amount": "425000.00", "currency": "USD" },
{ "name": "InterestAccrued", "amount": "0.00", "currency": "USD" },
{ "name": "PayoffAmount", "amount": "425000.00", "currency": "USD" }
]
}
If an escrow account is linked, its balance is not surfaced here — query the escrow account separately via GET /accounts/{escrowAccountId}/balances.
4.2 Retrieve loan transactions
GET /loans/{loanId}/transactions — supports the same pagination and filter parameters as account transactions (amount.gte, amount.lte, creationDate.gte, creationDate.lte, status.eq, cursor pagination).
Step 5 — Loan documentation (alternative path)
Loan boarding typically involves a substantial document trail: signed promissory notes, deeds of trust, appraisal reports, title insurance, closing disclosures, UCC filings, insurance certificates. These are uploaded and associated with the loan through the Documents API — a separate API surface from the main ORCA spec, exposed at /documents.
This is an alternative path in the sense that document upload can run in parallel with the core loan-record workflow (Steps 1–4): a fintech can originate the loan first and upload documents later, or upload supporting documents (e.g., the appraisal report) before originating. The Documents API is independent of the loan's lifecycle status.
Documents API is separate from the main ORCA spec
The Documents API has its own OpenAPI specification (v0.16.1) and its own base URL (https://{environment}.tenants.portx.io/documents/api). Authentication uses the same JWT/OIDC scheme.
5.1 Upload a document
POST /documents → 201 Created with Document in the body.
Key request fields (DocumentUpload)
| Field | Required | Description |
|---|---|---|
ownerId |
yes | The party that "owns" this document — typically the borrower's personId or organizationId. |
name |
yes | Filename including extension. |
fileType |
yes | MIME type (e.g., application/pdf, image/jpeg, image/tiff). |
data |
yes | Base64-encoded payload. Max 20 MiB. |
documentType |
yes | The kind of document (see standard values below). |
size |
no | Decoded payload size in bytes. |
linkages[] |
recommended | Links the document to one or more ORCA resources. Each entry: resourceType (account, party, loan), resourceId, relationship (e.g., primary, collateral). |
accessControl[] |
no | Per-user or per-role access control list. |
metadata.tags[] |
no | Free-form tags. |
metadata.categories[] |
no | Categorization (e.g., Closing, Servicing, Compliance). |
metadata.fileHash |
no | Client-side hash for integrity. |
audit |
no | Carries idempotencyKey, servicerId, servicerBranchId. |
Standard documentType values for loans
PromissoryNote, DeedOfTrust, Mortgage, LoanAgreement, AppraisalReport, TitleInsurance, ClosingDisclosure, UCCFiling, InsuranceCertificate, LoanEstimate, RightOfRescission, TruthInLending, BorrowerApplication, IncomeVerification, EmploymentVerification, BankStatement, TaxReturn, CreditReport.
Example — promissory note
POST /documents HTTP/1.1
Host: api.portx.io
Authorization: Bearer <jwt>
Content-Type: application/json
{
"ownerId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"name": "promissory-note-signed.pdf",
"fileType": "application/pdf",
"size": 284512,
"documentType": "PromissoryNote",
"data": "JVBERi0xLjQKJ...",
"linkages": [
{
"resourceType": "loan",
"resourceId": "loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e",
"relationship": "primary"
}
],
"metadata": {
"tags": ["LoanDocs", "Mortgage", "Signed"],
"categories": ["Closing"],
"fileHash": "sha256:9c1185a5c5e9fc54612808977ee8f548b2258d31"
},
"audit": {
"idempotencyKey": "3e4f5a6b-7c8d-9e0f-1a2b-3c4d5e6f7a8b",
"servicerId": "bank-001"
}
}
{
"id": "doc-1f2e3d4c-5b6a-7c8d-9e0f-1a2b3c4d5e6f",
"ownerId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"name": "promissory-note-signed.pdf",
"fileType": "application/pdf",
"size": 284512,
"createdAt": "2026-06-15T14:23:11Z",
"url": "https://docs.portx.io/d/1f2e3d4c-5b6a-7c8d-9e0f-1a2b3c4d5e6f",
"processing": {
"state": "ready",
"completedAt": "2026-06-15T14:23:14Z",
"progress": 100
},
"linkages": [
{
"resourceType": "loan",
"resourceId": "loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e",
"relationship": "primary"
}
],
"metadata": {
"tags": ["LoanDocs", "Mortgage", "Signed"],
"categories": ["Closing"]
}
}
Example — appraisal report linked to collateral
The same POST /documents endpoint can attach an appraisal report and link it to both the loan and a specific collateral asset, since linkages[] accepts multiple entries:
{
"ownerId": "person-7f3a8c92-4b1e-4d2f-9a0c-1b2c3d4e5f60",
"name": "appraisal-8821-oak-ridge.pdf",
"fileType": "application/pdf",
"documentType": "AppraisalReport",
"data": "JVBERi0xLjQKJ...",
"linkages": [
{
"resourceType": "loan",
"resourceId": "loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e",
"relationship": "supporting"
},
{
"resourceType": "account",
"resourceId": "col-c0d1e2f3-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"relationship": "collateral"
}
],
"metadata": {
"tags": ["Appraisal", "RealEstate"],
"categories": ["Underwriting"]
}
}
Linkage resourceType
linkages[].resourceType only allows account, party, or loan in the current spec. To link a document to a specific collateral asset, use resourceType: "account" with the collateral's identifier; the Core treats the collateral as an associated resource.
5.2 List documents for a loan
GET /documents?resourceType=loan&resourceId={loanId} — returns all documents linked to a specific loan.
GET /documents?resourceType=loan&resourceId=loan-b9c0d1e2-3f4a-5b6c-7d8e-9f0a1b2c3d4e HTTP/1.1
Optionally filter by documentType (e.g., &documentType=AppraisalReport).
5.3 Download a document
GET /documents/{id}/download — returns either the inline data or a signed URL.
| Query parameter | Effect |
|---|---|
encoding=base64 (or hex) |
Returns the file inline, base64- or hex-encoded. |
url=true |
Returns a signed URL with an expiresAt timestamp. Recommended for large files. |
Signed URL response example:
{
"url": "https://docs.portx.io/signed/abc123?token=...",
"expiresAt": "2026-06-15T15:30:00Z"
}
5.4 Share a document externally
POST /documents/{id}/share — generate a shareable URL with a TTL (in minutes) and an optional shareTo[] recipient list.
{
"expiresInMinutes": 1440,
"shareTo": ["counsel@external-law-firm.com"]
}
Response:
{
"urlId": "share-7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"url": "https://docs.portx.io/share/7a8b9c0d...",
"expiresAt": "2026-06-16T14:30:00Z"
}
5.5 Update document metadata
PATCH /documents/{id} — update name, access control, or metadata. Cannot change file contents; re-upload for that.
5.6 Bulk export (for archival, Project 3)
POST /documents/export starts an asynchronous export job; GET /documents/export/{jobId}/status polls it. This is the integration point for Project 3 (Document Movement / archival), but is callable from this use case too if needed.
Document processing status
After upload, large documents may take time to process (OCR, virus scanning, indexing). The response's processing field reflects current state:
| State | Meaning |
|---|---|
processing |
Upload accepted, server-side processing in progress. progress field shows 0–100%. |
ready |
Document is available for retrieval, share, and linkage queries. |
failed |
Processing failed (e.g., scan caught malware, file corrupt). Document is rejected. |
Error handling
Same Error envelope as other use cases.
| Status | When |
|---|---|
400 Bad Request |
Validation failure — interests[] missing for required loan type, contract.amount ≤ 0, collateral missing for securedLoanIndicator: true. |
404 Not Found |
loanId, partyId, or referenced account doesn't exist. |
409 Conflict |
Idempotency collision; duplicate loan origination attempt. |
422 Unprocessable Entity |
Core-side business-rule rejection — credit policy failure, LTV exceeds product threshold. |
500 Internal Server Error |
Unexpected ORCA or Core failure. |
502 Bad Gateway |
Core unreachable. |
Edge cases and caveats
Origination vs funding
POST /loans originates the loan record but does not disburse funds. The loan moves to Funded only after Project 2 (Loan Funding) executes the disbursement.
Collateral is inline, not a sub-resource
There is no POST /loans/{loanId}/collateral endpoint. Pledged assets must be included in contract.collateral.collaterals[] either at origination (POST /loans) or via PATCH /loans/{loanId}.
Escrow lives outside ORCA
The ORCA spec does not model mortgage escrow (no Escrow schema, no escrow-specific fields for cushion, shortage analysis, RESPA reporting). Escrow accounts are opened and managed in the servicer's back-office system. ORCA exposes the linkage via relatedAccounts[] with accountRelationType: "EscrowAccount" so the relationship is discoverable, but the escrow's funds, disbursements, and analysis logic happen elsewhere.
Merge patch on arrays
A merge patch on an array field (collaterals[], relatedAccounts[], interests[]) replaces the whole array. Always send the complete list, not just the new items.
Core validation modes
Set Core-Validation-Mode: PERMISSIVE during initial integration testing to let invalid requests through to the Core for diagnostic purposes. Use ENFORCING in production.
Interest schemas vary by Core
interests[] (and contract.interests[]) is rich enough to express fixed, variable, tiered, and step-up rates, but Core systems implement different subsets. Verify which features the target Core supports before relying on advanced rate structures.
Related building blocks
- Documents API — Step 5 covers the upload, listing, download, share, and bulk-export endpoints. Document archival (Project 3) reuses the same API, primarily through
POST /documents/export. - Account Opening — Borrower onboarding (Step 1) overlaps with deposit-account opening. See Account Opening.
- Servicing transactions — Loan payments and disbursements (Project 2) are out of scope for boarding.