After a year of running two production tenants in the 5,000-orders-per-day range, the Acumatica REST API stops being mysterious. It becomes a tool with known sharp edges. This is the guide I wish someone had handed me on day one — what it does well, where it bites, and the patterns that keep your client quiet at 3 AM.
1. Two APIs, one platform
Acumatica exposes two HTTP APIs you can call today:
- Contract-based REST — the modern, recommended path. Endpoints live at
/entity/{Endpoint}/{Version}/{Entity}, returning nested JSON. - System REST (OData) — a flat OData surface good for ad-hoc reads, less so for writes.
- SOAP (legacy) — still available but treated as deprecated for greenfield work.
For new work, use the contract-based API. The rest of this guide assumes you are.
2. Authentication — pick once, defend it forever
Three flows, with a clear "right answer" for each scenario:
- Server-to-server (cookie): simplest. POST
/entity/auth/loginwith{name, password, company, branch}, hold the session cookie, call/entity/auth/logoutwhen done. Watch your licence concurrent-user count. - Server-to-server (OAuth client credentials): best for unattended jobs that run across many tenants.
- User-facing (OAuth authorization code): the only acceptable flow when an end-user is signing in.
If you are building one backend that talks to one tenant, the cookie flow is fine. The moment you talk to multiple tenants, switch to OAuth client credentials. The accounting will save you.
3. Endpoints and contracts
The Default endpoint is always available, but it changes between Acumatica versions. Always publish a custom endpoint for production work — you control the schema, the version pin, and the field surface. Custom endpoints are the single biggest upgrade-protection lever you have.
Within an endpoint, the contract is the schema. The same DACs map to nested entity JSON. Most fields end up wrapped in {"value": ...} to express nullability, ID references, and partial updates cleanly.
logout, even on errors.
4. Reading, writing, and the PUT-as-upsert convention
Acumatica uses HTTP verbs with a distinct Acumatica-specific meaning:
- GET — read one or many records. Use
$filter,$select,$expand,$top,$skip. - PUT — upsert. Create if missing, update if present. Body uses nested entity JSON.
- POST — invoke an action (Release, Approve, Cancel, Email…).
- DELETE — only valid on screens that explicitly support deletion; otherwise prefer a void/cancel action.
5. Actions are async — model them that way
Every action call returns 202 Accepted with a Location header. Poll that location until you see 204 No Content. Most teams ignore this, then wonder why their "Release" call sometimes returns the same record state as the read. Don't be that team.
6. Errors — there are four flavours
- 4xx with body — validation. Surface
exceptionMessageand field-level errors. - 401/403 — session expired or unauthorised. Re-auth and retry once.
- 500 — usually a session/tenant/licence issue. Re-auth and retry; if it persists, log and pause.
- Concurrency — the dreaded ETag mismatch. Re-fetch and retry with the new ETag.
7. Webhooks vs polling
For event-driven work (order released, payment received), Acumatica's webhook (push notification) framework is the right tool. Register the screen, choose the events, point at your HTTPS endpoint. Use polling only for jobs, not for user-visible state.
8. Versioning — a strategy, not a wish
Pin the version in the URL. When Acumatica ships 26.200.001, the Default endpoint silently upgrades; your custom endpoint does not. Plan a regression suite that exercises both versions in parallel during the cut-over window.
9. Performance — the rules of thumb
- Batch reads with
$expandrather than walking related entities one by one. - For writes, never exceed 10–20 concurrent requests. Acumatica is more sensitive to write throughput than read.
- Use
$selectaggressively — you do not need 80 fields when you need 6. - Cache entity schemas at startup. They do not change at runtime.
10. Idempotency — the only safe way to retry
Every PUT that creates a business document must carry an idempotency key (use ExternalRefNbr or your own UsrExtRef field). Without it, a retry after a timeout creates a duplicate invoice. With it, the retry is a no-op.
11. Testing — what to actually cover
Your test suite needs at least:
- Round-trip tests for every entity you write (create, read, update, action, verify).
- Negative tests for every validation rule you care about.
- Concurrency tests where two integrations write the same record simultaneously.
- A smoke test that runs every endpoint once after a tenant upgrade.
12. Operational hygiene
Things that pay for themselves within a month:
- Log full request and response bodies (with redaction) for the first 30 days after go-live.
- Track session-open duration; alert on anything over 5 minutes.
- Track per-tenant licence headroom; alert before you run out.
- Track 4xx and 5xx rates per endpoint; surface them on a dashboard.
Common production failure modes
Five things I have seen break more than once:
- Session leak from missing logout — licence exhaustion, every user locked out.
- Default endpoint upgrade breaking a field name — silent data loss in writes.
- PUT without idempotency key — duplicate invoices on retry.
- Ignoring 202 + Location — "my action returned the old state, the API is broken" (no, you didn't poll).
- Filtering on a non-indexed DAC field — every call triggers a table scan, the tenant crawls.
Related reading
If you are building on top of this guide, these two articles are the natural next stops:
- Acumatica OAuth 2.0 Setup — Step by Step
- Acumatica REST API — Rate Limiting and Retry
- Bulk Data Loads with the Acumatica REST API
Going deeper: production-grade patterns
The patterns above cover the basics. In production, the same patterns have to survive three things: scale, edge cases, and the next Acumatica upgrade. Here are the patterns that distinguish a working customisation from a great one — the ones I have applied to every client project in East and Southern Africa, and the ones that make the difference between a customisation the user trusts and a customisation they curse.
Defensive coding for the unexpected
Production is where the assumption dies. Every customisation that "works in test" fails in production the first time a customer name has a special character, an invoice is in a foreign currency, or a record has a null in a field you thought was required. The defensive habit is to explicitly handle the null, the empty, the special character, and the foreign currency in every event handler and every code path. The cost is 20% more code. The payoff is 95% fewer production tickets.
Three patterns I apply everywhere:
- Null-safe property access. Use
?.on every property access; the alternative is a NullReferenceException at 2 AM. - Explicit value handling. If a field can be empty, treat it as empty. Do not assume the default value.
- Defensive database reads. A
PXSelectthat returns null is a valid result, not an error. Handle it.
public class DefensiveExt : PXGraphExtension<BaseGraph>
{
protected void _(Events.RowSelected<MyDAC> e)
{
var row = e.Row;
if (row == null) return; // null-safe
var ext = row.GetExtension<MyDACExt>();
if (ext == null) return; // null-safe extension
var value = ext.UsrField ?? "DEFAULT"; // null-coalesce
var ok = decimal.TryParse(value, out var n); // try-parse
if (!ok) { /* handle */ }
}
}
Performance: the patterns that scale
Five performance patterns I apply on every customisation, in order of impact:
- Move heavy logic out of
RowSelected. Push validation toRowPersisting, side effects to a graph action triggered by a button.RowSelectedfires for every row on every render. - Index the join columns. Every BQL
Where<>filter needs an index. Check the execution plan before you ship. - Filter at the GI, not the UI. A GI that returns 5 million rows and filters in the presentation layer will time out. Push filters into the Conditions tab.
- Batch the work. Loop with 1,000 calls is slow; loop with 10 calls of 100 records is fast. Batch where you can.
- Cache the static. Tax schedules, account lists, and other static reference data can be cached for the lifetime of the app pool. Reduce the database load.
For the full performance playbook, see the performance tuning guide and the SQL Server indexing guide.
Upgrade survival
The customisation that breaks on the next Acumatica upgrade is the one that took a shortcut. The patterns that survive:
- Extend, never modify.
PXCacheExtension<T>over editing the base DAC.PXGraphExtension<T>over editing the base graph. - Usr prefix on every field. Acumatica uses this to separate your fields from base fields. Without it, your field collides with a base field on the next upgrade.
- Source control with a clear branch strategy. Main is production; develop is next release; feature branches are work in flight. Tag every release.
- Test on production data. The staging tenant is a copy of production. The data shape is the same. The bugs are the same.
// Base field — Acumatica owns this
[PXDBString(40)]
public string RefNbr { get; set; }
// Your field — always Usr prefix, never collides
[PXDBString(40)]
[PXUIField(DisplayName = "External Ref")]
public string UsrExternalRef { get; set; }
// Your DAC extension — soft extension, survives table drops
[PXTable(IsOptional = true)]
public class MyDACExt : PXCacheExtension<MyDAC>
{
#region UsrCustomField
[PXDBString(60)]
public string UsrCustomField { get; set; }
public abstract class usrCustomField :
PX.Data.BQL.BqlString.Field<usrCustomField> { }
#endregion
}
Testing: the habit that pays for itself
If you are not testing your customisation with the Acumatica Unit Test Framework, you are running blind. The framework ships with every installation, costs nothing, and pays for itself the first time an upgrade changes a method signature on you. The minimum coverage:
- Every graph action with business logic — happy path + the most common error path.
- Every DAC field with a defaulting or validation rule.
- Every workflow transition — that the right state is reached from the right source state.
- Every import scenario with a sample CSV that exercises the validation rules.
For the full test framework walkthrough, see the unit test framework guide.
Operations: what to do after the customisation is live
A customisation is not "done" when it ships. It is "done" when it has run in production for a quarter without a critical incident. The operational habits that get you there:
- Monitor the slow queries. Acumatica's System Monitor has a slow-query log. Review weekly; the slow query you ship is the production incident in two months.
- Track the licence headroom. Every active session counts. A leaking integration can lock out real users within an hour.
- Review the audit log. Not for compliance — for understanding how the system is used. The audit log tells you where to optimise next.
- Document the runbook. When the customisation fails (and it will), the runbook is what saves the on-call. Document the failure modes, the diagnostic flow, the fix.
For the broader operational patterns, see the monitoring guide and the licence concurrency guide.
The migration off the old customisation
Every customisation is eventually replaced. Plan for that day from the start. The patterns:
- Wrap external dependencies. If your customisation talks to an external API, wrap the call in your own service. When the API changes, you change one place.
- Tag the version. Every customisation has a version. The version is in the database, in the metadata, in the package. When you upgrade, you know what you are upgrading from.
- Document the data model. Every custom field, every new table, every relationship. The next person who has to read the customisation should be able to start with the data model.
- Test the migration path. A customisation that ships and cannot be removed is a liability. The migration path off should be tested before the customisation is in production.
For the broader migration patterns, see the data migration guide.
Wrapping up
That is the working approach I use on Acumatica projects. The same patterns show up whether you are in Nairobi, Johannesburg, Kigali, Lusaka or Harare — and they are the things that keep work moving when an upgrade lands at 6 PM on a Friday. If you are stuck on something specific, reach out or keep reading through the rest of the Acumatica blog.