AI Agents · Integration

Acumatica AWS S3 Integration — A Complete Guide

Acumatica AWS S3 Integration — A Complete Guide is the kind of integration that pays for itself the first time it runs without intervention.

John Kihiu12 min read

Acumatica stores every attachment — the signed delivery notes, the scanned supplier invoices, the fifteen-megapixel photos your field team insists on taking — in the UploadFile tables inside the application database. On a young instance nobody notices. Three years in, the database is 200 GB, 170 of that is files, backups take hours, and your SaaS tier or your DBA is complaining. That's the moment someone asks about S3. There are two genuinely different integrations hiding under "Acumatica S3 integration", and it's worth being clear which one you're building.

Two different problems

  1. Getting files out of the database — archiving or offloading attachments to object storage to keep the DB lean.
  2. Getting external files into Acumatica — documents that land in S3 from other systems (EDI, scanners, a mobile app) and need to appear attached to the right ERP record.

Both lean on the same two Acumatica capabilities: the contract-based REST API's file endpoints, and business events as change triggers. Let's take them in turn.

The REST file API in ninety seconds

Attachments hang off records via NoteID. Through the contract-based API, a GET on an entity returns a files array (when you ask for it) with metadata and an href per file; you download by following that href, and you attach by PUTting bytes to the record's files URL:

HTTP · ATTACH A FILE TO A SALES ORDER
PUT /entity/Default/24.200.001/SalesOrder/SO/SO006721/files/pod-SO006721.pdf HTTP/1.1
Host: erp.example.com
Authorization: Bearer eyJ...
Content-Type: application/pdf

%PDF-1.7 ... (raw bytes)

Downloads are the mirror image: GET .../SalesOrder/SO/SO006721?$expand=files to list, then GET each file href. That's the whole vocabulary the sync service needs.

Problem 1: archiving attachments out to S3

My standard build is a small worker service (usually .NET, sometimes Laravel where the client's stack is PHP) that runs on a schedule and does this loop:

  1. Query a Generic Inquiry exposed via OData — the GI joins UploadFile/UploadFileRevision metadata with the parent record type so the worker can build a meaningful S3 key. Something like customer-docs/AR/INV-004521/pod.pdf beats a flat bucket of GUIDs forever.
  2. Download each file over the REST API, stream it to PutObject with SSE enabled, and verify the ETag/checksum.
  3. Record the mapping — NoteID, file revision ID, S3 key — in a small state table owned by the worker. This ledger is the actual asset; treat it as production data.
  4. Only after a verified upload, delete or truncate the source file in Acumatica (there are maintenance screens and APIs for file cleanup — and take a DB backup before your first bulk run, obviously).

The deletion step is where you decide policy, not code: most of my clients keep the last 12 months of files hot in Acumatica and archive older revisions. Users overwhelmingly open recent attachments; the 2019 delivery notes can live in S3 Glacier Instant Retrieval at a fraction of the cost.

Archived files vanish from the paperclip

Once you delete a file from Acumatica, it's gone from the record's attachment list — users won't magically see S3. Plan the retrieval story before you archive: mine is a custom "Archived Documents" tab (a grid over the mapping table) with a button that generates a presigned S3 GET URL, valid for 15 minutes, and opens it. Presigned URLs mean the browser talks to S3 directly and no AWS credentials ever touch the client.

Problem 2: S3 as an inbound document channel

The reverse flow is event-driven and pleasantly clean: S3 raises an event notification on ObjectCreated, which triggers a Lambda, which pushes the file into Acumatica over REST. The only design work is the correlation rule — how does a dropped file know which record it belongs to? Options that have worked for me, in order of preference: a key convention (inbound/SO/SO006721/photo1.jpg — the path is the address), object metadata tags set by the producing system, or a manifest JSON dropped alongside a batch. Whatever you choose, make the Lambda idempotent: S3 events are at-least-once, and attaching the same POD twice looks unprofessional on a customer-facing record.

C# · LAMBDA: S3 EVENT TO ACUMATICA ATTACHMENT
public async Task Handle(S3Event evt)
{
    foreach (var rec in evt.Records)
    {
        var key = rec.S3.Object.Key;                 // inbound/SO/SO006721/pod.pdf
        var parts = key.Split('/');
        if (parts.Length != 4 || parts[0] != "inbound") continue;

        var (docType, orderNbr, fileName) = (parts[1], parts[2], parts[3]);

        using var obj = await _s3.GetObjectAsync(rec.S3.Bucket.Name, key);
        var url = _erp + "/entity/Default/24.200.001/SalesOrder/" +
                  docType + "/" + orderNbr + "/files/" + fileName;

        var put = new HttpRequestMessage(HttpMethod.Put, url)
        { Content = new StreamContent(obj.ResponseStream) };
        put.Headers.Authorization = await _auth.BearerAsync();

        var resp = await _http.SendAsync(put);
        if (resp.StatusCode == HttpStatusCode.NotFound)
            await _dlq.SendAsync(key, "order not found");   // park it, don't crash
        else
            resp.EnsureSuccessStatusCode();
        // move processed object out of inbound/ so replays are visible
        await _s3.CopyAsync(rec.S3.Bucket.Name, key, _processedPrefix + key);
        await _s3.DeleteAsync(rec.S3.Bucket.Name, key);
    }
}

Operational notes

Wrapping up

S3 integration with Acumatica is two projects wearing one name. The archive direction is a scheduled worker with a mapping ledger and a retrieval UI — measured in database gigabytes saved. The inbound direction is an event-driven Lambda with a key convention and idempotent attaches — measured in manual filing eliminated. Both stand on the REST file API and both live or die on the boring parts: the state ledger, idempotency, and not exhausting your API user licenses. Build those first and the AWS SDK calls are the easy afternoon.

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.