Upload API
Overview
The SDAC upload contract is implemented by Mastra at POST /sdac/upload.
Some deployments may additionally expose the same contract through APIM under /ai/sdac/upload, but that alias is deployment-specific and should be verified in the target environment before you depend on it.
The upload flow registers a durable Mastra upload job, deduplicates by file hash, submits the workbook to an internal ingestion worker for parsing, and starts report analysis after parsing writes the report rows. Large files usually return 202 with a jobId; poll the upload status endpoint until reportId is available.
Authentication
When this route is exposed through APIM, call it with:
Authorization: Bearer {access_token}Ocp-Apim-Subscription-Key: {subscription_key}
See Authentication for token acquisition and File Uploads for the route and deployment split.
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/sdac/upload | POST | Uploads and processes an SDAC workbook |
/sdac/uploads/{jobId}/status | GET | Returns durable upload progress and the parsed reportId when available |
/sdac/report-analysis/{reportId}/status | GET | Returns post-ingestion report-analysis workflow status |
Upload Request
Use multipart/form-data.
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | Excel workbook in .xlsx or .xls format |
user_email | string | Yes | Email of the uploader |
user_name | string | Yes | Display name of the uploader |
district | string | Yes | District or organization label for the upload |
force_reingest | string | No | Set to true to bypass duplicate-file detection and create a new report ID |
Accepted Response
For normal async processing:
{
"status": "processing",
"jobId": "11111111-1111-4111-8111-111111111111",
"stage": "processing_file",
"message": "The ingestion worker accepted the file and is parsing it now.",
"fileName": "Q1-2026-district-costs.xlsx",
"fileSizeBytes": 328192,
"largeFile": false,
"statusUrl": "/sdac/uploads/11111111-1111-4111-8111-111111111111/status",
"stalled": false,
"stalledAfterSeconds": 120
}
| Field | Type | Description |
|---|---|---|
jobId | UUID string | Durable Mastra upload job ID |
status | string | processing, success, duplicate, or error |
stage | string | Current upload stage |
message | string | Human-readable progress message |
statusUrl | string | Mastra-relative status polling URL; widget/browser clients should poll /api/mastra/sdac/uploads/{jobId}/status on the widget origin instead of fetching this value directly |
largeFile | boolean | Whether the upload is expected to take longer |
stalled | boolean | Whether no heartbeat has been observed for the configured timeout |
Status Response
Server-side Mastra callers can poll GET /sdac/uploads/{jobId}/status until
reportId appears. Widget/browser callers should use
GET /api/mastra/sdac/uploads/{jobId}/status on the widget origin.
{
"status": "success",
"jobId": "11111111-1111-4111-8111-111111111111",
"reportId": "8201EDC2-2EDE-4CA1-AF44-D0F5AA185CDB",
"report_id": "8201EDC2-2EDE-4CA1-AF44-D0F5AA185CDB",
"stage": "parsed",
"analysisStatus": {
"status": "queued",
"runId": "run-001"
}
}
Once reportId appears, poll GET /sdac/report-analysis/{reportId}/status
when the UI needs report-analysis progress or readiness.
Duplicate Upload Response
If the file hash already exists and force_reingest is not set:
{
"report_id": "8201EDC2-2EDE-4CA1-AF44-D0F5AA185CDB",
"status": "duplicate",
"record_count": 21,
"message": "This file has already been uploaded. You can use the existing report or re-ingest under a new ID.",
"existing_report": {
"district": "Maplewood Richmond Heights",
"quarter": "Q3",
"year": 2025,
"processed_at": "2026-03-29T18:42:11.000Z"
},
"can_reingest": true
}
Error Cases
| Status | Description |
|---|---|
| 400 | Missing required form fields or invalid file type |
| 401 | Missing or expired bearer token |
| 403 | Invalid subscription key or insufficient scope |
| 500 | Upload or processing failure |
Processing failures may still return a JSON body that includes report_id, status: "error", and a message explaining the failure.
Side Effects
Successful uploads do more than allocate a report_id:
- Mastra creates
SDAC.ingestion_UploadJobs - the internal ingestion worker parses the workbook
- workbook rows are inserted into
SDAC.data_CostReportsandSDAC.data_PersonnelRecords - Mastra starts
sdac-report-analysis - analysis results are persisted in
SDAC.analysis_Results
If the workbook hash already exists and force_reingest is not set, the service returns the existing report reference instead of writing a new report.
Example
curl -X POST "$UPLOAD_URL" \
-F "file=@./Q1-2026-district-costs.xlsx" \
-F "user_email=auditor@state.gov" \
-F "user_name=Alex Auditor" \
-F "district=Maplewood Richmond Heights"
When calling through APIM, set UPLOAD_URL="$BASE_URL/ai/sdac/upload" and
include the Authorization and Ocp-Apim-Subscription-Key headers. For direct
server-to-server calls, set UPLOAD_URL to the deployed Mastra /sdac/upload
origin. Browser/widget clients should use /api/mastra/sdac/upload on the
widget origin instead.
Operational Notes
- Only
.xlsxand.xlsuploads are accepted. - Duplicate detection is based on the uploaded file hash.
- Use
force_reingest=trueonly when you intentionally want a new report ID for a workbook that already exists. - After a successful upload, use the returned
report_idwith the Report Info API, Validation API, or Chat API.