Create NetSuite vendor bills from Paige with SuiteScript
A scheduled SuiteScript uses NetSuite's built-in N/sftp module to pull each finished invoice from Paige's SFTP export folder, creates the vendor bill from Paige's field values, and attaches the PDF from the File Cabinet — no middleware, NetSuite pulls directly.
Before you begin
- A NetSuite administrator or developer who can deploy scheduled scripts and create a credential field
- SuiteScript 2.1 and the N/sftp module (standard in NetSuite)
- An SFTP server Paige can reach, with a folder for this scan job
- A Paige scan job with an invoice document type whose fields include vendor name, invoice number, invoice date, due date, and total
At the end of this guide, a scheduled SuiteScript checks Paige's export folder every few minutes, and for each new invoice creates a vendor bill in NetSuite — vendor matched, dates and reference set, an expense line at the total — with the PDF saved to the File Cabinet and attached to the bill.
Nothing sits between the two systems. NetSuite's N/sftp module connects to your SFTP server directly, downloads the files Paige wrote, and the script does the rest with N/record and N/file.
Step 1 — Point Paige at your SFTP server
In Paige, open Settings (the gear at the bottom of the left menu — administrators only), pick the scan job, and choose Export.
Click the FTP card. Enter the FTP Address, Port
22, Username, Password, and in FTP Folder Path a folder for this scan job, such as/paige/invoices.Click Test Connection. What you should see:
FTP connection successful!Connect —Connected to FTP Server!and a green Connected label.Turn on Enable Auto Export. Under Auto Export Settings, pick your invoice type under Document Types, set Auto Field Entry Complete to Yes, and set Human Reviewed to Reviewed. Click Save.
Open port 22 on your firewall to Paige's address ranges — Outbound IP ranges for firewall allow-lists. NetSuite connects from NetSuite's own addresses, so also allow those (your NetSuite account's outbound IPs are listed in its setup documentation).
What Paige writes per document, and what the script reads:
| File | Use |
|---|---|
<name>.pdf | Saved to the File Cabinet and attached to the bill |
<name>.json | Field values (fields), document type, and the PDF's filename (metadata.pdf_file) |
<name>.xml, <name>.csv | Not needed; the script deletes or ignores them |
Paige uploads the PDF before the data files, so processing on the .json is safe.
Step 2 — Store the SFTP password and host key in NetSuite
N/sftp never takes a plain password. It takes a password GUID created through a credential field, plus the server's host key.
Host key: from any machine, run
ssh-keyscan your.sftp.hostand copy the base64 key for the algorithm your server offers (RSA or ECDSA).Password GUID: create a small Suitelet with a form that includes
form.addCredentialField({ id: 'custpage_pw', label: 'SFTP password', restrictToScriptIds: ['customscript_paige_sftp'], restrictToDomains: ['your.sftp.host'] }). Open it once, enter the SFTP password, and save the returned GUID in a script parameter of the scheduled script. The GUID is only usable by the scripts and hosts you listed.
Step 3 — Write the scheduled script
The skeleton below is SuiteScript 2.1. Fill in the constants and the field-name mapping for your scan job.
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/sftp', 'N/file', 'N/record', 'N/search', 'N/runtime', 'N/log'],
(sftp, file, record, search, runtime, log) => {
const HOST = 'your.sftp.host';
const USER = 'paige-sftp';
const DIR = '/paige/invoices';
const HOST_KEY = 'AAAAB3NzaC1yc2E...'; // from ssh-keyscan
const FOLDER_ID = 1234; // File Cabinet folder for invoice PDFs
const EXPENSE_ACCOUNT_ID = 567; // internal ID of your AP expense account
const execute = () => {
const pwdGuid = runtime.getCurrentScript().getParameter({ name: 'custscript_paige_pw_guid' });
const conn = sftp.createConnection({ username: USER, passwordGuid: pwdGuid, url: HOST, hostKey: HOST_KEY, directory: DIR });
const listing = conn.list({ path: '.' });
for (const entry of listing) {
if (entry.directory || !entry.name.endsWith('.json')) continue;
try {
processOne(conn, entry.name);
} catch (e) {
log.error({ title: 'Paige import failed: ' + entry.name, details: e });
}
}
};
const processOne = (conn, jsonName) => {
const jsonFile = conn.download({ directory: '.', filename: jsonName });
const doc = JSON.parse(jsonFile.getContents());
const f = doc.fields;
const pdfName = doc.metadata.pdf_file;
// 1. Find the vendor by name (create it, or fail loudly, per your policy).
const vendorId = findVendor(f['Vendor Name']);
// 2. Create the vendor bill.
const bill = record.create({ type: record.Type.VENDOR_BILL, isDynamic: true });
bill.setValue({ fieldId: 'entity', value: vendorId });
bill.setValue({ fieldId: 'tranid', value: f['Invoice Number'] });
bill.setValue({ fieldId: 'trandate', value: parseDate(f['Invoice Date']) });
if (f['Due Date']) bill.setValue({ fieldId: 'duedate', value: parseDate(f['Due Date']) });
bill.selectNewLine({ sublistId: 'expense' });
bill.setCurrentSublistValue({ sublistId: 'expense', fieldId: 'account', value: EXPENSE_ACCOUNT_ID });
bill.setCurrentSublistValue({ sublistId: 'expense', fieldId: 'amount', value: parseAmount(f['Total']) });
bill.setCurrentSublistValue({ sublistId: 'expense', fieldId: 'memo', value: 'Paige ' + doc.metadata.document_type });
bill.commitLine({ sublistId: 'expense' });
const billId = bill.save();
// 3. Download the PDF into the File Cabinet and attach it to the bill.
const pdf = conn.download({ directory: '.', filename: pdfName });
pdf.folder = FOLDER_ID;
pdf.name = (f['Invoice Number'] || pdfName) + '.pdf';
const fileId = pdf.save();
record.attach({ record: { type: 'file', id: fileId }, to: { type: 'vendorbill', id: billId } });
// 4. Move the processed files aside so they aren't imported twice.
for (const n of [jsonName, pdfName, jsonName.replace('.json', '.xml'), jsonName.replace('.json', '.csv')]) {
try { conn.move({ from: n, to: 'processed/' + n }); } catch (e) { /* xml/csv may not matter */ }
}
};
const findVendor = (name) => {
const res = search.create({ type: search.Type.VENDOR, filters: [['entityid', 'is', name]], columns: ['internalid'] }).run().getRange({ start: 0, end: 1 });
if (!res.length) throw new Error('Vendor not found: ' + name);
return res[0].getValue('internalid');
};
const parseDate = (s) => new Date(s); // adjust for your date format
const parseAmount = (s) => parseFloat(String(s).replace(/[^0-9.\-]/g, ''));
return { execute };
});Points to adapt:
- Vendor matching by exact name is the simplest policy. Paige's reviewer can standardize the vendor field to NetSuite's spelling; or match on a vendor's external ID if your invoices carry a vendor number Paige extracts.
- Line items: Paige's JSON includes
line_itemstables with rows and columns. Loop over them to add one expense or item line each instead of a single total line. - Dates: Paige exports values as printed. Parse according to your invoices' format.
- Idempotency: the
processed/move is what prevents duplicates. If the script fails after saving the bill but before the move, the next run would create a second bill — search for an existing bill with the sametranidand vendor before creating.
Deploy the script as a Scheduled Script, add the script parameter custscript_paige_pw_guid, and schedule it every 15 minutes. Create the processed folder on the SFTP server.
Step 4 — Test it
Process one invoice through the scan job in Paige, review it, and wait for the export.
Run the scheduled script once from the deployment (Save and Execute). What you should see: the execution log shows no errors.
In NetSuite, open Transactions > Payables > Enter Bills > List. What you should see: the bill with the vendor, dates, reference, and amount from the invoice, and the PDF under Communication > Files on the bill. On the SFTP server, the four files moved to
processed/.
When it doesn't work
| Symptom | Where to look |
|---|---|
createConnection fails | Wrong host key (must match exactly), wrong password GUID, or the GUID was created for a different script ID or domain. |
| Connection times out | NetSuite's outbound addresses aren't allowed by your firewall. |
| "Vendor not found" | The invoice's vendor spelling differs from NetSuite's. Correct it in Paige during review. |
| Bill saves but the amount is wrong | The Paige field name in the script doesn't match the scan job's field, or a currency symbol slipped through parseAmount. |
| Duplicate bills | The processed/ move failed (folder missing or no rights). Add the existing-bill check. |
| Nothing arrives from Paige | Auto-export criteria aren't met, or the invoice was processed before you enabled auto export. Export once from the Documents page with Export and tick FTP. |
If the Paige side is the problem, contact Ademero support with the scan job name and the message from Test Connection.
