Invoices
AR invoices. Lines go in the same request - see Conventions on nested writes.
The object#
| Field | Type | Description |
|---|---|---|
| idread-only | integer | NolaPro id. |
| invoicenumber | string(30) | Invoice number. Auto-assigned from the company counter when not supplied. |
| ponumber | string(30) | The customer's purchase order number. |
| wherefrom | integer | Module that raised the invoice: 2 = manual AR entry, 4 = billed from an order, fulfillment, or service order, 7 = billed from Estimating (print orders), 12 = generated by a payment-plan run, 44 = created by an RMA return. |
| orderid | integer | Orderid. Id only. |
| orderbycompanyid | integer | Orderbycompanyid. Id only. |
| shiptocompanyid | integer | Shiptocompanyid. Id only. |
| status | string | Where the invoice is in its life. Labels are Arinvoice_model::$statuses. Stored as int -1/0/1/2/3/4; never exposed or accepted (D35). unposted means no GL voucher has been written yet, so lines can still be added; everything else means the ledger has been told. One of abandoned, unposted, open, paid, writeoff, refundedcredit. |
| customerbillcode | string(20) | Customer Bill Code printed on the invoice. |
| shipcostmoney | string(4dp) | Stored as decimal(19,4). |
| invoicetotalmoney | string(4dp) | Stored as decimal(19,4). |
| invoicetermsid | integer | Invoicetermsid. Id only. |
| invoiceterms | string(30) | The verbal of the referenced invoiceterms, instead of the id. Send this or invoicetermsid, not both unless they agree. |
| salesmanid | integer | Salesmanid. Id only. |
| salesman | string(50) | The name of the referenced salesmen, instead of the id. Send this or salesmanid, not both unless they agree. |
| invoicedate | string | Invoice date - the accounting date the invoice posts under. |
| duedate | string | Date payment is due. |
| discountdate | string | Last date the early-payment discount applies. |
| discountamountmoney | string(4dp) | Stored as decimal(19,4). |
| accruedinterestmoney | string(4dp) | Stored as decimal(19,4). |
| datelastinterestcalcread-only | string | When finance-charge interest was last assessed on the invoice. |
| cancel | boolean | Cancelled. NolaPro cancels rather than deletes, so a cancelled row is still readable. |
| entrydate | string | When the record was created. |
| lastchangedateread-only | string | Last modification. Drives modifiedsince. |
| pricelevelid | integer | Pricelevelid. Id only. |
| inventorylocationid | integer | Inventorylocationid. Id only. |
| inventorylocation | string(20) | The locationcode of the referenced inventorylocations, instead of the id. Send this or inventorylocationid, not both unless they agree. |
| payment_method | string(12) | Payment method text carried in from the web store order the invoice came from. |
| shipping_method | string(32) | Shipping method text carried in from the web store order the invoice came from. |
| currency | string(10) | Currency the invoice is raised in. |
| transaction_id | string(32) | Gateway transaction id of the payment taken with the invoice (web store and POS sales). |
| customers_id | integer | Customers id. Id only. |
| customers | string(30) | The customercode of the referenced customers, instead of the id. Send this or customers_id, not both unless they agree. |
| currency_valuenumber | string(6dp) | Numeric string, 6 decimal places. Not money - do not apply currency rounding. |
| servicerepid | integer | Servicerepid. Id only. |
| servicerep | string(50) | The name of the referenced salesmen, instead of the id. Send this or servicerepid, not both unless they agree. |
| discountappliedmoney | string(4dp) | Stored as decimal(19,4). |
| payplan | boolean | True when the invoice is a payment-plan template that generates recurring invoices. |
| payplan_period | integer | Recurrence period of the payment plan: 1 = daily, 2 = weekly, 3 = monthly, 4 = quarterly, 5 = yearly; anything else repeats every payplan_days days. |
| payplan_days | integer | Interval in days between generated invoices, when payplan_period does not name a standard period. |
| payplan_next | string | Next date the payment plan generates an invoice. |
| payplan_numtimes | integer | Remaining number of invoices the plan will generate; -1 repeats indefinitely. |
| sales_categoriesid | integer | Sales categoriesid. Id only. |
| sales_categories | string(50) | The description of the referenced salescategories, instead of the id. Send this or sales_categoriesid, not both unless they agree. |
| coverage | integer | 1 prints the coverage period - the date range the recurring invoice covers - on generated invoices. |
| formnumber | string(50) | Form Number of the printed piece, for invoices billed from Estimating. |
| shiptoattn | string(100) | Attention line printed with the ship-to address. |
| old_idread-only | string(255) | Invoice id in the system this record was imported from. |
| client_po | string(30) | The customer’s PO number for the invoice. |
| curratenumber | string(10dp) | Numeric string, 10 decimal places. Not money - do not apply currency rounding. |
| arglaccountid | integer | Arglaccountid. Id only. |
| arglaccount | string(20) | The name of the referenced glaccounts, instead of the id. Send this or arglaccountid, not both unless they agree. |
| leaseid | integer | Leaseid. Id only. |
| costcodeid | integer | Cost code. |
| costcode | string(20) | Code for the referenced costcode, instead of the id. |
| delivered | integer | 1 once the invoiced goods are recorded as delivered. |
| carrierserviceid_preferred | integer | Carrier service the customer prefers for this shipment. |
| b2bhidden | boolean | True hides the invoice from the customer’s B2B portal. |
| billtrustsentread-only | string | Date the invoice was sent to the Billtrust delivery service. Maintained by the send flow, so it cannot be written through the API. |
| contactnameid | integer | Contactnameid. Id only. |
| glcategoryid | integer | Glcategoryid. Id only. |
| glcategory | string(20) | The code of the referenced glcategories, instead of the id. Send this or glcategoryid, not both unless they agree. |
| interestforinvoiceid | integer | Interestforinvoiceid. Id only. |
| interestforinvoice | string(30) | The invoicenumber of the referenced invoices, instead of the id. Send this or interestforinvoiceid, not both unless they agree. |
| invoicenotificationmethod | string(1) | How the customer wants this invoice delivered: E = email, M = mail, F = fax. Empty uses the customer default. |
| jobid | integer | Jobid. Id only. |
| taxclassid | integer | Taxclassid. Id only. |
| taxclass | string(100) | The name of the referenced taxclasses, instead of the id. Send this or taxclassid, not both unless they agree. |
| zlienid | integer | Zlienid. Id only. |
| emailsentread-only | string | Date the invoice was emailed. Maintained by the send flow, so it cannot be written through the API. |
| faxsentread-only | string | Date the invoice was faxed. Maintained by the send flow, so it cannot be written through the API. |
| isacustomerreturn | string(1) | 1 when the invoice is a credit raised for a customer return. |
| modifiedfromarinvoiceid | integer | Modifiedfromarinvoiceid. Id only. |
| modifiedfromarinvoice | string(30) | The invoicenumber of the referenced invoices, instead of the id. Send this or modifiedfromarinvoiceid, not both unless they agree. |
| paiddateread-only | string | Date the invoice was fully paid. |
| paymeth | integer | Payment method: 1 = cash, 2 = check, 3 = credit card, 4 = ACH/EFT, 5 = card terminal; 0 = on account or unset. |
| priceadjustmentforinvoiceid | integer | Priceadjustmentforinvoiceid. Id only. |
| priceadjustmentforinvoice | string(30) | The invoicenumber of the referenced invoices, instead of the id. Send this or priceadjustmentforinvoiceid, not both unless they agree. |
| rental | integer | 1 when the invoice bills a rental. |
| rentalconverttosale | boolean | True when the invoice converts a rental into a sale. |
| taxadjustmentforinvoiceid | integer | Taxadjustmentforinvoiceid. Id only. |
| taxadjustmentforinvoice | string(30) | The invoicenumber of the referenced invoices, instead of the id. Send this or taxadjustmentforinvoiceid, not both unless they agree. |
| taxexemptid | integer | Taxexemptid. Id only. |
| taxexempt | string(32) | The exemptname of the referenced taxexempts, instead of the id. Send this or taxexemptid, not both unless they agree. |
| voucherid | integer | Voucherid. Id only. |
| voucher | string(50) | The voucher of the referenced gltransactions, instead of the id. Send this or voucherid, not both unless they agree. |
| billtoshiptoid | integer | Billtoshiptoid. Id only. |
| defaultassetid | integer | Defaultassetid. Id only. |
| passthruarinvoiceid | integer | Passthruarinvoiceid. Id only. |
| passthruarinvoice | string(30) | The invoicenumber of the referenced invoices, instead of the id. Send this or passthruarinvoiceid, not both unless they agree. |
| passthrucontactid | integer | Passthrucontactid. Id only. |
| passthrucustomerbilltoshiptoid | integer | Passthrucustomerbilltoshiptoid. Id only. |
| passthrucustomerid | integer | Passthrucustomerid. Id only. |
| passthrucustomer | string(30) | The customercode of the referenced customers, instead of the id. Send this or passthrucustomerid, not both unless they agree. |
| passthrufromarinvoiceid | integer | Passthrufromarinvoiceid. Id only. |
| passthrufromarinvoice | string(30) | The invoicenumber of the referenced invoices, instead of the id. Send this or passthrufromarinvoiceid, not both unless they agree. |
| specialpurposeread-only | string(15) | Marker on system-generated invoices with a special purpose - e.g. CCFEE for credit-card fee invoices - so sales reports can exclude them. |
| externalid | string(100) | Your own key. Scoped to your company. |
| lines | array of InvoiceLine | |
| _metaread-only | object | Present only when the server has something to report, e.g. the invoice posted but the customer is now over their credit limit. |
| affectsinventory | boolean | Whether this invoice withdraws stock. Leave it out. Omitted, it is derived, and derived is right in every normal case:
- Invoice raised from an order or shipment - stock already left at the shipment, so the invoice moves nothing. Sending true here is rejected: it would withdraw the same units twice.
- Direct invoice with stocked lines - it withdraws, because nothing else ever will.
Send false only when importing history whose stock was already adjusted by another route. The response then carries an inventory_not_adjusted warning, because an invoice that sold goods and moved no stock is a discrepancy someone will chase later. |
Endpoints#
Notes on an invoice and its lines.
Operational context that lives with the document - why a price was changed, what a customer asked for, what to tell the driver.
Notes on the invoice as a whole and notes against individual lines come back in one list; level says which a row is, because NolaPro stores the two separately and their ids are independent.
hide keeps a note off the printed invoice.
Requires scope invoices:read.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | The invoice id. |
| Status | Code | Meaning |
|---|---|---|
| 422 | unknown_value | No such parent record in this company. |
curl \ 'https://acme.nolapro.com/!/api/v2/invoices/104/notes' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/104/notes'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.get( "https://acme.nolapro.com/!/api/v2/invoices/104/notes", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices/104/notes', { method: 'GET', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var res = await http.GetAsync( "https://acme.nolapro.com/!/api/v2/invoices/104/notes"); res.EnsureSuccessStatusCode();
List invoices.
Requires scope invoices:read.
| Name | In | Type | Notes |
|---|---|---|---|
| status | query | string | Restrict to one status. |
| orderbycompanyid | query | string | Restrict to one orderbycompanyid. |
| shiptocompanyid | query | string | Restrict to one shiptocompanyid. |
| inventorylocationid | query | string | Restrict to one inventorylocationid. |
| salesmanid | query | string | Restrict to one salesmanid. |
| jobid | query | string | Restrict to one jobid. |
| orderid | query | string | Restrict to one orderid. |
| ponumber | query | string | ponumber. Partial match, case-insensitive. |
| invoicenumber | query | string | Partial match, case insensitive. |
| cancel | query | string | Defaults to false. Pass true or any. |
| modifiedsince | query | string | RFC 3339 UTC timestamp. Returns only records changed since then, including cancelled ones - so cancel defaults to any rather than false when this is used. The response carries _meta.synced_through; store it and send it back next time. |
| externalid | query | string | Exact match on your own key. |
| include | query | string | Comma-separated extras to embed. Only custom_fields is available: the extra fields this install has defined on the record. Off by default, and an unrecognised value is refused rather than ignored. See Conventions. |
| Status | Code | Meaning |
|---|---|---|
| 403 | insufficient_scope | Token lacks read scope. |
curl \ 'https://acme.nolapro.com/!/api/v2/invoices' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.get( "https://acme.nolapro.com/!/api/v2/invoices", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices', { method: 'GET', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var res = await http.GetAsync( "https://acme.nolapro.com/!/api/v2/invoices"); res.EnsureSuccessStatusCode();
Create.
Requires scope invoices:write.
| Name | In | Type | Notes |
|---|---|---|---|
| Idempotency-Key | header | string | Makes the write safely retryable. Any unique string per logical operation; a UUID is fine.
The failure this prevents is specific: the request succeeds, the response is lost to a timeout, you retry, and the customer is billed twice. With a key, the retry returns the original response - same status, same body, same id - and creates nothing.
The body is part of the key's identity. Reusing a key with a different body is a bug on your side, not a retry, and answers 409 idempotency_replay rather than silently handing back the old response. Keys are remembered for 24 hours. |
| Field | Type | Description |
|---|---|---|
| customeridrequired | integer | Who is being invoiced. |
| date | string | Invoice date. Defaults to today. |
| post | boolean | Post the invoice to the general ledger. Defaults to true. Send false to create it as a DRAFT: no GL voucher is written, so lines can still be added with POST /invoicelines or changed with PATCH /invoicelines/{id}. Post it later by cancelling and re-raising, or build it fully before posting. Once an invoice carries a voucher its lines are closed to further change. |
| duedate | string | Payment due date. Defaults to the invoice date. |
| ponumber | string(30) | The customer's PO number. |
| shiptoid | integer | Ship-to company, if different from the customer. |
| number | string(30) | Invoice number. Leave it out and NolaPro allocates the next one. |
| externalid | string(100) | Your own identifier for this record. Stored verbatim and returned on reads; GET /<resource>?externalid=... finds it again. |
| linesrequired | array of object | At least one line. The tax and the total are derived by NolaPro - do not send them. |
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A required field was missing. |
| 422 | period_closed | The GL period containing the date is closed. |
| 422 | rule_violation | NolaPro refused the document. message carries its reason. |
| 422 | shipto_mismatch | The shiptoid belongs to a different customer. |
| 422 | unknown_value | A reference did not resolve in this company. |
curl -X POST \ 'https://acme.nolapro.com/!/api/v2/invoices' \ -H 'Authorization: Bearer $NP_TOKEN' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: your-unique-key' \ -d '{ "customerid": 104, "date": "2026-08-02", "post": false, "duedate": "2026-08-02", "externalid": "crm-8842", "lines": [ { "quantity": "3.000000", "price": "125.0000" } ] }'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => json_encode([ 'customerid' => 104, 'date' => '2026-08-02', 'post' => false, 'duedate' => '2026-08-02', 'externalid' => 'crm-8842', 'lines' => [ [ 'quantity' => '3.000000', 'price' => '125.0000', ], ], ]), ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.post( "https://acme.nolapro.com/!/api/v2/invoices", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, json={ "customerid": 104, "date": "2026-08-02", "post": False, "duedate": "2026-08-02", "externalid": "crm-8842", "lines": [ { "quantity": "3.000000", "price": "125.0000", }, ], }, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, body: JSON.stringify({ customerid: 104, date: "2026-08-02", post: false, duedate: "2026-08-02", externalid: "crm-8842", lines: [ { quantity: "3.000000", price: "125.0000", }, ], }), } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var body = new { customerid = 104, date = "2026-08-02", post = false, duedate = "2026-08-02", externalid = "crm-8842", lines = new[] { new { quantity = "3.000000", price = "125.0000", }, }, }; var req = new HttpRequestMessage(HttpMethod.Post, "https://acme.nolapro.com/!/api/v2/invoices") { Content = JsonContent.Create(body), }; req.Headers.Add("Idempotency-Key", "your-unique-key"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
{
"invoicenumber": "INV-88421",
"total": "125.0000",
"posted": false,
"voucherid": 104
}
Create many.
Requires scope invoices:write.
| Name | In | Type | Notes |
|---|---|---|---|
| Idempotency-Key | header | string | Makes the write safely retryable. Any unique string per logical operation; a UUID is fine.
The failure this prevents is specific: the request succeeds, the response is lost to a timeout, you retry, and the customer is billed twice. With a key, the retry returns the original response - same status, same body, same id - and creates nothing.
The body is part of the key's identity. Reusing a key with a different body is a bug on your side, not a retry, and answers 409 idempotency_replay rather than silently handing back the old response. Keys are remembered for 24 hours. |
| Field | Type | Description |
|---|---|---|
| customeridrequired | integer | Who is being invoiced. |
| date | string | Invoice date. Defaults to today. |
| post | boolean | Post the invoice to the general ledger. Defaults to true. Send false to create it as a DRAFT: no GL voucher is written, so lines can still be added with POST /invoicelines or changed with PATCH /invoicelines/{id}. Post it later by cancelling and re-raising, or build it fully before posting. Once an invoice carries a voucher its lines are closed to further change. |
| duedate | string | Payment due date. Defaults to the invoice date. |
| ponumber | string(30) | The customer's PO number. |
| shiptoid | integer | Ship-to company, if different from the customer. |
| number | string(30) | Invoice number. Leave it out and NolaPro allocates the next one. |
| externalid | string(100) | Your own identifier for this record. Stored verbatim and returned on reads; GET /<resource>?externalid=... finds it again. |
| linesrequired | array of object | At least one line. The tax and the total are derived by NolaPro - do not send them. |
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request was malformed. |
| 422 | period_closed | The GL period containing the date is closed. |
| 422 | rule_violation | NolaPro refused the document. message carries its reason. |
| 422 | shipto_mismatch | The shiptoid belongs to a different customer. |
| 422 | unknown_value | A reference did not resolve in this company. |
curl -X POST \ 'https://acme.nolapro.com/!/api/v2/invoices/batch' \ -H 'Authorization: Bearer $NP_TOKEN' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: your-unique-key' \ -d '[ { "customerid": 104, "date": "2026-08-02", "post": false, "duedate": "2026-08-02", "externalid": "crm-8842", "lines": [ { "quantity": "3.000000", "price": "125.0000" } ] } ]'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/batch'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => json_encode([ [ 'customerid' => 104, 'date' => '2026-08-02', 'post' => false, 'duedate' => '2026-08-02', 'externalid' => 'crm-8842', 'lines' => [ [ 'quantity' => '3.000000', 'price' => '125.0000', ], ], ], ]), ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.post( "https://acme.nolapro.com/!/api/v2/invoices/batch", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, json=[ { "customerid": 104, "date": "2026-08-02", "post": False, "duedate": "2026-08-02", "externalid": "crm-8842", "lines": [ { "quantity": "3.000000", "price": "125.0000", }, ], }, ], ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices/batch', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, body: JSON.stringify([ { customerid: 104, date: "2026-08-02", post: false, duedate: "2026-08-02", externalid: "crm-8842", lines: [ { quantity: "3.000000", price: "125.0000", }, ], }, ]), } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var body = new[] { new { customerid = 104, date = "2026-08-02", post = false, duedate = "2026-08-02", externalid = "crm-8842", lines = new[] { new { quantity = "3.000000", price = "125.0000", }, }, }, }; var req = new HttpRequestMessage(HttpMethod.Post, "https://acme.nolapro.com/!/api/v2/invoices/batch") { Content = JsonContent.Create(body), }; req.Headers.Add("Idempotency-Key", "your-unique-key"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
{
"results": [
{
"index": 1,
"status": 1
}
]
}
Retrieve one record.
Requires scope invoices:read.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | The record id. |
| include | query | string | Comma-separated extras to embed. Only custom_fields is available: the extra fields this install has defined on the record. Off by default, and an unrecognised value is refused rather than ignored. See Conventions. |
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found | No record with that id. |
curl \ 'https://acme.nolapro.com/!/api/v2/invoices/104' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/104'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.get( "https://acme.nolapro.com/!/api/v2/invoices/104", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices/104', { method: 'GET', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var res = await http.GetAsync( "https://acme.nolapro.com/!/api/v2/invoices/104"); res.EnsureSuccessStatusCode();
{
"status": "abandoned",
"lines": [
{
"itemid": 3391,
"item": "WIDGET-1",
"description": "Bracket, 4in galvanised",
"quantity": "24.000000",
"price": "14.2500",
"taxable": true
}
]
}
Update.
Requires scope invoices:write.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | The record id. |
| Idempotency-Key | header | string | Makes the write safely retryable. Any unique string per logical operation; a UUID is fine.
The failure this prevents is specific: the request succeeds, the response is lost to a timeout, you retry, and the customer is billed twice. With a key, the retry returns the original response - same status, same body, same id - and creates nothing.
The body is part of the key's identity. Reusing a key with a different body is a bug on your side, not a retry, and answers 409 idempotency_replay rather than silently handing back the old response. Keys are remembered for 24 hours. |
| Field | Type | Description |
|---|---|---|
| ponumber | string(30) | The customer's own PO reference. Amendable whatever the invoice has done. |
| duedate | string | When payment is expected. Unposted invoices only - returns 422 once posted. |
| externalid | string(100) | Your own identifier for this record. |
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request was malformed. |
| 404 | not_found | No such record, or it belongs to another company. |
| 409 | stale_record | If-Match did not match; someone else changed it first. |
| 422 | rule_violation | NolaPro refused the change. message carries its reason. |
curl -X PATCH \ 'https://acme.nolapro.com/!/api/v2/invoices/104' \ -H 'Authorization: Bearer $NP_TOKEN' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: your-unique-key' \ -d '{ "ponumber": "INV-88421", "duedate": "2026-08-01" }'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/104'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PATCH', CURLOPT_POSTFIELDS => json_encode([ 'ponumber' => 'INV-88421', 'duedate' => '2026-08-01', ]), ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.patch( "https://acme.nolapro.com/!/api/v2/invoices/104", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, json={ "ponumber": "INV-88421", "duedate": "2026-08-01", }, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices/104', { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, body: JSON.stringify({ ponumber: "INV-88421", duedate: "2026-08-01", }), } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var body = new { ponumber = "INV-88421", duedate = "2026-08-01", }; var req = new HttpRequestMessage(HttpMethod.Patch, "https://acme.nolapro.com/!/api/v2/invoices/104") { Content = JsonContent.Create(body), }; req.Headers.Add("Idempotency-Key", "your-unique-key"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
Cancel.
Requires scope invoices:cancel.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | The record id. |
| Idempotency-Key | header | string | Makes the write safely retryable. Any unique string per logical operation; a UUID is fine.
The failure this prevents is specific: the request succeeds, the response is lost to a timeout, you retry, and the customer is billed twice. With a key, the retry returns the original response - same status, same body, same id - and creates nothing.
The body is part of the key's identity. Reusing a key with a different body is a bug on your side, not a retry, and answers 409 idempotency_replay rather than silently handing back the old response. Keys are remembered for 24 hours. |
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The request was malformed. |
| 409 | cannot_cancel | Something already depends on this document - a payment, a shipment, an invoice. |
| 422 | period_closed | The current GL period is closed, so a reversal has nowhere to post. |
| 422 | unknown_value | No such record in this company. |
curl -X DELETE \ 'https://acme.nolapro.com/!/api/v2/invoices/104' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/104'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'DELETE', ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.delete( "https://acme.nolapro.com/!/api/v2/invoices/104", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices/104', { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var req = new HttpRequestMessage(HttpMethod.Delete, "https://acme.nolapro.com/!/api/v2/invoices/104"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
Post this invoice to the general ledger.
Writes the GL voucher for a DRAFT invoice - one created with post: false.
Until it is posted an invoice has no voucher, which is exactly what lets POST /invoicelines and PATCH /invoicelines/{id} change it. Posting closes it: the ledger has been told what the invoice is worth, so its lines stop being amendable.
An already-posted invoice answers 409 rather than writing a second voucher.
Requires scope invoices:write.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | |
| Idempotency-Key | header | string | Makes the write safely retryable. Any unique string per logical operation; a UUID is fine.
The failure this prevents is specific: the request succeeds, the response is lost to a timeout, you retry, and the customer is billed twice. With a key, the retry returns the original response - same status, same body, same id - and creates nothing.
The body is part of the key's identity. Reusing a key with a different body is a bug on your side, not a retry, and answers 409 idempotency_replay rather than silently handing back the old response. Keys are remembered for 24 hours. |
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found | No invoice with that id in this company. |
| 409 | invoice_already_posted | Already posted. Nothing was written a second time. |
| 422 | rule_violation | NolaPro refused the document. message carries its reason. |
| 422 | unknown_value | A reference did not resolve in this company. |
| 422 | period_closed | The GL period the document posts into is closed, so the voucher has nowhere to go. |
curl -X POST \ 'https://acme.nolapro.com/!/api/v2/invoices/104/post' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/104/post'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.post( "https://acme.nolapro.com/!/api/v2/invoices/104/post", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices/104/post', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var req = new HttpRequestMessage(HttpMethod.Post, "https://acme.nolapro.com/!/api/v2/invoices/104/post"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
Printed invoice as a PDF
The invoice exactly as NolaPro prints it, including any attachments the Document Center is set to combine.
Answers application/pdf, not JSON. Because the body is binary it cannot be carried inside a /batch results array; asking for it there returns 415 not_batchable.
A draft is refused with 409 not_posted: the totals on an unposted invoice can still change, so there is no document to print yet. The screens do not offer it either.
Requires scope invoices:read.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | The invoice id. |
| Status | Code | Meaning |
|---|---|---|
| 409 | The invoice is still a draft. |
curl \ 'https://acme.nolapro.com/!/api/v2/invoices/104/pdf' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/104/pdf'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.get( "https://acme.nolapro.com/!/api/v2/invoices/104/pdf", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices/104/pdf', { method: 'GET', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var res = await http.GetAsync( "https://acme.nolapro.com/!/api/v2/invoices/104/pdf"); res.EnsureSuccessStatusCode();
List files attached to an invoice.
The attachments on this invoice, newest first. Accepted filedefinitionid values: 8 (default 8). Metadata only - each file's bytes are at GET /attachments/{id}/content.
Requires scope invoices:read.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | The invoice id. |
| filedefinitionid | query | integer | Only files in this list. Accepted filedefinitionid values: 8 (default 8). |
| filecategoryid | query | integer | Only files filed under this category. |
| filename | query | string | Partial match on the file name, case insensitive. |
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found | No invoice with that id in this company. |
curl \ 'https://acme.nolapro.com/!/api/v2/invoices/104/attachments' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/104/attachments'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.get( "https://acme.nolapro.com/!/api/v2/invoices/104/attachments", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/invoices/104/attachments', { method: 'GET', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var res = await http.GetAsync( "https://acme.nolapro.com/!/api/v2/invoices/104/attachments"); res.EnsureSuccessStatusCode();
Attach a file to an invoice.
multipart/form-data with one file part; see the AttachmentUpload schema for the other parts. Accepted filedefinitionid values: 8 (default 8). Answers 201 with the attachment's metadata and Location: /attachments/{id}. Check _meta.warnings for datatype_truncated and stored_under_parent_company.
Requires scope invoices:write.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | The invoice id. |
| Idempotency-Key | header | string | Makes the write safely retryable. Any unique string per logical operation; a UUID is fine.
The failure this prevents is specific: the request succeeds, the response is lost to a timeout, you retry, and the customer is billed twice. With a key, the retry returns the original response - same status, same body, same id - and creates nothing.
The body is part of the key's identity. Reusing a key with a different body is a bug on your side, not a retry, and answers 409 idempotency_replay rather than silently handing back the old response. Keys are remembered for 24 hours. |
| Field | Type | Description |
|---|---|---|
| filerequired | string | The file. Exactly one part named file; a second one, or file[], is 400 invalid_request so an idempotent retry can never half-succeed. Empty is refused. Over the cap (the smaller of APIV2_MAX_UPLOAD_BYTES, 64 MB by default, and PHP's upload_max_filesize / post_max_size) is 413 payload_too_large naming the limit that applied. |
| filename | string(255) | Overrides the part's file name. |
| notes | text | Free text shown beside the file. |
| filedefinitionid | integer | Only when the parent accepts more than one list. Anything the parent does not accept is 422 unknown_value naming the accepted ids. |
| filecategoryid | integer | Category by id. |
| filecategory | string(255) | Category by name. Sent with filecategoryid and disagreeing is 422 conflicting_reference. |
| Status | Code | Meaning |
|---|---|---|
| 400 | unknown_parameter | A form field that is not on the AttachmentUpload schema. |
| 400 | invalid_type | filedefinitionid or filecategoryid that is not an integer. |
| 404 | not_found | No invoice with that id in this company. |
| 409 | upload_limit_reached | This file list allows a fixed number of files on an invoice and it is full. Remove one first. |
| 413 | payload_too_large | The file is over the cap. The body names the limit that applied. |
| 415 | multipart_required | The body was not multipart/form-data. |
| 422 | unknown_value | filedefinitionid is not one this invoice accepts, or the category does not exist in this company. |
curl -X POST \ 'https://acme.nolapro.com/!/api/v2/invoices/104/attachments' \ -H 'Authorization: Bearer $NP_TOKEN' \ -H 'Idempotency-Key: your-unique-key' \ -F 'file=@grant-letter-signed.pdf' \ -F 'filename=grant-letter-signed.pdf' \ -F 'notes=Signed copy received 21 Sep' \ -F 'filedefinitionid=8' \ -F 'filecategory=Receipts'
$fields = [ 'filename' => 'grant-letter-signed.pdf', 'notes' => 'Signed copy received 21 Sep', 'filedefinitionid' => '8', 'filecategory' => 'Receipts', ]; $fields['file'] = new CURLFile('grant-letter-signed.pdf'); $ch = curl_init('https://acme.nolapro.com/!/api/v2/invoices/104/attachments'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN'), 'Idempotency-Key: your-unique-key'], CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $fields, ]); $res = json_decode(curl_exec($ch), true);
import os, requests with open("grant-letter-signed.pdf", "rb") as fh: r = requests.post( "https://acme.nolapro.com/!/api/v2/invoices/104/attachments", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}", "Idempotency-Key": "your-unique-key"}, files={"file": fh}, data={ "filename": "grant-letter-signed.pdf", "notes": "Signed copy received 21 Sep", "filedefinitionid": "8", "filecategory": "Receipts", }, ) r.raise_for_status()
import { openAsBlob } from 'node:fs';
const form = new FormData();
form.append('file', await openAsBlob('grant-letter-signed.pdf'), 'grant-letter-signed.pdf');
form.append('filename', "grant-letter-signed.pdf");
form.append('notes', "Signed copy received 21 Sep");
form.append('filedefinitionid', "8");
form.append('filecategory', "Receipts");
const res = await fetch(
'https://acme.nolapro.com/!/api/v2/invoices/104/attachments',
{
method: 'POST',
headers: { Authorization: `Bearer ${process.env.NP_TOKEN}`, 'Idempotency-Key': 'your-unique-key' },
body: form,
}
);
const data = await res.json();
var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); using var form = new MultipartFormDataContent(); form.Add(new StreamContent(File.OpenRead("grant-letter-signed.pdf")), "file", "grant-letter-signed.pdf"); form.Add(new StringContent("grant-letter-signed.pdf"), "filename"); form.Add(new StringContent("Signed copy received 21 Sep"), "notes"); form.Add(new StringContent("8"), "filedefinitionid"); form.Add(new StringContent("Receipts"), "filecategory"); var req = new HttpRequestMessage(HttpMethod.Post, "https://acme.nolapro.com/!/api/v2/invoices/104/attachments") { Content = form }; req.Headers.Add("Idempotency-Key", "your-unique-key"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();