Coming from v1
v1 is the original NolaPro API: XML responses, HMAC-signed requests, /rest/{controller}/{action}/ URLs. It was written in 2007 and it still runs.
v1 is not being switched off. There is no shutdown date and none is planned. It runs on its own codebase, on its own URL, against the same database. If your integration works, you do not have to touch it.
Move when you want something v2 has: better errors, no request signing, JSON, or an endpoint that only exists in v2.
What changed, and why#
| v1 | v2 | |
|---|---|---|
| Format | XML, <rsp stat="ok"> envelope | JSON |
| Auth | HMAC-SHA1 over concatenated fields | Authorization: Bearer |
| Company | auth[companyid] in every request | A property of the token |
| URLs | /rest/customer/view/123 | /!/api/v2/customers/123 |
| Errors | Numeric codes | String codes, plus legacy_code |
| Search | General expression parser | Named filter allow-list |
| Deletes | Various | None. Cancel actions instead |
| Rules | Enforced in the API's own model layer | Enforced by NolaPro's models |
The last row is the one that matters most. v1 has its own set of models with their own validation, which means a rule tightened in NolaPro did not automatically apply to the API. v2 calls the application's own model code, so there is one implementation of every rule and it cannot drift.
Mapping your calls#
| v1 | v2 |
|---|---|
GET /rest/customer/view/123 | GET /!/api/v2/customers/123 |
GET /rest/customer/search/companyname/Acme/like | GET /!/api/v2/customers?name=Acme |
GET /rest/customer/get_balance/123 | GET /!/api/v2/customers/123/balance |
GET /rest/item/get_item_stats/33/2 | GET /!/api/v2/items/33/stock?location=2 |
GET /rest/item/get_item_cost/33/2 | GET /!/api/v2/items/33/stock?location=2 |
POST /rest/customer/save | POST /!/api/v2/customers or PATCH /api/v2/customers/{id} |
POST /rest/arinvoice/save | POST /!/api/v2/invoices |
POST /rest/gltransaction/save | POST /!/api/v2/gltransactions |
get_item_stats and get_item_cost collapse into one call because splitting them only ever meant two round trips for one question.
Field names#
v2 uses names that describe what the field is, not what the column has historically been called. Where a name changed, the reference tables show the v1 name underneath.
| v2 | v1 | Note |
|---|---|---|
name | companyname | |
customerid | orderbycompanyid | On invoices and orders. The v1 name is a long-standing source of confusion: it is the customer, not an order id. |
shiptoid | shiptocompanyid |
Things that will surprise you#
Money is a string. v1 returned numbers in XML text nodes that most clients coerced to floats. v2 returns "367.6500" and expects the same from you. If you send 367.65 you get 400 invalid_type.
There is no save that does both. v1's save inserted when there was no id and updated when there was. v2 splits them: POST creates, PATCH updates. This was a real source of accidental duplicate records when an id went missing.
Detail rows are a normal array. v1 wanted data[arinvoicedetail][1][field] with 1-based indexes. v2 takes a JSON array called lines.
Search is not general any more. If your integration built expressions and sent them as conditions, there is no v2 equivalent. Tell us which filters you need and they get added by name. This is a real loss of flexibility and a deliberate one: the expression parser could not be made safe.
Company comes from the token. Remove auth[companyid] from your code. If you integrate with several companies, issue one token per company.
Running both at once#
Nothing stops you. They are separate codebases against the same database.
The sane migration is per flow, not big-bang: move your stock reads to v2, leave invoice creation on v1 until you have watched the reads behave for a few weeks, then move the writes.
Because v2 goes through NolaPro's models and v1 does not, a rule tightened in the application applies to your v2 calls and not your v1 calls. In practice that means v2 occasionally rejects something v1 accepts. That is v2 working correctly, and it is worth knowing before you conclude the new API is broken.
flexfield generically, because a generic read would return the definitions this endpoint deliberately hides.