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 & creationPerson (personal loans) and Organization (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 classifiersLoanPurpose, 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 /loans202 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:

PendingFormReviewApprove / Denied / WithdrawnFundedActive → (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:

  1. Adding or updating collateral records after origination
  2. Linking an externally managed escrow account
  3. 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

  1. The escrow account is opened in the servicer's back-office system. It receives a stable identifier (which becomes the accountId in ORCA's view).
  2. 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 /documents201 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.