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.

Coming from v1

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.

AccessGrants
<area>:readList and retrieve, including sub-resources like /balance and /stock
<area>:writeCreate and update
<area>:cancelCancel 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.

ScopeCovers
customersCustomers, their ship-to addresses and contacts.
vendorsVendors, their types and order-from addresses.
ordersSales orders, service orders and AR quotes, with their lines.
invoicesAR invoices and their lines. Posting an invoice writes to the ledger.
paymentsPayments applied to invoices, and daily closes.
shipmentsShipments against an order.
purchaseordersPurchase orders and what was received against them.
billsVendor bills and their lines.
estimatingQuotes from the Estimating module, and their price lists.
itemsItems, their vendors, options and special pricing.
glaccountsThe chart of accounts, categories, cost centers and cost codes.
gltransactionsRaw journal entries. Bypasses the document layer - granted only on request.
bankingBank accounts, deposits, card accounts and card transactions.
assetsFixed assets and their categories.
jobsConstruction jobs, draws, sites and projects.
propertiesBuildings, units and leases.
projectsThe whole Team Projects module: projects, tasks, time and everything under them.
payrollEmployees, departments and pay types. Employee records are read-only and minimal.
taxTax groups, exemptions, classes and use codes. Separate from general setup because tax configuration changes what customers are charged.
setupThe shared lookups documents point at: terms, salespeople, categories, carriers, work types, statuses and the rest.
referenceCountries, zones, time zones and carriers-of-the-world. Read-only, static.
usersRead-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:

  • tax is separate from setup, because tax configuration changes what your customers are charged and a payment term does not.
  • gltransactions is on its own, because it bypasses the document layer and writes journal entries directly.
  • invoices is separate from orders, 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:

  1. The user the token belongs to must be a supervisor in that token's company. This gates who may hold a token at all.
  2. 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.

Note

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.

Careful

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:

  1. Create a second token with the same scopes and a label that says what it replaces.
  2. Deploy the new token to your integration.
  3. 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.
  4. Revoke the old token.

Revocation takes effect immediately on the next request. There is no cache to wait out.

When authentication fails#

StatusCodeWhat happened
401unauthorizedNo header, malformed header, or a token that does not exist
401token_revokedThe token was revoked
401token_expiredThe token passed its expiry date
403insufficient_scopeValid token, but it lacks the scope this endpoint needs
403forbiddenValid token, but its user is not a supervisor in that company (or is inactive)
403feature_not_licensedThe 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.

Careful

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.