API Reference

Fifteen endpoints for talking to NetSuite from your local machine. Same auth, same shape, same shared secret. Sample responses below are illustrative — exact field values vary by account.

Base URL and authentication

The Chartstone server binds to http://127.0.0.1:<port> where <port> is the port shown in the control panel (you can pin it under Local HTTP server → Port). Every request requires a Bearer token — your shared secret, visible in the same panel.

Auth header

Authorization: Bearer <your-shared-secret>

Requests without a valid token return 401. Requests to a disabled endpoint return 403. Lite-tier callers that exceed the per-launch counters return 429 with the same usage headers described below.

All JSON endpoints accept Accept: application/toon for a token-optimized response format that's 25–50% cheaper to feed to LLM contexts.

Before writing a single line of client code, you can hit any endpoint right from the control panel. The Test endpoint dialog (Local HTTP server → Test…) sends a real request through the same auth and returns the live response — useful for confirming that your shape, body, and Accept headers are right.

The Test endpoint dialog showing a POST /reports call and the live JSON response.

Response headers

Every response — including /health, error responses, and 429 rate-limit replies — carries a set of usage headers so callers can budget their calls and detect tier without parsing the body.

Usage headers (every response)

X-MaxQueries:    30          # cap for /suiteql
X-QueriesRun:    7           # /suiteql calls used this launch

X-MaxReports:    15          # cap for /search, /report, /report-info
X-ReportsRun:    2           # …calls used this launch

X-MaxScripts:    15          # cap for /restlet and /script
X-ScriptsRun:    0           # …calls used this launch

X-ScriptsEnabled: true       # whether /script is enabled under Endpoints → Manage

A Max value of 999999 means unlimited — that's the Pro tier. Lite values are 30 / 15 / 15 per app launch (counters reset each time the user relaunches Chartstone). When any X-*Run equals its X-Max* counterpart, subsequent calls in that bucket return 429.

AI agents in particular should call /health on their first turn — the body lists which endpoints are reachable, and the headers reveal the per-launch budget so the agent knows whether to conserve calls or splurge.

Every request — whether from your script, an AI agent, or the built-in tester — lands in the in-memory ring buffer and the on-disk JSONL log. View request log (in the Local HTTP server card) shows method, path, status, and duration so you can debug what your client actually sent.

The Request log dialog listing recent requests with timestamp, status, method, path, and duration.

GET/docs

Interactive Swagger UI for the entire API. Paste the URL into a browser and click any endpoint to inspect its request/response shape, sample bodies, and headers. /docs/json returns the raw OpenAPI 3.0 document for machine consumers (codegen, AI agents that want a typed schema, importing into Postman / Insomnia / Bruno, etc.).

Open the explorer

http://127.0.0.1:<port>/docs

Fetch the OpenAPI document

curl http://127.0.0.1:<port>/docs/json

No bearer token required for either path. The documentation is intentionally open to anyone who can reach the loopback interface — that's only the logged-in user's own machine. The actual NetSuite-touching endpoints below all still require the bearer token.

GET/health

Lightweight ping. Returns server port + start time, NetSuite session presence, and the current per-endpoint gate state. Useful for clients that want to discover the API surface before calling.

Sample request

curl http://127.0.0.1:58480/health \
  -H "Authorization: Bearer $CHARTSTONE_SECRET"

Sample response

{
  "status": "ok",
  "port": 58480,
  "startedAt": "2026-05-04T19:06:06.928Z",
  "netsuite": {
    "sessionActive": true,
    "cookieCount": 30
  },
  "endpoints": {
    "enabled": [
      "/health", "/session", "/session/permissions",
      "/suiteql", "/search", "/reports", "/report-info",
      "/report", "/page", "/record-xml", "/record-json",
      "/records-catalog", "/records-catalog/schema", "/restlet"
    ],
    "disabled": ["/script"]
  }
}

GET/session

Active NetSuite session context: account, user, role, environment classification, the role’s NetSuite-side permission set, and a fresh cookie liveness check. AI agents and scripts use this on their first turn to plan: which account am I on? which role? what can my role actually do? is the session still alive?

Permissions come from a SuiteQL fetch against RolePermissions that runs once per role per session, is cached in-process, and is eager-warmed during the “Use this account” confirm flow — so the first /session call after confirm is already populated. The fetch goes through the internal SuiteQL helper and does not count toward the Lite-tier queries quota.

Sample request

curl http://127.0.0.1:58480/session \
  -H "Authorization: Bearer $CHARTSTONE_SECRET"

Sample response (permissions populated)

{
  "active": true,
  "accountId": "TD3016323",
  "envType": "PRODUCTION",
  "companyName": "Acme Co",
  "user": {
    "id": "42",
    "name": "Tim Dietrich",
    "email": "tim@example.com"
  },
  "role": {
    "id": "3",
    "name": "Administrator",
    "permissions": [
      { "permkey": "TRAN_SALESORD", "name": "Sales Order", "level": 3, "levelLabel": "Edit" },
      { "permkey": "LIST_CUSTJOB",  "name": "Customers",   "level": 4, "levelLabel": "Full" }
    ],
    "permissionsFetchedAt": "2026-05-07T12:00:00.500Z",
    "permissionsError": null
  },
  "sessionActive": true,
  "cookieCount": 7,
  "capturedAt": "2026-05-07T12:00:00.000Z"
}

Sample response (permissions fetch failed)

"role": {
  "id": "3",
  "name": "Administrator",
  "permissions": null,
  "permissionsFetchedAt": null,
  "permissionsError": { "reason": "permission-denied" }
}

active is the convenience boolean — true when an accountId is configured and a NetSuite cookie is currently in the jar. Identity fields (user, role.id, role.name, envType, companyName, capturedAt) come from the snapshot taken when the user last clicked Use this account, so they can lag if the user changes role inside NetSuite without re-confirming. sessionActive and cookieCount are always live. role.permissions is refreshed on confirm and held in-process otherwise.

On a permissions fetch failure, role.permissions is null and role.permissionsError.reason is one of permission-denied (the current role can’t query RolePermissions), not-logged-in, no-account, invalid-role-id, or unknown. Permission levels are NetSuite’s native scale — 0=None, 1=View, 2=Create, 3=Edit, 4=Full.

GET/session/permissions

Chartstone-side permissions for this caller: the active subscription tier, which endpoints are reachable right now, and how much of the per-launch usage budget remains. Pure in-process read; safe to call frequently. Pairs with /session — that one tells you who you are; this one tells you what you can do.

Sample request

curl http://127.0.0.1:58480/session/permissions \
  -H "Authorization: Bearer $CHARTSTONE_SECRET"

Sample response (Lite)

{
  "tier": "lite",
  "endpoints": {
    "enabled": [
      "/health", "/session", "/session/permissions",
      "/suiteql", "/search", "/reports", "/report-info",
      "/report", "/page", "/record-xml", "/record-json",
      "/records-catalog", "/records-catalog/schema", "/restlet"
    ],
    "disabled": ["/script"]
  },
  "usage": {
    "queries": { "count": 5, "max": 30 },
    "reports": { "count": 0, "max": 15 },
    "scripts": { "count": 0, "max": 15 }
  }
}

Sample response (Pro)

{
  "tier": "pro",
  "endpoints": { "enabled": [...], "disabled": [...] },
  "usage": {
    "queries": { "count": 5, "max": null },
    "reports": { "count": 2, "max": null },
    "scripts": { "count": 0, "max": null }
  }
}

usage.{kind}.max is null when unlimited (Pro) and an integer on Lite. Counters reset on every app launch. Only /suiteql, /report, and /script increment counters; every other endpoint is unmetered on both tiers.

POST/suiteql

Run a SuiteQL query against your NetSuite session. Auto-paginated; results come back as column-keyed objects.

Request body

{
  "query":   "SELECT id, companyname FROM customer WHERE rownum < 5",
  "maxRows": 50           // optional — caps pagination
}

Sample request

curl http://127.0.0.1:58480/suiteql \
  -H "Authorization: Bearer $CHARTSTONE_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"query":"SELECT id, entityid, email FROM customer WHERE rownum < 3"}'

Sample response

{
  "items": [
    { "id": 1060, "entityid": "Daniel Soule", "email": "dsoule@example.com" },
    { "id": 1062, "entityid": "Dylan Earl",   "email": "dearl@example.com" }
  ],
  "count": 2,
  "pagesRun": 1,
  "capReached": false,
  "navigatedUrl": "https://<account>.app.netsuite.com/app/setup/country.nl?id=US"
}

Pagination & maxRows

By default, Chartstone walks every page NetSuite returns up to a hard ceiling of 500,000 rows (100 pages × 5,000 rows each). Most callers want this — it makes /suiteql feel like a single call rather than a paging API.

For sampling, exploration, or LLM token budgets, pass "maxRows": N on the request body (1 to 500,000). Pagination stops as soon as N rows have been collected. When the cap kicks in, capReached is true in the response — your signal that more rows likely exist beyond what was returned.

pagesRun reports how many NetSuite pages were actually fetched. navigatedUrl echoes the host page the hidden window used (your Default script page preference).

POST/restlet

Call any deployed RESTlet by script ID + deployment ID. Supports GET / POST / PUT / DELETE. The response body is whatever the RESTlet returns.

Request body

{
  "scriptId":     "customscript_my_restlet",
  "deploymentId": "customdeploy_1",
  "method":       "POST",
  "queryParams":  { "id": 123 },
  "payload":      { "action": "ping" }
}

Sample request

curl http://127.0.0.1:58480/restlet \
  -H "Authorization: Bearer $CHARTSTONE_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "scriptId":     "customscript_my_restlet",
    "deploymentId": "customdeploy_1",
    "method":       "POST",
    "payload":      { "action": "ping" }
  }'

Sample response (depends on the RESTlet)

{
  "ok": true,
  "echoed": { "action": "ping" }
}

POST/script

Run an ad-hoc SuiteScript snippet supplied as the request body. The last expression’s value is returned. Disabled by default — opt in under Endpoints → Manage.

Under the hood, Chartstone navigates a hidden NetSuite window to a host page that loads the SuiteScript runtime, then evaluates your code in that page’s context. The host must be a record-edit-style URL (country.nl, customer.nl?id=1, etc.) — center, dashboard, and portal pages don’t load the runtime and your script will fail.

Request body (raw script as text)

var runtime = require("N/runtime");
runtime.getCurrentUser().name;

Optional headers

  • X-Chartstone-Url — NetSuite URL the hidden window navigates to before running. Must be https://*.netsuite.com. Override per-call when the app-wide default isn’t accessible to your role. The app-wide default is whatever’s set under Preferences → Script host path (factory default /app/setup/country.nl?id=US, which requires Administrator).
  • X-Chartstone-Modules — Comma-separated AMD modules to preload (e.g. N/record, N/query). Once preloaded, your script can use require('N/foo') synchronously. Send the literal string none to skip preloading. Default: N/query, N/search, N/record, N/runtime, N/url, N/log.
  • X-Chartstone-Timeout-Ms — Per-call timeout in milliseconds. Range 1,000–1,800,000. Default 300,000 (5 min).

Sample request (with headers)

curl http://127.0.0.1:58480/script \
  -H "Authorization: Bearer $CHARTSTONE_SECRET" \
  -H "Content-Type: text/javascript" \
  -H "X-Chartstone-Url: /app/common/entity/customer.nl?id=1" \
  -H "X-Chartstone-Modules: N/runtime" \
  -H "X-Chartstone-Timeout-Ms: 60000" \
  --data-binary 'var runtime = require("N/runtime"); runtime.getCurrentUser().name;'

Sample response

{
  "result": "Tim Dietrich",
  "navigatedUrl": "https://<account>.app.netsuite.com/app/common/entity/customer.nl?id=1"
}

POST/reports

Enumerate the standard reports available in the current account.

Request body

{}

Sample response

{
  "reports": [
    { "reportID": 285,  "name": "A/P Aging - Detail" },
    { "reportID": 286,  "name": "A/P Aging Summary" },
    { "reportID": 273,  "name": "A/R Aging - Detail" },
    { "reportID": -202, "name": "Balance Sheet" },
    { "reportID": -203, "name": "Cash Flow Statement" }
  ]
}

POST/report-info

Inspect a specific report — its filters, columns, and metadata — without running it.

Request body

{ "reportId": 286 }

Sample response (abridged — full options arrays elided)

{
  "title": "A/P Aging Summary",
  "extractedAt": "2026-05-04T19:07:50.831Z",
  "filters": [
    {
      "fieldName": "crit_1_mod",
      "label": "Date",
      "type": "timeselector",
      "options": [
        { "value": "TODAY", "text": "today" },
        { "value": "LM",    "text": "end of last month" },
        { "value": "LFY",   "text": "end of last fiscal year" }
        /* …many more time-relative options… */
      ]
    },
    {
      "fieldName": "subsidiary",
      "label": "Subsidiary",
      "type": "select",
      "options": [ { "value": "1", "text": "SuiteStep, LLC" } ]
    }
  ],
  "columns": [ /* … */ ]
}

POST/report

Run a NetSuite standard report and return its rows. Pass filters to override defaults, or Accept: text/csv to get the raw CSV NetSuite produces.

Request body

{
  "reportId": 286,
  "filters": [
    { "name": "PERIOD", "value": "TY" }
  ]
}

Sample response (JSON)

{
  "reportId": 286,
  "name": "Income Statement",
  "columns": ["account", "amount"],
  "rows": [
    { "account": "Total Revenue",    "amount": 1240500.00 },
    { "account": "Total Expenses",   "amount":  812300.00 },
    { "account": "Net Income",       "amount":  428200.00 }
  ]
}

POST/record-xml

Fetch a record by its NetSuite URL and return the raw XML representation NetSuite serves.

Request body

{
  "url": "https://<account>.app.netsuite.com/app/common/entity/customer.nl?id=123"
}

Sample response

<customer>
  <id>123</id>
  <entityid>C-0123</entityid>
  <companyname>ACME, Inc.</companyname>
  ...
</customer>

POST/record-json

Same as /record-xml, but the XML is parsed into a flat JSON object for easier consumption.

Request body

{
  "url": "https://<account>.app.netsuite.com/app/common/entity/customer.nl?id=123"
}

Sample response (abridged — many fields elided)

{
  "data": {
    "?xml": { "@_version": 1, "@_encoding": "UTF-8" },
    "nsResponse": {
      "record": {
        "currid": 1060,
        "entityid": "Daniel Soule",
        "companyname": null,
        "email": "dsoule@example.com",
        "billcity": "Los Angeles",
        "billstate": "CA",
        "billzip": "90041",
        "creditlimit": 300000,
        "balance": 0,
        "datecreated": "4/18/2026 11:04 am"
        /* …every field NetSuite serializes is present… */
      }
    }
  }
}

The response mirrors NetSuite's XML structure converted to JSON — you'll see ?xml and nsResponse wrappers, with the actual fields under data.nsResponse.record.

POST/records-catalog

List record types available in the current account. Useful as a schema-discovery starting point for AI agents.

Request body

{
  "action": "getRecordTypes",
  "data":   { "structureType": "FLAT" }
}

Sample response (abridged)

{
  "action": "getRecordTypes",
  "status": 200,
  "contentType": "application/json",
  "json": {
    "type": "response",
    "status": "ok",
    "data": [
      {
        "type": "RECORD_TYPE",
        "id": "BankImportHistory",
        "label": "Bank Import History",
        "detailType": "OVERVIEW",
        "isAvailable": true,
        "children": [ /* DETAIL_SPEC entries */ ]
      }
      /* …one entry per record type… */
    ]
  }
}

The full envelope (action / status / contentType / json) lets you tell whether the underlying NetSuite call succeeded independently of HTTP-level errors. Most callers can read straight from json.data.

POST/records-catalog/schema

Fetch the field schema for a specific record type. Pass shape: "headers" for a compact list, or omit it for full field metadata.

Request body

{
  "recordType": "customer",
  "shape":      "headers"
}

Sample response (abridged — one entry per record type)

{
  "recordTypes": {
    "BankImportHistory": {
      "type": "response",
      "status": "ok",
      "data": {
        "type": "RECORD_TYPE",
        "id": "BankImportHistory",
        "label": "Bank Import History",
        "recordClass": "RECORD"
      }
    },
    "RevRecAmortizationTemplate": {
      "type": "response",
      "status": "ok",
      "data": {
        "type": "RECORD_TYPE",
        "id": "RevRecAmortizationTemplate",
        "label": "Revenue Recognition Amortization Template",
        "recordClass": "AGGREGATEDRECORD"
      }
    }
    /* …other record types… */
  }
}

POST/page

Render any NetSuite page in a hidden window and extract the result as HTML, plain text, or markdown. Useful for scraping pages that don't have a clean record/RESTlet equivalent.

Request body

{
  "url":            "https://<account>.app.netsuite.com/app/center/card.nl?sc=-9",
  "mode":           "markdown",
  "expandSubtabs":  false
}

Sample response (mode: "text")

{
  "mode": "text",
  "title": "Reports - NetSuite (SuiteStep, LLC)",
  "url": "https://<account>.app.netsuite.com/app/center/card.nl?sc=-9",
  "navigatedUrl": "https://<account>.app.netsuite.com/app/center/card.nl?sc=-9",
  "fetchedAt": "2026-05-04T19:08:29.131Z",
  "text": "Help Feedback Tim Dietrich SuiteStep, LLC - Administrator Activities Payments Transactions Lists Reports Analytics Customization Documents Setup …"
}

Modes: text (plain text), html (raw HTML), or markdown (HTML converted to markdown). Pass expandSubtabs: true to walk through subtab content as well.