API · Api

File Attachments with the Acumatica REST API

How to upload and link file attachments to Acumatica documents via REST — invoices, sales orders, expense receipts — including the often-misunderstood Files endpoint and link-by-key flow.

John Kihiu12 min read

Attaching a PDF, image, or document to a Sales Order or Bill through the REST API is a common integration requirement — customer-uploaded proof of delivery, a signed PO, an emailed invoice — and it's handled differently from a normal field update because a file isn't part of the entity's field contract.

Files are children of an entity, not fields on it

You don't attach a file by including it in the JSON body of a PUT to the entity. Files hang off a dedicated sub-resource path: /entity/{endpointName}/{version}/{entity}/{key}/files/{filename}. That path takes the raw binary content of the file directly in the request body — it isn't wrapped in JSON and, on current contract versions, isn't base64-encoded inside a field.

The attach request

HTTP · ATTACH A FILE
PUT /entity/Default/24.200.001/SalesOrder/SO/SO006480/files/PackingSlip.pdf HTTP/1.1
Host: yourinstance.acumatica.com
Authorization: Bearer {access_token}
Content-Type: application/pdf

<raw binary content of the PDF>

The key segments in the URL (SO, SO006480 above) are the entity's own key fields, in the order the contract defines them — get that order wrong and you'll attach to nothing, or hit a 404, rather than silently attaching to the wrong record.

Size and payload cost

Because the file goes over the wire as part of the HTTP request body rather than a multipart form, there's no chunked-upload mechanism in the contract-based API for very large files — you're bound by whatever request-size limits IIS and the Acumatica web application are configured for on that instance. For anything beyond a few megabytes routinely, check the actual maxAllowedContentLength/request-size configuration on the target instance rather than assuming a default; this is an infrastructure setting, not a REST API contract, so it varies by deployment.

Retrieving vs. listing files

A GET on the same entity with $expand=files (or a dedicated files sub-collection, depending on contract version) returns the list of attached file names and IDs without pulling their content — useful for checking what's already attached before uploading a duplicate. Fetching the actual bytes back out is a separate GET against the specific file path.

Check for an existing file before re-attaching

Re-running an integration that attaches the same document on every sync (a nightly job that re-uploads an invoice PDF, say) will keep adding duplicate attachments rather than replacing the old one unless you check the file list first and skip or delete-then-replace. The files sub-resource doesn't dedupe by filename automatically.

Practical gotchas

Two things trip people up in practice: the exact URL shape for the files sub-resource changed between older and newer System Contract versions (some older instances used a slightly different path convention before the "child files" model was standardized), so confirm the pattern against the specific endpoint version you're targeting rather than copying a snippet written for a different Acumatica release; and Content-Type on the PUT should match the actual file type — sending the wrong MIME type doesn't always error, but it can affect how the file previews inside Acumatica's own file viewer later.

Wrapping up

File attachments are a sub-resource of the entity, keyed the same way the entity itself is keyed, carrying raw binary in the request body rather than JSON. Check the existing file list before re-attaching, and verify the exact sub-resource path against your instance's contract version rather than assuming it matches an example written for a different release.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.