Skip to main content
All Docs
FeaturesmyProp (AgentOS People Portal)Updated April 4, 2026

Direct Upload: AgentOS People Portal v1 API

Direct Upload: AgentOS People Portal v1 API

As of v0.1.71, myProp supports direct file uploads via the original AgentOS People Portal /v1/corporate/peopleportal/... endpoint paths. These complement the existing /v3/ document upload routes and are required for certain upload workflows (maintenance requests, tenancy applications, contractor invoices) that are only available on the v1 API.

Overview

The direct upload service lives in src/lib/letmc/upload.ts and exposes a set of typed async functions. Each function:

  • Sends api_key as a request header (not a query parameter).
  • Uses a 120-second timeout to accommodate large file uploads.
  • Returns a DirectUploadResult object with success, data, and error fields so callers can handle failures gracefully without catching exceptions.

For batch operations, uploadMultipleDocuments() introduces a 500 ms delay between sequential uploads to avoid overwhelming the AgentOS API.

Prerequisites

The direct upload service requires the LETMC_API_KEY environment variable to be set. You can check availability at runtime:

import { isDirectUploadAvailable } from "@/lib/letmc/upload";

if (!isDirectUploadAvailable()) {
  // LETMC_API_KEY is not configured
}

Endpoints

Generic Document Upload

Attaches any document to an AgentOS object by OID.

Endpoint: POST /v1/corporate/peopleportal/letmcletting/{clientName}/uploads/generic/{objectOID}

import { uploadGenericDocument } from "@/lib/letmc/upload";

const result = await uploadGenericDocument(
  "myagency",        // clientName — agency identifier
  "OBJ-001",         // objectOID — target object OID
  {
    DocumentName: "lease-agreement.pdf",
    MimeType: "application/pdf",
    Content: "<base64-encoded content>",
    Size: 204800,
    // Timestamp defaults to Date.now() if omitted
  }
);

if (!result.success) {
  console.error(result.error);
}

Payload: GenericDocumentPayload

FieldTypeRequiredDescription
DocumentNamestringFile name including extension
MimeTypestringMIME type (e.g. application/pdf)
ContentstringBase64-encoded file content
SizenumberFile size in bytes
TimestampnumberEpoch ms; defaults to Date.now()

Maintenance Request

Submits a maintenance request on behalf of a person.

Endpoint: POST /v1/corporate/peopleportal/{shortName}/{clientName}/uploads/{personID}/maintenance-request

import { uploadMaintenanceRequest } from "@/lib/letmc/upload";

const result = await uploadMaintenanceRequest(
  "agencyslug",   // shortName — agency slug
  "myagency",     // clientName — agency identifier
  "PER-123",      // personID — tenant/landlord person OID
  {
    Fault: "Plumbing",
    Message: "Leak under kitchen sink.",
    PresenceRequested: true,
  }
);

Payload: MaintenanceRequestPayload

FieldTypeRequiredDescription
FaultstringFault/issue category or ID
MessagestringDescription of the issue
PresenceRequestedbooleanWhether tenant presence is needed

Tenancy Application

Submits a tenancy application for a specific tenancy.

Endpoint: POST /v1/corporate/peopleportal/letmcletting/{clientName}/uploads/{personID}/tenancy-application/{tenancyID}

import { uploadTenancyApplication } from "@/lib/letmc/upload";

const result = await uploadTenancyApplication(
  "myagency",    // clientName
  "PER-456",     // personID — tenant OID
  "TEN-789",     // tenancyID — tenancy OID
  { /* application fields */ }
);

The TenancyApplicationPayload is an open object ([key: string]: unknown) — pass whatever fields the AgentOS API requires for the application.


Contractor Invoice

Submits an invoice from a contractor against a maintenance job.

Endpoint: POST /v1/corporate/peopleportal/letmcletting/{clientName}/uploads/{contractorID}/submitted-invoices/submit

import { uploadContractorInvoice } from "@/lib/letmc/upload";

const result = await uploadContractorInvoice(
  "myagency",    // clientName
  "CON-999",     // contractorID
  {
    MaitenanceJobID: "JOB-001",
    PropertyID: "PROP-002",
    Amount: 350.00,
    Description: "Emergency plumbing repair",
    Documents: [
      {
        DocumentName: "invoice.pdf",
        MimeType: "application/pdf",
        Content: "<base64>",
        Size: 51200,
      },
    ],
  }
);

Payload: ContractorInvoicePayload

FieldTypeRequiredDescription
MaitenanceJobIDstringMaintenance job OID
PropertyIDstringProperty OID
AmountnumberInvoice amount
DescriptionstringInvoice description
DocumentsInvoiceDocument[]Attached invoice files

Batch (Multi-file) Upload

Upload multiple documents to the same object with a 500 ms stagger between requests.

import { uploadMultipleDocuments } from "@/lib/letmc/upload";

const result = await uploadMultipleDocuments(
  "myagency",
  "OBJ-001",
  [doc1, doc2, doc3]
);

console.log(`${result.successCount} succeeded, ${result.failedCount} failed`);
result.results.forEach((r, i) => {
  if (!r.success) console.error(`File ${i} failed: ${r.error}`);
});

MultiUploadResult

FieldTypeDescription
successbooleantrue only if all files succeeded
resultsDirectUploadResult[]Per-file result in order
successCountnumberNumber of successful uploads
failedCountnumberNumber of failed uploads

Passing an empty array returns { success: true, results: [], successCount: 0, failedCount: 0 } immediately.


tRPC Procedures

All functions are also exposed through the document router as tRPC procedures:

ProcedureDescription
document.directUploadSingle generic document via v1 path
document.directUploadMultipleBatch upload with 500 ms stagger
document.submitMaintenanceRequestMaintenance request via v1 path
document.submitTenancyApplicationTenancy application via v1 path
document.submitContractorInvoiceContractor invoice via v1 path
document.statusReports directUploadAvailable: boolean alongside existing status fields

Error Handling

All upload functions return a DirectUploadResult and do not throw. Errors are captured internally and returned in the error field.

If the upload times out (after 120 s), the error message will indicate a timeout with HTTP status 408. For non-2xx responses, the DirectUploadError includes the HTTP status code and the response body for debugging.

if (!result.success) {
  // result.error is a human-readable string
  // Log or surface to the user as appropriate
}

Comparison with /v3/ Endpoints

Behaviour/v3/ (existing)/v1/ (this feature)
API key transportQuery parameter (?api_key=…)Request header (api_key)
Timeout30 s120 s
Multi-file support500 ms stagger via uploadMultipleDocuments()
Backward compatibilityUnchangedFully additive — no existing behaviour changed