24.200.001 for 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.{"value": "..."} objects — sending flat key-value pairs returns 400 errors. Also, endpoint version must exactly match the instance version or you get 404. [src2]api scope recommended for production. [src1, src3]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 |
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 |
| 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 |
| 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] |
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] |
api:concurrent_access scope 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]/entity/auth/logout after completing your operations, the session remains open and counts against your concurrent session limit until it times out (~20 min). [src2, src6]api for standard access, api:concurrent_access for multi-session scenarios (with cookie management), and api:offline_access for refresh tokens. Combining scopes incorrectly can cause authentication failures. [src6]/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]{"value": "..."} objects, not flat strings. This applies to creates, updates, and action parameters. [src2]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
| 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 |
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.
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.
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.
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.
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.
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.
# 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
// 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;
}
}
# 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"
| 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 |
SalesOrder works; salesorder returns 404. [src2]| 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 |
/entity/auth/logout leave 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]Implement a caching layer for frequently accessed data. Consider upgrading to L-series license. [src7]Store endpoint version in configuration, not hardcoded. Test in sandbox after each upgrade. [src2, src8]Implement heartbeat checks on the notification endpoint. Use polling with LastModifiedDateTime as fallback. [src1]Query entity metadata to check field lengths before sending data. [src5]// BAD — Acumatica requires {"value": "..."} wrapping
{
"CustomerID": "CUST01",
"CustomerName": "My Customer"
}
// GOOD — Proper Acumatica REST API field format
{
"CustomerID": {"value": "CUST01"},
"CustomerName": {"value": "My Customer"}
}
# 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")
# 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")
# BAD — breaks when Acumatica is upgraded
endpoint = f"{instance}/entity/Default/24.200.001/SalesOrder"
# 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"
api:concurrent_access without 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]Always implement pagination with $top and $skip. Continue until returned page is smaller than $top. [src1, src2]Query instance version first and dynamically construct endpoint URLs. [src2]Test each OData feature against your instance before relying on it. [src1]PX-CbApiBusinessDate use the server's current date. Fix: Set this header explicitly for financial operations. [src1]Use PUT for all CRUD operations. POST is reserved for business actions. [src2]# 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)
| 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 |
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]
| 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 |