Uploading documents
Documents are attached to a client, then picked up when you create a submission. There are two ways to upload — pick whichever fits your stack.
Accepted formats & size limits
Both upload paths accept the same file types:
| Kind | Extensions |
|---|---|
.pdf |
|
| Images | .jpg .jpeg .png .bmp .heic .heif .gif .webp .tif .tiff |
| Office / spreadsheets | .xls .xlsx .csv .doc .docx .odt .txt .rtf .ppt .pptx |
.eml .msg |
|
| Archive | .zip (see tax-software archive handling below) |
| Lacerte | .TC5 or a ZIP of Lacerte backup data for tax year 2025 |
Size limits: presigned upload (Option A) — 1 GB per file; direct multipart upload (Option B) — 10 MB per request. Anything over a path's limit is rejected (a too-large multipart request returns 413 pointing you to the presigned flow). There's no fixed cap on the number of documents per client or per prepare call.
One tax-software input bundle
For the submission's tax software, provide at most one distinct input bundle for the return being prepared: one native return file, one supported backup archive, or the required companion-file set (for example, an Axcess .rctrl + .rtnbak pair). Identifiable native files or backups for a different tax software are rejected with tax_software_input_mismatch rather than ignored.
Processing removes duplicate native files with identical bytes and the same extension. Supported backup archives are deduplicated by their tree relative to the detected backup root: file paths, file contents, and empty directories. Compression, timestamps, entry order, wrapper-folder names, and OS metadata do not distinguish backups; path case and meaningful empty directories do. The original retained ZIP is not rewritten.
If more than one distinct bundle remains, the submission is cancelled with multiple_tax_software_files. A Lacerte .TC5 plus a backup ZIP counts as two alternatives, even if they may contain the same return. The portal's filename-based warnings cannot determine duplicate content; the final check runs during preprocessing.
Lacerte backup ZIPs
Lacerte backups are identified from their contents; no document-type flag is needed. Backups may be nested inside folders or other ZIPs alongside source documents. We preserve each backup as a ZIP for the preparer and process the surrounding source documents normally. Native backup files, including empty support files, are not sent to OCR.
Archives already tagged as tax software are handled differently: recognized backups stay intact, and other tagged archives expand only when all non-metadata files are native tax-software files or supported nested archives. Nested archives inherit the same protection. Unrecognized mixed payloads stay intact rather than entering OCR, so upload source documents separately from tagged tax-software archives.
The backup's internal tax year must be identifiable and match the submission's tax year. An absent or unverifiable requested year cancels processing with outdated_tax_software_file. If the requested year is present but other year evidence conflicts, processing stops for internal investigation without automatically cancelling the submission. Recognition does not certify that a backup is complete or restorable; Lacerte import and the preparer perform that validation. Completed 2025 returns are delivered as .TC5, not ZIP backups.
ZIP is currently the only supported archive format. Archive processing rejects encrypted, corrupt, or unsafe archives. Across a submission's archives, preprocessing allows at most 10 nested archive layers, 100,000 archive entries, and 2 GiB of cumulative expanded data (nested archives count at each layer).
Option A — Presigned exchange (recommended)
You upload the bytes directly to storage; they never pass through the API, so large or many files don't tie up your request or ours. Three steps:
Prepare —
POST /v1/clients/{id}/documents/prepare{ "documents": [ { "filename": "W2.pdf", "content_type": "application/pdf" }, { "filename": "1099.pdf", "content_type": "application/pdf" } ] }Returns an
upload_targets[]array, each with adocument_id, anupload_url, and a set offields.Optional integrity check (
content_md5). Add a base64-encoded 128-bit MD5 of a file's bytes to its prepare entry and we pin it into the upload target, so storage rejects the upload if the bytes don't match — catching truncated or corrupted transfers. The returnedfieldsthen include aContent-MD5you must submit with the upload.{ "documents": [ { "filename": "W2.pdf", "content_type": "application/pdf", "content_md5": "XUFAKrxLKna5cZ2REBfFkg==" } ] }It's optional and per-document — omit it to skip the check. A malformed digest is rejected with
400 invalid_content_md5.Upload each file to its
upload_urlasmultipart/form-data— include every key/value infields, then thefilepart last. A success is HTTP204.Upload URL lifetime & CORS. Each
upload_urlis valid for up to 7 days; after that, request a freshprepare. The presigned POST is designed for server-side upload — uploading directly from a browser needs cross-origin (CORS) permission on the storage bucket, which isn't enabled by default. Contact us if you need browser-direct upload.Finalize —
POST /v1/clients/{id}/documents/finalizewith thedocument_ids you uploaded:{ "document_ids": [456, 457] }Finalize verifies the objects landed and marks them ready. Anything that didn't upload comes back in
failed_document_ids; just retry those. (The olderfailed_document_indicesis still returned but is deprecated.)
A document isn't attached to a submission until it's finalized. If you try to create a submission while a prepared document is still unfinalized, the request is rejected with
409 documents_still_uploading— finalize (or retry) all prepared documents first. An abandoned upload (apreparethat never completed) won't block the client forever: once it's about 1 hour old, the next time you create a submission it's automatically swept aside. You don't have to wait, though — list the client's documents, find the one still in stateuploading, andDELETEit to clear the block immediately. (Finalizing a never-uploaded document does not clear it — delete it.)
Option B — Direct multipart upload
For thin clients that can't do the presigned dance, send the files straight to us in one request. The bytes stream through our API, so this path is capped at 10 MB per request — meant for a handful of small documents. For larger or bulk uploads, use the presigned exchange (Option A); an over-limit request is rejected with 413 and a pointer to /prepare.
curl -s -H "Authorization: Bearer $MAGNETIC_API_KEY" \
-F "[email protected]" -F "[email protected]" \
$BASE/v1/clients/123/documents
Returns the created documents (each with its id) and — like prepare/finalize — a failed_document_ids and a deprecated failed_document_indices. No finalize step is needed. On this path a failed file never got created, so it has no id: failed_document_ids is always empty here and the failed entries appear in failed_document_indices (or as fewer documents than files sent). See Deprecations.
Which should I use?
| Presigned (A) | Multipart (B) | |
|---|---|---|
| Bytes flow | Browser/server → storage directly | Through our API |
| Best for | Large/many files, server-to-server | Simple clients, small files (≤ 10 MB/request) |
| Round trips | prepare → upload → finalize | one |
After uploading
Uploaded documents sit on the client until you create a submission, which picks up every finalized document not already attached to one (see which documents get attached) and starts processing. Upload everything first, then submit.