Every Acumatica integration I take over that still logs in with /entity/auth/login and a session cookie eventually hits the same walls: sessions pinned to one node behind a load balancer, mysterious 401s when a session expires mid-batch, and API users locked out because the password rotated. OAuth 2.0 fixes all of it, and setting it up takes about fifteen minutes once you know the screens. Here's the exact sequence I use.
Step 1: Register a connected application
Go to Integration > Connected Applications (SM303010). Create a new record:
- Client Name: something identifying the consumer — "Warehouse Sync Service", not "Test".
- OAuth 2.0 Flow: pick the flow (more on choosing below). For a server-to-server integration, choose Resource Owner Password Credentials; for anything acting on behalf of a human user in a browser, choose Authorization Code; Acumatica also supports Implicit (legacy, avoid) and, in recent versions, client credentials via API-user mapping.
Save the record — Acumatica generates the Client ID, which is a GUID suffixed with the tenant name, like 4B1E...@CompanyTenant. That suffix matters: it routes the token request to the right tenant, so don't trim it.
Then, on the Secrets tab, click Add Shared Secret. Give it a description and an expiration date. The secret value is shown once. Copy it into your secrets manager immediately; if you lose it you'll be adding a new one, not recovering it.
Step 2: Choose the right flow
For 90% of ERP integrations — a Laravel backend, an Azure Function, a middleware box — the consumer is a machine, and Resource Owner Password Credentials (ROPC) with a dedicated API user is the pragmatic choice. Yes, ROPC is deprecated in the broader OAuth world; in Acumatica's context it's the supported server-to-server pattern and it's still a huge upgrade over cookie sessions. Authorization Code (with PKCE if the client is public) is correct when a real user should authenticate — say, a portal where each user's Acumatica permissions must apply.
Step 3: Create a dedicated API user
Never run integrations as a person. Create a user like svc-warehouse, assign it the narrowest roles that cover the entities it touches, and — critically — set its date format and locale deliberately, because responses are formatted per user settings. Also confirm the user's license type: on newer licenses, API users are a separate, unlimited category that doesn't burn an interactive seat, but they must be flagged accordingly on the Users screen.
Step 4: Request a token
POST /identity/connect/token HTTP/1.1
Host: yourinstance.acumatica.com
Content-Type: application/x-www-form-urlencoded
grant_type=password
&client_id=4B1E7A22-...@CompanyTenant
&client_secret=YOUR_SECRET
&username=svc-warehouse
&password=THE_API_USER_PASSWORD
&scope=api offline_access
The response contains access_token, expires_in (3600 seconds by default), and — because we asked for offline_access — a refresh_token. The scopes worth knowing:
api— access to the contract-based REST and OData APIs.offline_access— issues a refresh token so you can renew without re-sending credentials.api:concurrent_access— lets the token be used by parallel requests; without it, concurrent calls on one token can trip each other into 401s. If your integration is multithreaded, request this scope.
Step 5: Use the token, refresh before expiry
Send Authorization: Bearer eyJhbGciOi... on every request. For refresh, post grant_type=refresh_token with the refresh token to the same endpoint. My client code refreshes proactively at 80% of expires_in rather than reacting to 401s — reactive refresh works, but it turns every expiry into a failed request in your logs and a retry you have to reason about.
If the API user's password expires or the account locks after failed attempts, the token endpoint returns a generic invalid_grant. Set the API user's password to not expire (policy permitting) and alert on invalid_grant in your integration logs — it almost always means credentials, not code.
Step 6: Verify end to end
Prove the whole chain with one call:
GET /entity/Default/24.200.001/StockItem?$top=1 HTTP/1.1
Host: yourinstance.acumatica.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
A 200 with one item means the connected app, secret, user, roles, and endpoint version are all aligned. A 401 here with a valid token usually means the user lacks access rights to the entity's screen; a 403/500 mentioning the endpoint usually means a version mismatch — list available endpoints at /entity to check what the instance actually exposes.
Operational notes from production
- Secret rotation: secrets take an expiry date — set one, and add the renewal to whatever calendar your team actually reads. An expired shared secret is my number-one cause of "the integration died overnight" calls.
- One connected app per consumer. Sharing a client ID across integrations means you can't revoke one without killing the others, and you can't tell them apart in logs.
- Tenant matters. The tenant suffix in the client ID binds the app to a tenant; multi-tenant instances need a connected app per tenant.
Wrapping up
The sequence is short: connected application, shared secret, dedicated API user with minimal roles, token request with api offline_access (plus api:concurrent_access if you parallelize), proactive refresh, and a smoke-test GET. Do it once properly and authentication becomes the one part of your Acumatica integration you never think about again — which is exactly what auth should be.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.