# 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.

Product: paige · Audience: integrator · Time: Half a day for a developer · Last verified: 2026-09-05

Canonical: https://help.ademero.com/paige/integrations/create-netsuite-vendor-bills-from-paige-with-suitescript

**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

1. In Paige, open **Settings** (the gear at the bottom of the left menu — administrators only), pick the scan job, and choose **Export**.
2. 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`.
3. Click **Test Connection**. **What you should see:** `FTP connection successful!` Connect — `Connected to FTP Server!` and a green **Connected** label.
4. 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](/paige/document-processing/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**.

1. **Host key:** from any machine, run `ssh-keyscan your.sftp.host` and copy the base64 key for the algorithm your server offers (RSA or ECDSA).
2. **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.

```javascript
/**
 * @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_items` tables 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 same `tranid` and 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

1. Process one invoice through the scan job in Paige, review it, and wait for the export.
2. Run the scheduled script once from the deployment (**Save and Execute**). **What you should see:** the execution log shows no errors.
3. 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**.

## What's next

- [Connect Paige to NetSuite](https://help.ademero.com/paige/integrations/connect-paige-to-netsuite)
- [Integrate your own system with the Paige API](https://help.ademero.com/paige/integrations/integrate-your-own-system-with-the-paige-api)
