Authentication
Every request carries a bearer token:
Authorization: Bearer np_v2_7f3c9a...
That is the whole scheme. No signing, no timestamp, no canonical string to build, nothing to get subtly wrong at three in the morning.
v1 used HMAC-SHA1 request signing with auth[url], auth[time], and auth[hash]. Between the exact URL match, the five minute clock window, and the concatenation order, that scheme produced more support tickets than the rest of the API combined. v2 drops it. Transport security is TLS's job, and TLS is better at it.
What a token is#
A token is bound to one user and one company, and carries scopes.
The company is not something you send with a request. It is a property of the token. This is deliberate: it makes it structurally impossible to write a record into the wrong company because of a bug in your code, which is the kind of mistake that is very expensive to unwind in accounting data.
If you integrate with several companies, issue one token per company and key them in your own config.
Scopes#
Scopes are area:access pairs.
| Access | Grants |
|---|---|
<area>:read | List and retrieve, including sub-resources like /balance and /stock |
<area>:write | Create and update |
<area>:cancel | Cancel or reverse an already-posted record |
An area covers a group of related resources, not one table. There are over a hundred resources; one scope each would be several hundred checkboxes on the token screen, and nobody reads several hundred checkboxes carefully. So resources are grouped by what you actually want to be able to do, and by how much damage the permission could do if it were wrong.
Every reference page states the exact scope its endpoints need, so you never have to work it out.
| Scope | Covers |
|---|---|
customers | Customers, their ship-to addresses and contacts. |
vendors | Vendors, their types and order-from addresses. |
orders | Sales orders, service orders and AR quotes, with their lines. |
invoices | AR invoices and their lines. Posting an invoice writes to the ledger. |
payments | Payments applied to invoices, and daily closes. |
shipments | Shipments against an order. |
purchaseorders | Purchase orders and what was received against them. |
bills | Vendor bills and their lines. |
estimating | Quotes from the Estimating module, and their price lists. |
items | Items, their vendors, options and special pricing. |
glaccounts | The chart of accounts, categories, cost centers and cost codes. |
gltransactions | Raw journal entries. Bypasses the document layer - granted only on request. |
banking | Bank accounts, deposits, card accounts and card transactions. |
assets | Fixed assets and their categories. |
jobs | Construction jobs, draws, sites and projects. |
properties | Buildings, units and leases. |
projects | The whole Team Projects module: projects, tasks, time and everything under them. |
payroll | Employees, departments and pay types. Employee records are read-only and minimal. |
tax | Tax groups, exemptions, classes and use codes. Separate from general setup because tax configuration changes what customers are charged. |
setup | The shared lookups documents point at: terms, salespeople, categories, carriers, work types, statuses and the rest. |
reference | Countries, zones, time zones and carriers-of-the-world. Read-only, static. |
users | Read-only lookup of users, so an id can be resolved to a name. |
GET /me lists exactly what your token holds, so you never have to guess.
Grant the narrowest set that does the job
A storefront pushing orders and reading stock needs orders:write and items:read. It does not need gltransactions:write, and granting that turns a bug in a sync loop into a bug in your general ledger.
Three of these lines are drawn deliberately:
taxis separate fromsetup, because tax configuration changes what your customers are charged and a payment term does not.gltransactionsis on its own, because it bypasses the document layer and writes journal entries directly.invoicesis separate fromorders, because posting an invoice writes to the ledger and saving an order does not.
Cancelling is a separate grant
invoices:write does not let you void an invoice. Creating a document and cancelling a posted one have very different consequences: cancelling changes what has already been reported, and for some documents it writes to the general ledger.
This matters most for the integration you are most likely to build. A storefront should create invoices all day and never be able to void one, because a bug in a sync loop that voids posted invoices is a much worse afternoon than one that creates duplicates.
Posting to the ledger is off by default
gltransactions:write posts raw journal entries, bypassing the document layer entirely. It is not granted unless you ask for it, and most integrations never need it: creating an invoice already writes the correct GL entries for you.
If you find yourself wanting it to correct something an earlier call got wrong, open a ticket instead. That is usually a sign the document endpoint is missing a field, and patching the ledger by hand underneath your own documents is how a set of books stops reconciling.
Scopes are the whole permission model#
Two checks run on every request:
- The user the token belongs to must be a supervisor in that token's company. This gates who may hold a token at all.
- The token's scopes must include the scope the endpoint requires. This is what bounds the token.
There is deliberately no third check against the NolaPro screens' Advanced User Access Rights. Scopes are resource-shaped and the screens' rights are page-shaped, and having both meant a 403 you could not diagnose from the response alone. So: if a call returns 403, read error.code — it tells you which of the two it was.
The supervisor requirement is per company. A supervisor in one company holding a token for another gets 403 forbidden, because the check is against the token's own company — not a global flag.
Because a token's user is always a supervisor, the token's scopes are the only thing limiting it. Grant the narrowest set that does the job, and issue a second token rather than widening the first.
Rotation#
Tokens do not expire by default. You can set an expiry when you create one.
To rotate without downtime:
- Create a second token with the same scopes and a label that says what it replaces.
- Deploy the new token to your integration.
- Confirm traffic has moved. The token list on My Info (or Admin → user edit for someone else) shows last-used date and IP per token.
- Revoke the old token.
Revocation takes effect immediately on the next request. There is no cache to wait out.
When authentication fails#
| Status | Code | What happened |
|---|---|---|
401 | unauthorized | No header, malformed header, or a token that does not exist |
401 | token_revoked | The token was revoked |
401 | token_expired | The token passed its expiry date |
403 | insufficient_scope | Valid token, but it lacks the scope this endpoint needs |
403 | forbidden | Valid token, but its user is not a supervisor in that company (or is inactive) |
403 | feature_not_licensed | The resource needs a NolaPro module this install is not licensed for — Printshop, say. Not an API-wide gate: the rest of the API keeps working |
Every 401 carries the standard challenge, and a 403 that is a scope refusal names the scope you need:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="NolaPro API v2", error="invalid_token"
HTTP/1.1 403 Forbidden WWW-Authenticate: Bearer realm="NolaPro API v2", error="insufficient_scope", scope="invoices:cancel"
The error= values are the registered RFC 6750 ones, so invalid_token covers revoked and expired alike. The narrower reason is in the body's code, which is what you should branch on.
401 means fix the token. 403 means fix permissions. They are deliberately distinct so your retry logic can tell "this will never work" from "this needs a new token".
Transport#
Use HTTPS. A bearer token is a password in a header, and over plain HTTP it is readable by anything on the path.
The API does not currently refuse a plain-HTTP request, so a misconfigured client will keep working and keep leaking. Nothing warns you. Point your base URL at https:// and treat any token that has been sent over http:// as compromised: revoke it and issue a new one.
Do not put tokens in query strings. They end up in server logs, browser history, and referrer headers. The Authorization header is the only supported place.