The Acumatica contract-based REST API is the cleanest way to integrate external systems with Acumatica ERP. Whether you're pushing invoices from an e-commerce store, pulling customer data into a CRM, or syncing inventory with a warehouse, the contract-based API is the right tool.
This guide walks through what I've learned shipping integrations against this API for production tenants — from the authentication choices that actually matter, to the patterns that prevent you waking up at 3 AM to retry batches.
Developers who already know REST and JSON, but are new to Acumatica's specific dialect. Examples are language-agnostic but I'll use C# and PHP where it helps.
Authentication
Acumatica supports two main authentication flows for the contract-based API. Pick based on whether your integration is server-to-server (cookie or OAuth client credentials) or user-facing (OAuth authorization code).
Cookie-based authentication
The classic approach. You POST credentials to /entity/auth/login, receive a session cookie, and include it in subsequent requests. Always remember to call /logout when you're done — Acumatica counts active sessions against your licence.
POST /entity/auth/login // returns Set-Cookie: ASP.NET_SessionId=...
Content-Type: application/json
{
"name": "admin",
"password": "********",
"company": "MyTenant",
"branch": "MAIN"
}
OAuth 2.0
For anything customer-facing or multi-tenant, OAuth is the better answer. Acumatica supports the standard authorization code grant. You register a client in the Connected Applications screen, then redirect users through the standard consent flow.
var client = new HttpClient();
var body = new FormUrlEncodedContent(new[] {
new KeyValuePair<string,string>("grant_type", "authorization_code"),
new KeyValuePair<string,string>("code", code),
new KeyValuePair<string,string>("client_id", clientId),
new KeyValuePair<string,string>("client_secret", secret),
new KeyValuePair<string,string>("redirect_uri", callback)
});
var resp = await client.PostAsync("/identity/connect/token", body);
var token = await resp.Content.ReadFromJsonAsync<TokenResponse>();
// Use token.access_token in Authorization: Bearer header
Endpoint discovery
Every contract-based endpoint lives at /entity/{Endpoint}/{Version}/{Entity}. The default endpoint is Default, but you can — and should — publish your own custom endpoint for production work.
- Default endpoint: always available, mirrors out-of-box screens.
- Custom endpoint: shape only the fields and entities you need. Faster, smaller, and protected from base-system schema drift.
Always build a custom endpoint for new integrations. It pays for itself on the first upgrade.
CRUD patterns
The contract-based API uses a JSON shape Acumatica calls nested entities. Every field is wrapped in { "value": ... }. Cumbersome? Yes. But it's how partial updates and conditional patches are expressed cleanly.
$payload = [
'CustomerID' => ['value' => 'JOHNK001'],
'CustomerName' => ['value' => 'John Kihiu Ltd.'],
'CustomerClass'=> ['value' => 'DEFAULT'],
'MainContact' => [
'Email' => ['value' => 'kihiujohn12@gmail.com'],
'Phone1'=> ['value' => '+254115169705'],
],
];
$response = $client->put('/entity/Default/24.200.001/Customer', [
'json' => $payload,
'cookies' => $jar,
]);
Notice we use PUT, not POST. In Acumatica, PUT is upsert — create if missing, update if present. POST is reserved for invoking actions.
Invoking actions
Actions like Release, Approve, Cancel, or Email Invoice are exposed as POST endpoints under /{Entity}/{Action}. The body identifies the record by key.
POST /entity/Default/24.200.001/SalesInvoice/ReleaseSalesInvoice
{
"entity": {
"ReferenceNbr": { "value": "INV001234" },
"Type": { "value": "Invoice" }
}
}
Actions are asynchronous. The response is 202 Accepted with a Location header. Poll that location until you get 204 No Content.
Error handling
Acumatica returns three classes of errors you must handle distinctly:
- 4xx with a body — validation errors. Parse
exceptionMessageand surface it. - 500 Internal Server Error — almost always a stale session or licence issue. Re-login and retry once.
- Concurrency exceptions — happen on heavy writes. Always re-fetch and retry with the new ETag.
Cookie sessions are billed per licence concurrent-user. Always log out, even on errors. A naive retry loop can starve your tenant of sessions in minutes.
Production tips
- Always use a custom endpoint. The Default endpoint changes between Acumatica versions; your custom one is yours forever.
- Throttle outgoing calls. 10–20 concurrent requests max. Acumatica is more sensitive to write throughput than read.
- Log the request and response in full for the first month after going live. The bugs you find are never the bugs you expected.
- Idempotency keys. Use the customer's order ID as your
ExternalRefNbrand dedupe on it.
That's the rough shape of every production-grade Acumatica integration I've shipped. Adapt it, and your client will sleep better than mine did the first time around.