Acumatica REST API: Contract-Based Versioning, Capabilities, and Integration Guide
What are the Acumatica REST API capabilities and how does contract-based versioning work?
TL;DR
- Bottom line: Acumatica uses a contract-based REST API where endpoints are versioned by release (e.g.,
24.200.001for 2024 R2). Contracts decouple API objects from UI screens, so customizations and upgrades do not break existing integrations. Use the Default endpoint for standard entities; extend it for custom fields. - Key limit: API rate limits are license-tier-dependent — standard tier: ~100 requests/min, 3 concurrent sessions; L-series: ~150 requests/min, 6 concurrent sessions. These cannot be self-configured. [src6, src7]
- Watch out for: JSON field values must be wrapped in
{"value": "..."}objects — sending flat key-value pairs returns 400 errors. Also, endpoint version must exactly match the instance version or you get 404. [src2] - Best for: Mid-market ERP integrations needing full CRUD operations, action execution (e.g., releasing invoices), and file attachments through a modern REST interface with stable versioned contracts. [src1, src4]
- Authentication: OAuth 2.0 (authorization code, resource owner password, implicit) or cookie-based login. OAuth with
apiscope recommended for production. [src1, src3]
System Profile
Acumatica Cloud ERP is a mid-market, cloud-native ERP platform built on a .NET/SQL Server stack. Its contract-based REST API was introduced to replace the legacy screen-based SOAP API, providing stability against UI customizations and platform upgrades. The contract-based approach means API endpoints expose business logic objects (not screen fields), so changes to forms, localizations, or customization projects do not break existing API integrations. This card covers the REST API surface across all Acumatica editions. The OData interface (GI-based and DAC-based) is a separate read-oriented surface not covered in depth here. [src1, src4]
| Property | Value |
|---|---|
| Vendor | Acumatica |
| System | Acumatica Cloud ERP 2024 R2 |
| API Surface | REST (Contract-Based) |
| Current API Version | Default endpoint 24.200.001 |
| Editions Covered | General Business, Distribution, Manufacturing, Construction, Retail Commerce |
| Deployment | Cloud (SaaS) / Private Cloud / On-Premise |
| API Docs | Acumatica Help — Contract-Based REST API |
| Status | GA |
API Surfaces & Capabilities
Acumatica provides multiple integration surfaces. The contract-based REST API is the primary and recommended surface for new integrations. [src1, src3, src4]
| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| Contract-Based REST API | HTTPS/JSON | Full CRUD, actions, file attachments | Configurable via $top | License-tier (100-150/min) | Yes | Partial (paged) |
| OData (GI-Based) | HTTPS/JSON+OData | Reporting, BI tools (Power BI, Excel) | Configurable via $top | Shared with REST | Yes | Read-only |
| OData (DAC-Based) | HTTPS/JSON+OData | Direct table access, deleted record tracking | Configurable via $top | Shared with REST | Yes | Read-only |
| Screen-Based SOAP API | SOAP/XML | Legacy integrations (deprecated for new dev) | N/A | Shared | Yes | No |
| Push Notifications | HTTP callbacks | Real-time change detection | N/A | N/A | Yes | N/A |
| Webhooks | HTTP POST (inbound) | External system pushes data into Acumatica | N/A | N/A | Yes | N/A |
Rate Limits & Quotas
Per-Request Limits
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max records per query page | Configurable via $top | REST API | Default page size varies by entity; use $top and $skip for pagination [src1] |
| Max $expand depth | 2 levels | REST API | Nested expansions beyond 2 levels return errors [src2] |
| Native batch operations | Not supported | REST API | No OData $batch equivalent; use sequential calls [src1] |
| Request body size | Not officially published | REST API | Large file attachments may hit IIS/web server limits |
Rolling / Daily Limits
| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| API requests per minute | ~100 (standard), ~150 (L-series) | Per minute | License-tier-dependent; configurable only by Acumatica support [src6, src7] |
| Concurrent API sessions | 3 (standard), 6 (L-series) | Per instance | Each unauthenticated request counts as a new session if cookies are not reused [src6] |
| Maximum web service API users | License-dependent | Per instance | Checked on License Monitoring Console (SM604000) [src1] |
| Session timeout | ~20 minutes idle | Per session | Cookie-based sessions expire; OAuth tokens depend on configuration [src2] |
Authentication
Acumatica supports OAuth 2.0 and cookie-based authentication. OAuth is recommended for production integrations. Client applications are registered on the Connected Applications screen (SM303010). [src1, src3]
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 Authorization Code | User-context web applications, interactive integrations | Configurable (~20 min session) | Yes (with offline_access scope) | Requires redirect URI; user authenticates via Acumatica login page [src1] |
| OAuth 2.0 Resource Owner Password | Server-to-server where auth code is not feasible | Configurable | Yes (with offline_access scope) | Sends username/password directly; less secure [src1] |
| OAuth 2.0 Implicit | Single-page apps (SPAs) | Short-lived | No | Tokens exposed in URL fragment; not recommended for production [src1] |
| Cookie-Based Login | Simple integrations, quick prototyping | ~20 minutes idle timeout | No (re-login required) | POST to /entity/auth/login; must explicitly logout to free session [src2] |
Authentication Gotchas
- OAuth
api:concurrent_accessscope and cookies: When using this scope, Acumatica tracks sessions via cookies. If you do not pass the session cookie back with subsequent requests, each API call counts as a new concurrent user — quickly exhausting your license limit. [src6] - Cookie-based login requires explicit logout: If you do not POST to
/entity/auth/logoutafter completing your operations, the session remains open and counts against your concurrent session limit until it times out (~20 min). [src2, src6] - Client ID includes company ID: The OAuth client ID generated by Acumatica includes the company/tenant identifier. The client application only has access to the data of the company specified in the client ID. [src1]
- OAuth scopes: Use
apifor standard access,api:concurrent_accessfor multi-session scenarios (with cookie management), andapi:offline_accessfor refresh tokens. Combining scopes incorrectly can cause authentication failures. [src6]
Constraints
- License-tier rate limits cannot be self-adjusted — you must contact Acumatica to increase the maximum requests per minute or concurrent sessions. There is no self-service throttle configuration. [src6, src7]
- No native batch/bulk API — unlike Salesforce Bulk API or NetSuite CSV import, Acumatica REST API processes records individually. For high-volume operations, implement client-side batching with pagination and rate limiting. [src1]
- Endpoint version must match instance version — calling
/entity/Default/24.200.001/against an instance running 2023 R2 (23.200.001) returns 404. Always verify the instance version before building endpoint URLs. [src2] - Field value wrapping is mandatory — all field values must be sent as
{"value": "..."}objects, not flat strings. This applies to creates, updates, and action parameters. [src2] - Custom endpoint extensions are instance-specific — if you extend the Default endpoint with custom fields or entities, those extensions exist only on that specific instance. They must be manually recreated or exported via customization packages when migrating between environments. [src1]
- Screen-based SOAP API is deprecated — Acumatica recommends contract-based REST for all new development. Screen-based SOAP is maintained for legacy compatibility only. [src3, src4]
Integration Pattern Decision Tree
START — User needs to integrate with Acumatica ERP
|-- What's the integration pattern?
| |-- Real-time (<1s) --> Contract-Based REST API CRUD [src1]
| | |-- Need business actions (release, approve)? --> REST API actions
| | |-- Need change notifications? --> Push Notifications (SM302000)
| |-- Batch/Bulk (scheduled, high volume)
| | |-- > 100K records/day? --> Import Scenarios or file-based
| | |-- < 100K records/day --> REST API with client-side batching
| |-- Event-driven --> Push Notifications (outbound) or Webhooks (inbound)
| |-- File-based --> Acumatica Import Scenarios (CSV/Excel)
|-- Which direction?
| |-- Inbound (writing) --> PUT with rate limiting
| |-- Outbound (reading) --> GET with $filter + pagination
| |-- Bidirectional --> LastModifiedDateTime + conflict resolution
|-- Error tolerance?
|-- Zero-loss --> Idempotency checks + logging
|-- Best-effort --> Retry on 4xx/5xx with backoff
Quick Reference
| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| Get all records | GET | /entity/Default/24.200.001/{Entity} | N/A | Use $top and $skip for pagination |
| Get single record | GET | /entity/Default/24.200.001/{Entity}/{ID} | N/A | ID is the internal GUID |
| Get by key fields | GET | /entity/Default/24.200.001/{Entity}/{KeyValue} | N/A | e.g., /SalesOrder/SO/000042 |
| Create record | PUT | /entity/Default/24.200.001/{Entity} | JSON | Fields wrapped in {"value": "..."} |
| Update record | PUT | /entity/Default/24.200.001/{Entity} | JSON | Include key fields + changed fields |
| Delete record | DELETE | /entity/Default/24.200.001/{Entity}/{ID} | N/A | Not all entities support delete |
| Execute action | POST | /entity/Default/24.200.001/{Entity}/{ID}/action/{ActionName} | JSON (params) | e.g., ReleaseDocument, ProcessPayment |
| Filter records | GET | /entity/Default/24.200.001/{Entity}?$filter=... | N/A | OData-style: eq, gt, lt, contains |
| Expand related | GET | /entity/Default/24.200.001/{Entity}?$expand=Details | N/A | Max 2 levels deep |
| Attach file | PUT | /entity/Default/24.200.001/{Entity}/{ID}/files/{filename} | Binary | Content-Type matches file type |
| Login (cookie) | POST | /entity/auth/login | JSON credentials | Returns session cookie |
| Logout (cookie) | POST | /entity/auth/logout | N/A | Always call to free session |
Step-by-Step Integration Guide
1. Register OAuth application in Acumatica
Navigate to System > Integration > Configure > Connected Applications (SM303010). Create a new application, select the OAuth 2.0 flow type, and configure the redirect URI. Copy the generated Client ID and create a shared secret. [src1]
Steps in Acumatica:
1. Navigate to System > Integration > Configure > Connected Applications
2. Click "+" to add new application
3. Enter Client Name (e.g., "MyIntegration")
4. Select OAuth 2.0 Flow: "Authorization Code" or "Resource Owner Password Credentials"
5. Add redirect URI (for auth code flow): https://myapp.com/callback
6. Click "Add Shared Secret" on the Secrets tab
7. Copy the Client ID and Secret Value immediately
8. Save the form
Verify: The application appears in the Connected Applications list with status Active.
2. Authenticate and obtain access token
Use the OAuth 2.0 resource owner password flow for server-to-server integrations. The token endpoint is at {instance}/identity/connect/token. [src1]
curl -X POST "https://yourinstance.acumatica.com/identity/connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "username=admin" \
-d "password=YourPassword" \
-d "scope=api"
Verify: Response contains an access_token field.
3. Retrieve records with filtering and pagination
Use GET with OData-style query parameters. Implement pagination using $top and $skip. [src1, src2]
curl -X GET "https://yourinstance.acumatica.com/entity/Default/24.200.001/SalesOrder?\$filter=Status%20eq%20'Open'&\$top=50&\$skip=0" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Accept: application/json"
Verify: Response is a JSON array. Fewer than $top records indicates the last page.
4. Create and update records
Use PUT for both create and update operations. All field values must be wrapped in {"value": "..."} objects. [src2]
curl -X PUT "https://yourinstance.acumatica.com/entity/Default/24.200.001/Customer" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"CustomerID": {"value": "NEWCUST01"},
"CustomerName": {"value": "New Customer LLC"},
"CustomerClass": {"value": "DEFAULT"}
}'
Verify: Response returns the full record with all fields populated.
5. Execute business actions
Use POST to invoke business actions on records, such as releasing invoices. [src1]
curl -X POST "https://yourinstance.acumatica.com/entity/Default/24.200.001/Invoice/INV000042/action/ReleaseInvoice" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Verify: GET the record again and confirm the status has changed.
6. Implement error handling and rate limit retries
Parse nested error details from Acumatica responses. Implement exponential backoff for 429 responses. [src5, src6]
import requests, time
def acumatica_request(method, url, token, data=None, max_retries=5):
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
for attempt in range(max_retries):
response = requests.request(method, url, headers=headers, json=data)
if response.status_code in (200, 201, 204):
return response.json() if response.content else None
if response.status_code == 429:
wait = min(2 ** attempt * 2, 60)
time.sleep(wait)
continue
if response.status_code == 400:
error_body = response.json()
raise Exception(f"Validation: {error_body.get('message', 'Unknown')}")
raise Exception(f"API error {response.status_code}: {response.text}")
raise Exception("Max retries exceeded")
Verify: Test with an invalid field to confirm error parsing.
Code Examples
Python: Retrieve and paginate all sales orders
# Input: Acumatica instance URL, access token
# Output: List of all open sales orders across all pages
import requests
def get_all_sales_orders(instance_url, token, status="Open", page_size=100):
headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
all_orders, skip = [], 0
while True:
url = (f"{instance_url}/entity/Default/24.200.001/SalesOrder"
f"?$filter=Status eq '{status}'&$top={page_size}&$skip={skip}")
page = requests.get(url, headers=headers).json()
if not page: break
all_orders.extend(page)
if len(page) < page_size: break
skip += page_size
return all_orders
JavaScript/Node.js: Create a customer with error handling
// Input: Acumatica instance URL, access token, customer data
// Output: Created customer record or error details
// npm install [email protected]
const axios = require("axios");
async function createCustomer(instanceUrl, token, customerData) {
const payload = {};
for (const [key, val] of Object.entries(customerData)) {
payload[key] = { value: val };
}
try {
const resp = await axios.put(
`${instanceUrl}/entity/Default/24.200.001/Customer`,
payload,
{ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } }
);
return resp.data;
} catch (err) {
if (err.response?.status === 429) console.error("Rate limited");
throw err;
}
}
cURL: Quick API connectivity test
# Cookie-based login
curl -c cookies.txt -X POST \
"https://yourinstance.acumatica.com/entity/auth/login" \
-H "Content-Type: application/json" \
-d '{"name":"admin","password":"YourPassword","company":"MyCompany"}'
# Retrieve stock items
curl -b cookies.txt "https://yourinstance.acumatica.com/entity/Default/24.200.001/StockItem?\$top=5"
# Always logout to free session
curl -b cookies.txt -X POST "https://yourinstance.acumatica.com/entity/auth/logout"
Data Mapping
Field Mapping Reference
| Acumatica Field Pattern | API Representation | Type | Transform | Gotcha |
|---|---|---|---|---|
| CustomerID | {"CustomerID": {"value": "CUST01"}} | String (key) | Wrapped in value object | Must match exact case and format |
| OrderTotal | {"OrderTotal": {"value": 1500.00}} | Decimal | Wrapped in value object | Currency decimals depend on config |
| OrderDate | {"OrderDate": {"value": "2026-03-02T00:00:00"}} | DateTime | ISO 8601 | Time zone depends on branch config |
| Status | {"Status": {"value": "Open"}} | String (enum) | Use display value | Available values vary by entity |
| Detail lines | {"Details": [{"InventoryID": {"value": "ITEM01"}}]} | Array | Nested value objects | Child records follow same wrapping |
| Custom fields | {"custom": {"FieldName": {"value": "..."}}} | Varies | Wrapped under custom key | Separate path from standard fields |
| NoteID (GUID) | {"id": "a1b2c3d4-..."} | GUID | Not wrapped | System-generated; used for GET by ID |
Data Type Gotchas
- DateTime values and time zones: Acumatica stores dates in the branch's configured time zone. The REST API returns ISO 8601 format but the time zone offset may not be included. [src1]
- Entity name capitalization matters: Entity names in endpoint URLs are case-sensitive.
SalesOrderworks;salesorderreturns 404. [src2] - Decimal precision: Currency fields follow the currency's decimal precision configuration. Extra decimal places may cause silent rounding. [src2]
Error Handling & Failure Points
Common Error Codes
| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 400 | Validation error | Missing required field, wrong value format | Parse nested error details for field-level messages [src5] |
| 401 | Authentication failure | Expired token, invalid credentials | Re-authenticate; request a new token [src2] |
| 403 | Insufficient permissions | API user lacks entity/field access | Check user role permissions in security config |
| 404 | Not found | Wrong endpoint version, misspelled entity | Verify endpoint version matches instance [src2] |
| 429 | Rate limit exceeded | More requests/min than license allows | Exponential backoff; upgrade license tier [src6, src7] |
| 500 | Internal server error | Business logic exception, DB constraint | Check Acumatica system trace; may require support ticket |
Failure Points in Production
- Session exhaustion from missing logout calls: Cookie-based integrations that crash without calling
/entity/auth/logoutleave orphaned sessions. With only 3-6 concurrent sessions, this can lock out all API access. Fix:Always wrap API calls in try/finally with logout in the finally block. Use OAuth tokens instead of cookie sessions when possible.[src2, src6] - Rate limit exhaustion in eCommerce scenarios: Real-time pricing lookups can easily exceed 100 req/min. Fix:
Implement a caching layer for frequently accessed data. Consider upgrading to L-series license.[src7] - Endpoint version mismatch after upgrade: Hardcoded endpoint versions break after Acumatica upgrades. Fix:
Store endpoint version in configuration, not hardcoded. Test in sandbox after each upgrade.[src2, src8] - Push notification delivery failures go unnoticed: Push notifications are fire-and-forget with no built-in retry. Fix:
Implement heartbeat checks on the notification endpoint. Use polling with LastModifiedDateTime as fallback.[src1] - Silent data truncation: Sending strings longer than field maximum may silently truncate. Fix:
Query entity metadata to check field lengths before sending data.[src5]
Anti-Patterns
Wrong: Sending flat field values without wrapper
// BAD — Acumatica requires {"value": "..."} wrapping
{
"CustomerID": "CUST01",
"CustomerName": "My Customer"
}
Correct: Wrap every field value in a value object
// GOOD — Proper Acumatica REST API field format
{
"CustomerID": {"value": "CUST01"},
"CustomerName": {"value": "My Customer"}
}
Wrong: Not logging out of cookie-based sessions
# BAD — session leak: if any call fails, logout never runs
session.post(f"{url}/entity/auth/login", json=credentials)
data = session.get(f"{url}/entity/Default/24.200.001/Customer").json()
process(data)
session.post(f"{url}/entity/auth/logout")
Correct: Use try/finally to guarantee logout
# GOOD — logout runs even if processing fails
session.post(f"{url}/entity/auth/login", json=credentials)
try:
data = session.get(f"{url}/entity/Default/24.200.001/Customer").json()
process(data)
finally:
session.post(f"{url}/entity/auth/logout")
Wrong: Hardcoding endpoint version
# BAD — breaks when Acumatica is upgraded
endpoint = f"{instance}/entity/Default/24.200.001/SalesOrder"
Correct: Make endpoint version configurable
# GOOD — version stored in configuration
import os
API_VERSION = os.environ.get("ACUMATICA_API_VERSION", "24.200.001")
endpoint = f"{instance}/entity/Default/{API_VERSION}/SalesOrder"
Common Pitfalls
- Using
api:concurrent_accesswithout cookie management: This scope tracks sessions via cookies. Without persisting cookies, every API call counts as a new session. Fix:Use plain 'api' scope or implement proper cookie jar management.[src6] - Ignoring pagination: The REST API returns a default page; it does not return all matching records. Fix:
Always implement pagination with $top and $skip. Continue until returned page is smaller than $top.[src1, src2] - Not verifying endpoint version before deployment: Using 24.200.001 against 23.200.001 instance returns 404. Fix:
Query instance version first and dynamically construct endpoint URLs.[src2] - Treating Acumatica REST as fully OData-compliant: Features like $count, $search, and $batch are not supported. Fix:
Test each OData feature against your instance before relying on it.[src1] - Not handling the business date header: Financial operations without
PX-CbApiBusinessDateuse the server's current date. Fix:Set this header explicitly for financial operations.[src1] - Using POST for creates instead of PUT: Acumatica uses PUT for both create and update; POST is for actions only. Fix:
Use PUT for all CRUD operations. POST is reserved for business actions.[src2]
Diagnostic Commands
# Test OAuth authentication
curl -s -X POST "https://yourinstance.acumatica.com/identity/connect/token" \
-d "grant_type=password&client_id=CLIENT_ID&client_secret=SECRET&username=admin&password=Pass&scope=api"
# List available entities on endpoint
curl -s "https://yourinstance.acumatica.com/entity/Default/24.200.001" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# Verify entity field schema
curl -s "https://yourinstance.acumatica.com/entity/Default/24.200.001/Customer/\$adHocSchema" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# Test cookie-based login
curl -c cookies.txt -s -X POST \
"https://yourinstance.acumatica.com/entity/auth/login" \
-H "Content-Type: application/json" \
-d '{"name":"admin","password":"Pass","company":"MyCompany"}'
# Check license limits: System > Licensing > License Monitoring Console (SM604000)
Version History & Compatibility
| Endpoint Version | Acumatica Release | Status | Key Changes | Migration Notes |
|---|---|---|---|---|
| 24.200.001 | 2024 R2 | Current | New OData URLs; OpenAPI 3.0 support; deleted record tracking | Legacy OData URLs deprecated, sunset 2025 R2 |
| 23.200.001 | 2023 R2 | Supported | Additional manufacturing and distribution entities | Same as 2024 R1 endpoint version |
| 22.200.001 | 2022 R2 | Supported | DAC Schema Browser; expanded default entities | Minimum version for many ISV integrations |
| 20.200.001 | 2020 R2 | Legacy | Push notifications and webhooks matured | Consider upgrading |
| 18.200.001 | 2018 R2 | EOL | Early contract-based REST API | Upgrade required |
Deprecation Policy
Acumatica supports previous endpoint versions for backward compatibility — calling an older endpoint version on a newer instance continues to work. However, new entities and fields are only available on the latest endpoint version. Acumatica typically provides at least one major release cycle (6-12 months) notice before removing deprecated features. [src1, src8]
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Full CRUD operations on business entities | Read-only reporting or BI dashboards | OData interface (GI-based or DAC-based) for Power BI/Excel |
| Executing business actions (release, approve, process) | Bulk data migration of >100K records | Acumatica Import Scenarios (built-in CSV/Excel import) |
| Building real-time integrations with external systems | Simple field customization without external integration | Acumatica Customization Framework |
| Server-to-server integration requiring stable versioned contracts | Direct database access or SQL queries | N/A — never use direct DB access with Acumatica Cloud |
| Attaching files and documents programmatically | Legacy screen-scraping integrations | Contract-based REST API replaces screen-based SOAP |
Important Caveats
- Rate limits and concurrent session limits are entirely determined by your Acumatica license tier and cannot be self-configured. Verify your license supports the required request volume before development. [src6, src7]
- The contract-based REST API uses PUT for both create and update operations — this is non-standard REST behavior. POST is reserved for executing actions on existing records. [src2]
- Custom endpoint extensions are instance-specific. When migrating between environments, include the endpoint customization in your Acumatica customization package. [src1]
- Acumatica's multi-tenant architecture means API limits are per-tenant. Multiple integrations sharing the same tenant share the same rate limit pool. [src6]
- Field values in API responses may differ from UI display values. Status fields may show internal codes rather than display labels depending on the endpoint configuration. [src2, src5]