Attachments

Files attached to a record: the signed grant letter on an invoice, the W-4 on an employee, the photo on an asset. Reached through the parent (GET|POST /invoices/{id}/attachments) and then by their own id. Hand-built, and deliberately without a flat list: the same table holds printed cheques, portal printouts and company logos, and a generic read would publish them. Every operation takes the PARENT's scope - uploading to an invoice needs invoices:write, removing an employee file needs payroll:cancel - so there is no attachments scope to grant. A file whose parent your token cannot read is 404, never 403: the flat routes do not name the parent, and a 403 naming payroll:read would say what the file is.

The object#

FieldTypeDescription
idread-onlyintegerNolaPro id of the file (fileitem.id).
resourceread-onlyrequest-onlystring(40)The parent resource this file hangs off (invoices, customers, ...), resolved from filedefinitionid. Not a column: it is here so a flat GET /attachments/{id} tells you where the file belongs.
tablerecordidread-onlyintegerThe parent record's id - the invoice, customer or employee the file is attached to. Set by the nested POST, never from the body.
filedefinitionidintegerWhich of the parent's file lists this belongs to. Most parents have one; a customer has four (general files, B2B-visible files, a before picture, an after picture). Defaults to the parent's main list on create; the parent's reference page lists the accepted ids. Cannot be changed after upload. Id only - definitions are static configuration with no endpoint of their own.
filenamestring(255)The file name as it shows on the screen. Taken from the multipart part unless a filename field overrides it; trimmed, reduced to its base name (a path separator is refused), never empty. Cannot be changed after upload - the screens have no rename either.
datatyperead-onlystring(100)Media type as the uploader declared it (the part's Content-Type, sniffed when absent). Served back as Content-Type on /content. Not validated against the bytes, exactly as the screen stores it.
sizeread-onlyintegerBytes, measured from what was stored.
filedateread-onlystringWhen it was uploaded. There is no last-changed date on a file; the content never changes.
notestextFree text shown beside the file. Stored as text, no practical length limit.
filecategoryidintegerOptional category the file is filed under. The target of filecategories; a category from another company is 422 unknown_value.
filecategorystring(255)The category by name instead of id (filecategory.name, case-insensitive). Read back beside filecategoryid.

Endpoints#

GET/attachments/{id} 200404

Retrieve an attachment's metadata.

Requires scope parent:read.

Parameters
NameInTypeNotes
idrequiredpathintegerThe attachment id.
When it fails
StatusCodeMeaning
404not_foundNo attachment with that id that your token can see. Also the answer for a file whose parent is in another company, whose parent your token lacks the scope to read, or whose file list is not one the API exposes (a printed cheque, a logo) - the three are deliberately indistinguishable.
curl \
  'https://acme.nolapro.com/!/api/v2/attachments/104' \
  -H 'Authorization: Bearer $NP_TOKEN'
Response 200
{
    "id": 4471,
    "resource": "invoices",
    "tablerecordid": 1043,
    "filedefinitionid": 8,
    "filename": "grant-letter-signed.pdf",
    "datatype": "application/pdf",
    "size": 184322,
    "filedate": "2026-09-21T15:04:11Z",
    "notes": "Signed copy received 21 Sep",
    "filecategoryid": 3,
    "filecategory": "Receipts"
}
PATCH/attachments/{id} 200400404409422

Update the note or category.

Requires scope parent:write.

Parameters
NameInTypeNotes
idrequiredpathintegerThe attachment id.
Idempotency-KeyheaderstringMakes 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.
Body
FieldTypeDescription
notestextFree text shown beside the file. Stored as text, no practical length limit.
filecategoryidintegerOptional category the file is filed under. The target of filecategories; a category from another company is 422 unknown_value.
filecategorystring(255)The category by name instead of id (filecategory.name, case-insensitive). Read back beside filecategoryid.
When it fails
StatusCodeMeaning
400unknown_parameterA field that is not on the schema.
404not_foundNo attachment with that id that your token can see. Also the answer for a file whose parent is in another company, whose parent your token lacks the scope to read, or whose file list is not one the API exposes (a printed cheque, a logo) - the three are deliberately indistinguishable.
409stale_recordIf-Match did not match the current ETag.
422read_only_fieldfilename, datatype, size or filedate in the body. None of them can change after upload.
curl -X PATCH \
  'https://acme.nolapro.com/!/api/v2/attachments/104' \
  -H 'Authorization: Bearer $NP_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: your-unique-key' \
  -d '{
    "notes": "Signed copy received 21 Sep",
    "filecategoryid": 3,
    "filecategory": "Receipts"
}'
Response 200
{
    "id": 4471,
    "resource": "invoices",
    "tablerecordid": 1043,
    "filedefinitionid": 8,
    "filename": "grant-letter-signed.pdf",
    "datatype": "application/pdf",
    "size": 184322,
    "filedate": "2026-09-21T15:04:11Z",
    "notes": "Signed copy received 21 Sep",
    "filecategoryid": 3,
    "filecategory": "Receipts"
}
DELETE/attachments/{id} 200403404409

Remove the file.

Removes the file from storage. This cannot be undone. fileitem has no cancel flag, so unlike every document DELETE this one is not a cancel: the row, the stored bytes and any cloud object are gone. A second DELETE answers 404, which a client retrying after a lost response should read as done. Send If-Match - it is the only protection there is. If what you want is an audit trail, cancel the parent document instead. Needs the parent's :cancel scope, which its :write does not imply.

Requires scope parent:cancel.

Parameters
NameInTypeNotes
idrequiredpathintegerThe attachment id.
Idempotency-KeyheaderstringMakes 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.
When it fails
StatusCodeMeaning
403insufficient_scopeToken has the parent's :write but not its :cancel.
404not_foundNo attachment with that id that your token can see. Also the answer for a file whose parent is in another company, whose parent your token lacks the scope to read, or whose file list is not one the API exposes (a printed cheque, a logo) - the three are deliberately indistinguishable.
409stale_recordIf-Match did not match the current ETag.
curl -X DELETE \
  'https://acme.nolapro.com/!/api/v2/attachments/104' \
  -H 'Authorization: Bearer $NP_TOKEN'
Response 200
{
    "id": 4471,
    "resource": "invoices",
    "tablerecordid": 1043,
    "filedefinitionid": 8,
    "filename": "grant-letter-signed.pdf",
    "datatype": "application/pdf",
    "size": 184322,
    "filedate": "2026-09-21T15:04:11Z",
    "notes": "Signed copy received 21 Sep",
    "filecategoryid": 3,
    "filecategory": "Receipts"
}
GET/attachments/{id}/content 200304404

Download the file.

The bytes, with Content-Type from datatype, Content-Length, a Content-Disposition carrying the file name, and an ETag. The content never changes, so If-None-Match with the ETag you were given answers 304 with no body. Binary, so it cannot be carried inside a /batch results array: asking for it there is 415 not_batchable.

Requires scope parent:read.

Parameters
NameInTypeNotes
idrequiredpathintegerThe attachment id.
When it fails
StatusCodeMeaning
304Not modified - If-None-Match matched.
404not_foundNo attachment with that id that your token can see. Also the answer for a file whose parent is in another company, whose parent your token lacks the scope to read, or whose file list is not one the API exposes (a printed cheque, a logo) - the three are deliberately indistinguishable.
curl \
  'https://acme.nolapro.com/!/api/v2/attachments/104/content' \
  -H 'Authorization: Bearer $NP_TOKEN'