# Netsuite Setup

> Set up credentials for an Orderful NetSuite SuiteApp customer. Creates a per-customer `.env` file with NetSuite Token-Based Auth (account ID, consumer key/secret, token ID/secret) and an Orderful API key, then validates both connections work. Use when the user is starting work with a new NetSuite customer, needs to configure access to a customer's NetSuite and Orderful, is onboarding a customer, or says things like "/netsuite-setup", "set up a new customer", "create customer credentials", "onboard <customer>", or "I need access to <customer>'s NetSuite".

- Skill: `orderful/netsuite-setup` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add orderful/netsuite-setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/orderful/netsuite-setup/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: Orderful (https://skillmd.com/u/orderful)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/orderful/netsuite-setup

---


# Setup: Orderful NetSuite Customer Credentials

Guide the user through bootstrapping credentials for an Orderful NetSuite SuiteApp customer. At the end, the user should have a working `.env` at `~/orderful-onboarding/<customer-slug>/.env` that any other skill in this repo can read.

## Step 0 — Verify the repo install

Before doing anything else, confirm the user has cloned `orderful-netsuite-skills` and run `./install.sh`. Quick checks:

- `~/.claude/skills/netsuite-setup` should be a symlink (`ls -l ~/.claude/skills/netsuite-setup`). If it's missing, the user hasn't run `install.sh` — point them at `SETUP.md` and stop.
- `~/orderful-onboarding/` should exist (created by `install.sh`). If missing, same fix.
- `node_modules/` should exist in the repo root (also created by `install.sh`). If missing, `test-connections.mjs` in Step 5 will fail.

If any of these are missing, instruct the user to:

```sh
cd <path-to-orderful-netsuite-skills>
./install.sh
```

…and re-invoke `/netsuite-setup` after it completes. Don't try to work around a missing install — the validation step depends on it.

## Step 1 — Identify the customer

Check `~/orderful-onboarding/` for existing customer directories using `ls`. If the directory doesn't exist, that's fine — we'll create it.

Ask the user:

- **Which customer?** If existing dirs were found, list them and ask if they want to reuse one or create a new one. If creating new, ask for a slug (kebab-case, e.g., `acme-foods`, `widgetco`).

**Assume sandbox.** Onboarding usually starts in sandbox. The template defaults `ENVIRONMENT=sandbox` and has separate blocks for sandbox (`NS_SB_*`) and production (`NS_PROD_*`) NetSuite credentials — only prompt about production if the user explicitly says they're skipping sandbox.

**Prod-only customers exist.** Smaller customers on lower NetSuite tiers (SuiteFoundation, Starter) may not have a sandbox environment. Ask: "Does this customer have a NetSuite sandbox, or only production?" If prod-only, set `ENVIRONMENT=production`, fill only `NS_PROD_*` fields, and ensure all testing uses Orderful's TEST stream + the SuiteApp's `custrecord_ord_tran_testmode = T` flag to isolate test data from live transactions. See `reference/procure-to-pay.md` for the first documented prod-only case (Acme Medical, May 2026).

Do NOT ask for credential values in chat yet. We'll have the user fill them into the file directly.

## Step 2 — Scaffold the customer directory

Create `~/orderful-onboarding/<customer-slug>/` if it doesn't exist. Copy the env template from this skill's directory to `<customer-dir>/.env`:

```sh
mkdir -p ~/orderful-onboarding/<slug>
cp <path-to-this-skill>/env-template.env ~/orderful-onboarding/<slug>/.env
```

Then pre-fill `CUSTOMER_SLUG` and `CUSTOMER_NAME` in the new `.env` with the values you already know — the user shouldn't have to retype them. Leave `ENVIRONMENT=sandbox` alone.

If the `.env` already exists, do not overwrite — ask whether to edit it, scrap it, or abort.

## Step 3 — Verify the customer has what they need on the NetSuite side

Before the user starts filling in values, confirm (ask if unsure):

- **Features enabled** in the customer's NetSuite: Setup > Company > Enable Features > SuiteCloud — "Token-Based Authentication" and "REST Web Services" must both be checked.
- **Integration record** exists or will be created. If the user hasn't created one yet, point them to `INTEGRATION-RECORD-SETUP.md` in this skill's directory for the step-by-step.
- **Access token** exists or will be created for a user/role with sufficient permissions. Required role permissions are listed in `INTEGRATION-RECORD-SETUP.md` ("Required role permissions" section) — note that skills that trigger MapReduce scripts (e.g. `run-poller`) need both `SuiteScript = Full` and `SuiteScript Scheduling` on the role, which Administrator has by default but custom roles often don't. **For P2P customers with custom SuiteScript:** the integration role needs full permissions for creating/editing records (Item Receipts, Item Fulfillments, Vendor Bills, Purchase Orders) and executing custom scripts. Jordan at Acme Medical confirmed "Full grant" for the integration role — this is the expected ask for P2P. See `reference/procure-to-pay.md`.

**P2P custom fields:** P2P implementations typically require custom fields on NS records (e.g., EDI ack status on PO header, inventory availability on items). The customer must create these in NS before the custom scripts will work. During setup, ask whether the custom field list has been shared with the customer. If not, flag as a blocker — scripts are ready but can't run without the fields. See `reference/procure-to-pay.md` "Script Status" section. Note: start with the bare minimum fields (Acme Medical went from 5 fields down to 1 free-text field for 855 status after the May 14 alignment call — see lesson #21 in procure-to-pay.md).

**Review historical records first:** Before designing the output format for any inbound doc type (vendor bills, item receipts, item fulfillments), review what the customer already has in production NS. Ask the customer for examples of historical production records (e.g., existing vendor bills from the same vendor). Match that structure rather than inventing a new format. This avoids surprises for the customer's AP/receiving team. Validated May 2026 on Acme Medical 810 Vendor Bill design.

Orderful's SuiteApp does not currently ship a pre-configured integration record, so the customer must create their own.

## Step 4 — Have the user fill the template

Tell the user the full path to their new `.env` (e.g., `~/orderful-onboarding/acme-foods/.env`). Offer to open it for them (`open -t <path>` on macOS).

The template marks every required field with `<PASTE HERE>`. For sandbox onboarding, the user only needs to fill:
- The five `NS_SB_*` NetSuite sandbox fields
- `ORDERFUL_API_KEY`

The `NS_PROD_*` block can stay blank — we'll come back to it at go-live.

**Do not ask the user to paste secrets into this chat.** They should edit the file locally, save, and reply "done" or equivalent.

While waiting, you can remind them:
- NetSuite shows Consumer Key/Secret and Token ID/Secret **only once** — if they missed them, they'll need to reset and get new values.
- Orderful API keys come from the Orderful organization (`ui.orderful.com` > Organization Logo > Settings > API Credentials)

## Step 5 — Validate

Once the user confirms they've filled the file, run the validation script:

```sh
node <path-to-this-skill>/test-connections.mjs ~/orderful-onboarding/<slug>
```

The script reads `ENVIRONMENT` from the `.env` (defaults to sandbox) and picks the matching `NS_SB_*` or `NS_PROD_*` NetSuite credentials. It runs three checks:

1. **NetSuite** — a harmless `SELECT TOP 1` SuiteQL query (validates TBA + REST Web Services + role's basic data access).
2. **RESTlet** — a probe POST to the SuiteApp's agent-write RESTlet with an unknown action, carrying `authorizedBy`/`agentPlanId` (validates the SuiteApp version is current AND the role has `SuiteScript = Full`). This does *not* validate `SuiteScript Scheduling`; that perm only fails when an action like `triggerInboundPolling` actually calls `task.create()`, which `/run-poller` will surface clearly on first use.
3. **Orderful** — a small authenticated GET to `api.orderful.com` (Orderful has one global endpoint, not a separate sandbox URL).

It prints pass/fail per system.

### If NetSuite fails

Common causes, check in this order:

1. **`INVALID_LOGIN` + empty Login Audit Trail** — the request was rejected at OAuth signature validation before NetSuite ever looked up the user/token, so nothing logs. The most common cause is **realm/account-ID case mismatch**: the OAuth `realm` parameter has to match the account ID character-for-character including case. Check `Setup > Company > Company Information` for the exact case (letter-prefixed IDs like `TDxxxxxxx` are usually uppercase) and update `NS_SB_ACCOUNT_ID` / `NS_PROD_ACCOUNT_ID` to match. Other less common signature-stage failures: corrupted consumer secret (extra whitespace from copy-paste), or consumer key/secret from a different integration record than the token is bound to.
2. **`INVALID_LOGIN_ATTEMPT` / 401 + audit trail entry exists** — wrong consumer key/secret, wrong token ID/secret, or token is for the wrong integration. Open the audit trail entry; the Detail column will name the specific failure (*"Invalid token"*, *"Invalid signature"*, etc.). Re-check values and note NetSuite UI only shows them once.
3. **`INVALID_LOGIN` + audit trail says "Role does not have permission..."** — the user tied to the token doesn't have the role permissions needed. Have them check the role for "REST Web Services" and "Log in using Access Tokens" permissions.
4. **`TBA not enabled`** — feature not enabled (Step 3).
5. **Wrong account ID format** — sandbox accounts look like `1234567_SB1`; the SuiteQL URL uses `-` instead of `_` (i.e., `1234567-sb1.suitetalk.api.netsuite.com`). The test script handles this substitution automatically as long as `NS_SB_ACCOUNT_ID` (or `NS_PROD_ACCOUNT_ID`) uses underscores.

**Diagnostic shortcut:** Before digging through values, check **`Setup > Users/Roles > User Management > View Login Audit Trail`** — filter to the user the token is bound to, look for a recent failure. **Empty trail = signature/realm failure (#1).** **Trail with detail = follow the detail (#2 or #3).** This 30-second check eliminates most guessing.

### If the RESTlet check fails

1. **`404 — endpoint not found`** — the customer's installed SuiteApp version predates the agent-write RESTlet (NS-926). Have them upgrade via **My SuiteApps**. Until that's done, `/run-poller` and any other agent-write skill will not work, but the rest of onboarding is unblocked.

2. **`INSUFFICIENT_PERMISSION` — "You do not have privileges to view this page"** — counter-intuitive, but this is **most often a deployment-audience issue, not a role-permission gap**. The agent-write RESTlet ships with the SuiteApp but its deployment audience is configured per-account at install time and is easy to miss. Even an `Administrator`-role token returns this error if the audience excludes the integration's user/role.

    **Diagnose with SuiteQL** (via the `.env` creds, before touching any roles):

    ```sql
    SELECT sd.scriptid, sd.allroles, sd.allemployees, sd.status, sd.isdeployed
    FROM scriptdeployment sd
    JOIN script s ON s.id = sd.script
    WHERE s.scriptid = 'customscript_orderful_agent_write_rl'
    ```

    - **`allroles = F` AND `allemployees = F`** → audience issue. Fix: Customization → Scripting → Script Deployments → `customdeploy_orderful_agent_write_rl` → **Edit** → **Audience** tab → check **All Roles** AND **All Employees** (broadest; matches what the SuiteApp expects for its own RESTlets), OR specifically add the integration's user + role. **Save**. Effective instantly; no token regeneration.
    - **`allroles = T` AND `allemployees = T`** → audience isn't the cause; treat as a real role-permission gap → see #3.

3. **`INSUFFICIENT_PERMISSION` after audience is ruled out** — the role on the token doesn't have `SuiteScript = Full`. Add it on the role's **Setup tab** (and add `SuiteScript Scheduling` while you're there — see `INTEGRATION-RECORD-SETUP.md` "Required role permissions"). No need to regenerate the token after editing the role.

4. **Other failure** — the script prints the raw response. The probe passes on either of the RESTlet's own rejections — "Unknown action", or the `authorizedBy and agentPlanId are required` reply from a build that validates attribution first — since both prove the script executed. If it returned anything else, treat it like any other RESTlet failure and check the script execution log (Customization > Scripting > Script Deployments > "Orderful Agent Write" > Execution Log).

### If Orderful fails

1. **401 / 403 "Application with key ... not found"** — the API key is wrong or hasn't been provisioned for this org. Orderful has one global endpoint (`api.orderful.com`) and one global key per org, so there's no "sandbox vs. prod key" to mix up — if it's failing, the key is either typo'd or not issued yet. Re-check the value in `app.orderful.com` > Settings > API Keys for the right org, or ask the Orderful team.
2. **403 (other message)** — API key is recognized but lacks permission for the test endpoint. Escalate to the Orderful team.

## Step 6 — Confirm and summarize

Once all three validations pass:

- Confirm the customer directory path to the user.
- Note which connections are working.
- Offer next steps: "Want me to run the audit/inventory skill against this customer now, or are we done for today?"

Keep the summary short — the user already knows what they did.

## ISA conventions and test-injection prerequisites (FYI)

The five NetSuite credentials and the Orderful API key are everything this skill needs to validate. But there are two pieces of customer-record state that come up later — once you start running test injections from sandbox via the `inject-test-transaction` skill — and they're worth flagging during onboarding so the eventual setup is straightforward.

### Live vs test ISAs

Every Orderful trading-partner relationship has both a `liveIsaId` and a `testIsaId` per side (sender and receiver). By convention, the test ISA is the live ISA with a `T` or `QT` suffix:

| Live | Test |
|---|---|
| `4166619606` | `4166619606T` |
| `4253138601CH` | `4253138601CHQT` |
| `5146366668` | `5146366668T` |

**ISA collision** is when `liveIsaId === testIsaId` on a relationship — i.e., the customer (or their counterparty) hasn't bothered to set up a distinct test ISA. When this happens, the SuiteApp can't tell from the ISA alone whether an inbound transaction is LIVE or TEST. It still works (the SuiteApp falls back to the `stream`/`testmode` flag in the payload), but you've removed a layer of defense and made routing audits harder. Surface ISA collisions to the user during onboarding if you spot them.

### Customer-record fields

In the customer's NetSuite, two fields on the Customer (Sub-Customer) record carry the ISAs:

- `custentity_orderful_isa_id` — the **live** ISA. Always set during onboarding; the SuiteApp uses it for normal LIVE-stream traffic.
- `custentity_orderful_isa_id_test` — the **test** ISA. Often left blank or copied from the live field (which causes problems during sandbox testing).

**For sandbox test injections to work end-to-end, `custentity_orderful_isa_id_test` must match the relationship's `sender.testIsaId`.** If the values differ, the SuiteApp polls the test transaction successfully but fails to resolve it back to a NS customer — and the test fails for the wrong reason, which is hard to diagnose.

This isn't something the `netsuite-setup` skill writes — it's part of customer-record EDI configuration, handled by the `enable-customer` skill or via the SuiteApp UI. Just know that if you're about to run a test injection and the customer's test_isa is wrong, the inject-test-transaction skill will catch it and propose a fix.

### Polling-bucket pairs

Each Artika-style customer with separate sandbox and prod NetSuite instances has **two polling buckets** in Orderful — one per environment. The sandbox NS polls bucket A; the prod NS polls bucket B. They must be distinct, and TEST-stream traffic must route to the sandbox bucket — otherwise a TEST injection meant for sandbox can land in the prod NS via the prod bucket.

Per-receiver-account settings in Orderful determine which bucket TEST traffic goes to. When a customer has multiple receiver accounts (multiple subsidiaries — e.g., `Artika 4166 CA`, `Artika 5146 US`), each one's poller assignment is configured separately. It's easy to fix one and forget the others.

The `.env` template now includes optional `ORDERFUL_POLLING_BUCKET_SANDBOX` and `ORDERFUL_POLLING_BUCKET_PROD` fields. Capture both during onboarding when known — the inject-test-transaction skill uses them as a tripwire to abort if the test transaction lands in the prod bucket.

### When this matters for onboarding

You don't need to fill any of this in to validate credentials in Step 5. But if the user's stated goal is "set up so we can do test injections," gather:

- The relationship's test ISA on the partner side (e.g., for Costco → Artika 850, the sender testIsaId)
- The customer's `custentity_orderful_isa_id_test` value (and confirm it matches the above)
- Both polling bucket ids

Hand off to `enable-customer` for the customer-record wiring and `inject-test-transaction` for the actual test runs.

## Known issue: SuiteTax + REST PATCH on transactions

If the customer's NetSuite account has **SuiteTax** enabled (the newer tax framework, not legacy "Standard Tax"), `PATCH` requests against `/services/rest/record/v1/salesOrder/{id}` and other transaction record endpoints fail with:

```
HTTP 400
{
  "title": "Bad Request",
  "status": 400,
  "o:errorDetails": [{
    "detail": "Error while accessing a resource. Unable to save the transaction due to an error being reported by the tax calculation engine: A User Error Has Occurred: You have entered an Invalid Field Value salesorder for the following field: type.",
    "o:errorCode": "USER_ERROR"
  }]
}
```

This is a known incompatibility between SuiteTax and the REST record API. SuiteTax's calculation step rejects the lowercase REST API record-type literal (`salesorder`) because it expects the internal type code (`SalesOrd`). It blocks every PATCH on transactions, even harmless custom-column updates, even with `taxDetailsOverride: true`. The async PATCH (`Prefer: respond-async`) returns HTTP 202 + `progress: succeeded`, but the underlying task result body is the same 400 — the async wrapper hides the failure unless you fetch `/job/{n}/task/{n}/result`.

### Diagnostic checklist

If a contributor reports this error and asks you to "find the offending script":
1. Look at user-event scripts deployed on `salesorder` (`SELECT s.scripttype, s.name FROM script s JOIN scriptdeployment sd ON sd.script = s.id WHERE sd.recordtype = 'SALESORDER'`). Almost always you'll find none that are tax-related.
2. Look at `plugintypeimpl` and `plugintype`. If both are empty, there's no custom Tax Engine Plugin — the error is from SuiteTax core.
3. Look for installed bundles named Avalara/Vertex/Sovos. If none, it's SuiteTax core (not a third-party SuiteApp).

The error wording "tax calculation engine" misleads — it's not a custom plugin issue, it's the SuiteTax framework itself.

### Workarounds

In rough order of preference:

1. **`record.submitFields` via a server-side RESTlet (connector enhancement — not available today).** SuiteScript's `record.submitFields()` skips the full save lifecycle (no user events fire, no tax recalc), which *would* sidestep SuiteTax entirely. **But the SuiteApp's agent-write RESTlet (`customscript_orderful_agent_write_rl`) does NOT currently expose a `submitFields` action — it exposes only `triggerInboundPolling` and `reprocessTransaction`.** So this path isn't usable as-is; adding a `submitFields` action would be a connector enhancement. Until then, use one of the fallbacks below.
2. **Update via NS UI** for one-off fixes. Tedious for bulk work but bypasses REST.
3. **CSV import.** For bulk field updates on existing transactions, the import framework runs through a different code path that sometimes tolerates SuiteTax better than REST. Test in sandbox first.
4. **SOAP/SuiteTalk SOAP API.** Sometimes works where REST doesn't, but SOAP is awful and most TBA tokens aren't tested against it.

**Don't** try to disable SuiteTax on the subsidiary as a "fix" — that's a tenant-wide change with massive blast radius.

### What you can still do via REST when SuiteTax blocks you

- Read transactions (`GET`) — never blocked.
- Patch *non-transaction* records (customer, item, vendor, custom records, etc.) — never blocked.
- Trigger SuiteScript actions on transactions via the agent-write RESTlet — runs server-side, doesn't touch the REST tax-calc path.

