{
  "openapi": "3.0.3",
  "info": {
    "title": "WRF Drupal API — NetSuite Integration",
    "version": "1.0.0",
    "description": "This is the **inbound** API: the surface NetSuite calls on waterrf.org.\n(Activity records that Drupal pushes *back* into NetSuite are a separate,\noutbound OAuth 1.0a integration and are not documented here.)\n\n## 60-second quickstart\n\n1. `POST /oauth/token` with `grant_type=client_credentials` → get an `access_token`.\n2. Send it as `Authorization: Bearer <access_token>` on every request.\n3. Append `?_format=json` to **every** URL. Without it you get `406 Not Acceptable`.\n4. Send `Content-Type: application/json` on every POST/PATCH. Without it you get `415`.\n5. Tokens live **300 seconds (5 minutes)**. There is no refresh token on this\n   grant — when it expires, request a new one.\n\n## Before anything else: is it the WAF or is it Drupal?\n\nBoth environments sit behind Cloudflare, and Cloudflare currently answers\nnon-browser clients with a **managed JavaScript challenge**. Verified\n2026-09-01 from an unallowlisted network: `GET /oauth/jwks` and\n`POST /oauth/token` both returned `403` with an HTML body on test *and*\nproduction, for every `User-Agent` tried.\n\nA WAF `403` and a Drupal `403` are the same status code and completely\ndifferent problems:\n\n| | WAF challenge | Drupal denial |\n|---|---|---|\n| **Header (decisive)** | `cf-mitigated: challenge` + `server: cloudflare` | no `cf-mitigated`; `server: nginx` |\n| Body | HTML, `<title>Just a moment...</title>` | JSON `{\"message\": \"…\"}` — **or** Drupal's HTML access-denied page |\n| Happens on | any path, including `/oauth/token` | only access-controlled paths |\n| Fix | WAF skip rule / allowlist the caller's egress IPs | grant permissions or fix the scope |\n| You can fix it | no — needs the Cloudflare admin | yes — Drupal config |\n\nDo **not** use \"HTML means WAF\" as the test. Drupal serves an HTML\naccess-denied page too — `GET /oauth/userinfo` without a token returns\n`403` HTML from Drupal, no WAF involved. Only the `cf-mitigated` header\nsettles it, so always capture response headers (`curl -D -`).\n\nSuiteScript's `N/https` cannot execute JavaScript, so it can never satisfy a\nchallenge. If NetSuite's calls are being challenged, the integration is\nblocked at the edge and no amount of credential rotation will help. Confirm\nthe Cloudflare zone administrator — not Acquia Support — that these paths\nare skipped: `/api*`, `/openapi/*`, `/oauth/*`, `/.well-known/jwks.json`,\n`/node*`, `/taxonomy/term*`, `/entity/node_type/*`.\n\nExcluding only `/api*` is not enough. `/api` is the site's own Swagger UI\npage; the spec it loads (`/openapi/rest`) and every endpoint it calls live\noutside `/api`. Ready-to-apply rules are in `cloudflare/` in this repo.\n\nRun `./health-check.sh test` — its first check distinguishes these two cases\nfor you before it looks at credentials at all.\n\n## Read this before you debug anything\n\n| Fact | Consequence |\n|---|---|\n| A Cloudflare challenge returns `403` **HTML** | Not a credential problem. See the section above. |\n| Access tokens expire after **300 s** | A long-running SuiteScript job must re-request a token mid-run, not cache one. |\n| `client_credentials` issues **no refresh token** | `grant_type=refresh_token` will fail. Re-authenticate instead. |\n| The `default` scope is bound to the **`administrator`** role | The Drupal user behind the consumer must hold that role or every write returns `403`. |\n| `_format=json` is a **query parameter**, not a header | `Accept: application/json` alone is not enough. |\n| `PUT` is silently rewritten to `PATCH` | Legacy NetSuite scripts sending `PUT` still work (`wrf_restapi_put`). |\n| Node writes require `\"type\": [{\"target_id\": \"...\"}]` in the body | Omitting the bundle on POST **or** PATCH returns `422`. |\n| `X-CSRF-Token` is **not** required | CSRF applies only to cookie auth. Bearer auth is stateless. |\n| CORS allows only named origins | Browser testing works from `https://wrf-api.robs.ws`, not from `file://`. |\n| All REST-created nodes are re-attributed to a service account | Author/revision user is forced to the configured service account UID, so `uid` in your payload is ignored. |\n\n## Browser \"Try it out\" — testing mode\n\n*Try it out* is **enabled**. Two conditions have to hold for it to work, and\nboth are outside this document:\n\n1. **Serve these docs from an allowlisted origin.** CORS now permits\n   `https://wrf-api.robs.ws` and `https://sandbox.waterrf.org` alongside the\n   existing `wrftechlink` origins, with the `Authorization` header and the\n   `GET`/`POST`/`PATCH`/`PUT` methods. Opening this page from `file://`\n   sends `Origin: null` and will always fail — use the hosted URL.\n2. **The Cloudflare WAF must not challenge the API paths.** CORS governs\n   what the *browser* permits; the WAF decides whether the request reaches\n   Drupal at all. Until the skip rule described above is in place, *Try it\n   out* will fail with an opaque network error while `curl` from an\n   allowlisted host succeeds.\n\nA failed *Try it out* shows only \"TypeError: Failed to fetch\" — the browser\nwithholds the real reason. Reproduce with `curl -i` to see the actual status\nand headers before concluding anything.\n\n### Do not paste production secrets into the Authorize dialog\n\nThe `clientCredentials` flow in the *Authorize* dialog sends your\n`client_secret` from the browser and keeps the resulting token in\n`localStorage`. That is acceptable for **test/sandbox** credentials on a\nmachine you control. Use `curl` or `health-check.sh` for production —\nnever a browser page.\n\n`curl`, `health-check.sh`, and SuiteScript's `N/https` remain the primary\npath: they are server-to-server and unaffected by CORS entirely.\n\n## Environments\n\n| Environment | Base URL | NetSuite side |\n|---|---|---|\n| Test | `https://e3test.waterrf.org` | Sandbox (`9336228-SB1`) |\n| Production | `https://www.waterrf.org` | Production (`9336228`) |\n\nCredentials are **not** shared between environments. A `401 invalid_client`\nis most often a production secret pointed at test, or the reverse.\n"
  },
  "servers": [
    {
      "url": "https://e3test.waterrf.org",
      "description": "Test — use this for all integration work and troubleshooting"
    },
    {
      "url": "https://www.waterrf.org",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Obtain and inspect OAuth2 access tokens. Everything else on this page\ndepends on getting this right first.\n"
    },
    {
      "name": "Health",
      "description": "Cheap, side-effect-free calls, in the order that isolates a fault fastest:\n\n1. `GET /oauth/jwks` — is the edge letting me through at all? (no credentials)\n2. `GET /entity/node_type/{type}` — is REST serving JSON? (also anonymous)\n3. `POST /oauth/token` — can I get a token?\n4. `GET /oauth/userinfo` — does Drupal accept the token? **This is the\n   authentication proof.**\n5. `GET /oauth/debug` — does the token carry the `administrator` role?\n"
    },
    {
      "name": "Content",
      "description": "Create and update Project and Resource nodes."
    },
    {
      "name": "Taxonomy",
      "description": "Create and update taxonomy terms referenced by content."
    }
  ],
  "security": [
    {
      "bearerAuth": [

      ]
    },
    {
      "oauth2": [
        "default"
      ]
    }
  ],
  "paths": {
    "/oauth/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Get an access token",
        "operationId": "getToken",
        "security": [

        ],
        "description": "Exchanges consumer credentials for a short-lived bearer token using the\n**client credentials** grant.\n\nThe body must be `application/x-www-form-urlencoded`. Sending JSON here\nis the single most common setup mistake — it returns\n`400 unsupported_grant_type` because Drupal never sees `grant_type`.\n\n### curl\n\n```bash\ncurl -sS -X POST 'https://e3test.waterrf.org/oauth/token' \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  -d 'grant_type=client_credentials' \\\n  -d 'client_id=YOUR_CLIENT_ID' \\\n  -d 'client_secret=YOUR_CLIENT_SECRET' \\\n  -d 'scope=default'\n```\n\n### SuiteScript 2.1\n\n```javascript\ndefine(['N/https', 'N/encode'], (https, encode) => {\n  const BASE = 'https://e3test.waterrf.org';\n\n  function getToken() {\n    const res = https.post({\n      url: BASE + '/oauth/token',\n      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n      body: {\n        grant_type: 'client_credentials',\n        client_id: 'YOUR_CLIENT_ID',\n        client_secret: 'YOUR_CLIENT_SECRET',\n        scope: 'default'\n      }\n    });\n    if (res.code !== 200) {\n      throw new Error('Token request failed: ' + res.code + ' ' + res.body);\n    }\n    return JSON.parse(res.body).access_token;\n  }\n\n  return { getToken };\n});\n```\n\nBecause the token is valid for only 300 seconds, fetch it immediately\nbefore the write batch it authorises — do not stash it in a script\nparameter or cache between scheduled runs.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type",
                  "client_id",
                  "client_secret"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "client_credentials"
                    ],
                    "description": "Must be `client_credentials`. No other grant is enabled for the NetSuite consumer."
                  },
                  "client_id": {
                    "type": "string",
                    "description": "Consumer UUID from `/admin/config/services/consumer`. Environment-specific."
                  },
                  "client_secret": {
                    "type": "string",
                    "format": "password",
                    "description": "Consumer secret. Environment-specific."
                  },
                  "scope": {
                    "type": "string",
                    "default": "default",
                    "description": "The `default` scope. Omitting it falls back to the consumer's\nconfigured scopes, which is usually the same thing — send it\nexplicitly so failures are unambiguous.\n"
                  }
                }
              },
              "example": {
                "grant_type": "client_credentials",
                "client_id": "2f9c1b7e-4d3a-4f21-9c8e-11a2b3c4d5e6",
                "client_secret": "••••••••••••••••",
                "scope": "default"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request. `unsupported_grant_type` almost always means the\nbody was sent as JSON instead of form-encoded.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                },
                "examples": {
                  "unsupported_grant_type": {
                    "value": {
                      "error": "unsupported_grant_type",
                      "error_description": "The authorization grant type is not supported by the authorization server.",
                      "hint": "Check that all required parameters have been provided"
                    }
                  },
                  "invalid_scope": {
                    "value": {
                      "error": "invalid_scope",
                      "error_description": "The requested scope is invalid, unknown, or malformed",
                      "hint": "Check the `default` scope"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`invalid_client` — wrong `client_id`/`client_secret`, or credentials\nfrom the other environment. Verify which environment you are hitting\nbefore rotating anything.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                },
                "example": {
                  "error": "invalid_client",
                  "error_description": "Client authentication failed"
                }
              }
            }
          },
          "500": {
            "description": "Usually a key-material problem: the OAuth private/public key pair at\n`../private/private.key` is missing, unreadable, or has wrong\npermissions on the environment. Not a client-side fault — escalate.\n"
          }
        }
      }
    },
    "/oauth/debug": {
      "get": {
        "tags": [
          "Health",
          "Authentication"
        ],
        "summary": "Inspect the token you just used",
        "operationId": "debugToken",
        "description": "Echoes back the identity, roles, and scopes Drupal resolved from your\nbearer token. This is the fastest way to distinguish the three failure\nmodes that all look alike from NetSuite:\n\n- **Bad token** → `401`\n- **Valid token, under-privileged user** → `403`\n- **Valid token, correct user** → `200` with roles listed\n\nRequires the `debug simple_oauth tokens` permission on the consumer's\nuser. If you get a `403` here but writes succeed, the permission is\nsimply not granted — that is a documentation-convenience gap, not an\nintegration fault.\n\n```bash\ncurl -sS 'https://e3test.waterrf.org/oauth/debug?_format=json' \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "Token is valid. Confirm `roles` contains `administrator`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "description": "Drupal user ID behind the consumer."
                    },
                    "roles": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "expires": {
                      "type": "integer",
                      "description": "Unix timestamp of token expiry."
                    }
                  }
                },
                "example": {
                  "id": 1,
                  "roles": [
                    "authenticated",
                    "administrator"
                  ],
                  "scopes": [
                    "default"
                  ],
                  "expires": 1788206498
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Token valid, but the user lacks `debug simple_oauth tokens`."
          }
        }
      }
    },
    "/oauth/userinfo": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Who am I",
        "operationId": "userinfo",
        "description": "OpenID Connect user-info endpoint. A lighter alternative to `/oauth/debug`\nthat needs no special permission — any authenticated token works. Use it\nto prove a token is live when `/oauth/debug` is returning `403`.\n\n```bash\ncurl -sS 'https://e3test.waterrf.org/oauth/userinfo' \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n",
        "responses": {
          "200": {
            "description": "Token is live.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sub": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/oauth/jwks": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Public keys (unauthenticated reachability check)",
        "operationId": "jwks",
        "security": [

        ],
        "description": "The only endpoint here that needs no credentials. Because of that it is\nthe correct **first** check when an integration breaks.\n\n**This path returns `301` to `/.well-known/jwks.json`.** Verified\n2026-09-01. A client that does not follow redirects sees a bare `301`\nand may misread it as a failure — use `curl -L`, or request\n`/.well-known/jwks.json` directly.\n\nInterpretation:\n\n- `200` with a `keys` array → the site is up, routing is intact, and\n  OAuth key material is deployed. The problem is your credentials.\n- `403`/`404`/`503` → the environment itself is the problem. Stop\n  debugging tokens and escalate.\n\n```bash\ncurl -sS -o /dev/null -w '%{http_code}\\n' \\\n  'https://e3test.waterrf.org/oauth/jwks'\n```\n",
        "responses": {
          "200": {
            "description": "Site reachable and OAuth keys present. Returned after following the\nredirect to `/.well-known/jwks.json`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "301": {
            "description": "Expected. Redirects to `/.well-known/jwks.json`. Follow it.\n",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "404": {
            "description": "Route missing — `simple_oauth` is disabled or not deployed on this\nenvironment. Escalate; no client change will help.\n"
          },
          "503": {
            "description": "Environment unavailable (maintenance mode, Acquia platform issue).\nRetry with backoff before escalating.\n"
          }
        }
      }
    },
    "/entity/node_type/{node_type}": {
      "get": {
        "tags": [
          "Health",
          "Content"
        ],
        "summary": "Read a content-type definition (reachability probe)",
        "operationId": "getNodeType",
        "description": "A read-only, zero-side-effect call that proves the REST layer and JSON\nserializer are working end to end.\n\n**This resource is readable anonymously.** Verified 2026-09-01: a\nrequest with no `Authorization` header returned `200`. So a `200` here\nproves the request reached Drupal and REST is functioning — it does\n**not** prove your token is valid. Use `GET /oauth/userinfo` for that.\n\nWhere it is still useful: if `/oauth/userinfo` returns `200` (token\ngood) and a node `POST` returns `403`, the fault is specifically\n**content permissions** on the consumer's user, not the token.\n\n```bash\ncurl -sS 'https://e3test.waterrf.org/entity/node_type/resource?_format=json' \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n",
        "parameters": [
          {
            "name": "node_type",
            "in": "path",
            "required": true,
            "description": "Machine name of the content type.",
            "schema": {
              "type": "string",
              "enum": [
                "resource",
                "project"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "responses": {
          "200": {
            "description": "Content type definition."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No such content type. Check the machine name."
          },
          "406": {
            "$ref": "#/components/responses/NotAcceptable"
          }
        }
      }
    },
    "/node": {
      "post": {
        "tags": [
          "Content"
        ],
        "summary": "Create a Resource or Project node",
        "operationId": "createNode",
        "description": "Creates a node. The bundle is chosen by the `type` field in the body —\nthere is no bundle in the URL.\n\n### Required in every body\n\n`type` and `title`. Everything else is optional, though a NetSuite-owned\nnode without `field_netsuite_id` is effectively orphaned from the sync\nand should be considered a bug in the calling script.\n\n### Drupal's field format\n\nEvery field value is an **array of objects**, even single-value fields:\n\n| Field kind | Shape |\n|---|---|\n| string / integer / float / boolean / datetime | `[{\"value\": ...}]` |\n| formatted text | `[{\"value\": \"<p>…</p>\", \"format\": \"full_html\"}]` |\n| entity reference | `[{\"target_id\": 123}]` |\n| multi-value | `[{\"value\": \"A\"}, {\"value\": \"B\"}]` |\n\nScalars written as bare values (`\"title\": \"Foo\"`) return `422`.\n\n### Author attribution\n\nAny `uid` you send is overwritten. `wrf_netsuite_node_presave()` detects\nthe `Bearer` header and forces owner and revision user to the configured\nservice account, then stamps the revision log with\n`Automated sync from NetSuite via REST API`. That log line is your audit\ntrail when reconciling what the integration actually wrote.\n\n### curl\n\n```bash\ncurl -sS -X POST 'https://e3test.waterrf.org/node?_format=json' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"type\":               [{\"target_id\": \"resource\"}],\n    \"title\":              [{\"value\": \"Nutrient Recovery from Sidestreams\"}],\n    \"field_netsuite_id\":  [{\"value\": \"5581102\"}],\n    \"field_product_code\": [{\"value\": \"4901\"}],\n    \"field_description\":  [{\"value\": \"<p>Project summary.</p>\", \"format\": \"full_html\"}]\n  }'\n```\n\n### SuiteScript 2.1\n\n```javascript\ndefine(['N/https'], (https) => {\n  const BASE = 'https://e3test.waterrf.org';\n\n  function createResource(token, data) {\n    const res = https.post({\n      url: BASE + '/node?_format=json',\n      headers: {\n        'Authorization': 'Bearer ' + token,\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({\n        type:               [{ target_id: 'resource' }],\n        title:              [{ value: data.name }],\n        field_netsuite_id:  [{ value: String(data.internalId) }],\n        field_product_code: [{ value: data.itemId }],\n        field_description:  [{ value: data.description, format: 'full_html' }]\n      })\n    });\n    if (res.code !== 201) {\n      throw new Error('Create failed: ' + res.code + ' ' + res.body);\n    }\n    // Keep the returned nid — you need it for every later PATCH.\n    return JSON.parse(res.body).nid[0].value;\n  }\n\n  return { createResource };\n});\n```\n\n**Store the returned `nid` in NetSuite.** Updates address nodes by `nid`,\nnot by `field_netsuite_id`; without it a re-sync creates duplicates\ninstead of updating.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ResourceNode"
                  },
                  {
                    "$ref": "#/components/schemas/ProjectNode"
                  }
                ]
              },
              "examples": {
                "resource": {
                  "summary": "Resource (a published deliverable)",
                  "value": {
                    "type": [
                      {
                        "target_id": "resource"
                      }
                    ],
                    "title": [
                      {
                        "value": "Nutrient Recovery from Wastewater Sidestreams"
                      }
                    ],
                    "status": [
                      {
                        "value": true
                      }
                    ],
                    "field_netsuite_id": [
                      {
                        "value": "5581102"
                      }
                    ],
                    "field_product_code": [
                      {
                        "value": "4901"
                      }
                    ],
                    "field_product_id": [
                      {
                        "value": 5581102
                      }
                    ],
                    "field_project_number": [
                      {
                        "value": "4901"
                      }
                    ],
                    "field_subtitle": [
                      {
                        "value": "Full technical report"
                      }
                    ],
                    "field_description": [
                      {
                        "value": "<p>Evaluates mainstream and sidestream nutrient recovery.</p>",
                        "format": "full_html"
                      }
                    ],
                    "field_available_date": [
                      {
                        "value": "2026-09-01T12:00:00"
                      }
                    ],
                    "field_members_only": [
                      {
                        "value": false
                      }
                    ],
                    "field_outbound_water_category": [
                      {
                        "value": "Wastewater"
                      },
                      {
                        "value": "Resource Recovery"
                      }
                    ],
                    "field_resource_type": [
                      {
                        "target_id": 42
                      }
                    ],
                    "field_access": [
                      {
                        "target_id": 17
                      }
                    ],
                    "field_project": [
                      {
                        "target_id": 1893
                      }
                    ]
                  }
                },
                "project": {
                  "summary": "Project (a research effort)",
                  "value": {
                    "type": [
                      {
                        "target_id": "project"
                      }
                    ],
                    "title": [
                      {
                        "value": "Sidestream Deammonification at Scale"
                      }
                    ],
                    "status": [
                      {
                        "value": true
                      }
                    ],
                    "field_netsuite_id": [
                      {
                        "value": "5580017"
                      }
                    ],
                    "field_project_number": [
                      {
                        "value": "4901"
                      }
                    ],
                    "field_werf_id": [
                      {
                        "value": "U4R14"
                      }
                    ],
                    "field_abstract": [
                      {
                        "value": "<p>Three-year evaluation across six utilities.</p>",
                        "format": "full_html"
                      }
                    ],
                    "field_project_status": [
                      {
                        "value": "FUNDED"
                      }
                    ],
                    "field_start_date": [
                      {
                        "value": "2026-01-15T00:00:00"
                      }
                    ],
                    "field_projected_completed_date": [
                      {
                        "value": "2028-06-30T00:00:00"
                      }
                    ],
                    "field_funding_amount": [
                      {
                        "value": 750000
                      }
                    ],
                    "field_research_program_type": [
                      {
                        "value": "Research"
                      }
                    ],
                    "field_web_enabled": [
                      {
                        "value": "Yes"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. The full serialized node is returned — read `nid[0].value`\nand persist it in NetSuite.\n",
            "headers": {
              "Location": {
                "description": "Canonical URL of the new node.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        }
      }
    },
    "/node/{nid}": {
      "parameters": [
        {
          "name": "nid",
          "in": "path",
          "required": true,
          "description": "Drupal node ID returned when the node was created.",
          "schema": {
            "type": "integer"
          }
        },
        {
          "$ref": "#/components/parameters/format"
        }
      ],
      "get": {
        "tags": [
          "Content"
        ],
        "summary": "Read a node",
        "operationId": "getNode",
        "description": "Returns the full serialized node. Useful for confirming that a write\nlanded and that field values look the way NetSuite intended.\n\n```bash\ncurl -sS 'https://e3test.waterrf.org/node/1893?_format=json' \\\n  -H \"Authorization: Bearer $TOKEN\"\n```\n",
        "responses": {
          "200": {
            "description": "The node.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No node with that ID."
          },
          "406": {
            "$ref": "#/components/responses/NotAcceptable"
          }
        }
      },
      "patch": {
        "tags": [
          "Content"
        ],
        "summary": "Update a node",
        "operationId": "patchNode",
        "description": "Partial update — send only the fields that changed, **plus** `type`.\nOmitting `type` returns `422` even though the bundle cannot change.\n\nFields absent from the body are left untouched. To clear a field, send\nit as an empty array: `\"field_subtitle\": []`.\n\n`PUT` is accepted: `wrf_restapi_put` rewrites the method to `PATCH`\nbefore routing, so legacy NetSuite scripts continue to work unchanged.\nNew scripts should send `PATCH` directly.\n\n### curl\n\n```bash\ncurl -sS -X PATCH 'https://e3test.waterrf.org/node/1893?_format=json' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"type\":               [{\"target_id\": \"resource\"}],\n    \"field_product_code\": [{\"value\": \"4901-A\"}],\n    \"field_description\":  [{\"value\": \"<p>Revised summary.</p>\", \"format\": \"full_html\"}]\n  }'\n```\n\n### SuiteScript 2.1\n\n```javascript\n// N/https has no patch() helper — use request() with method 'PATCH'.\nconst res = https.request({\n  method: https.Method.PATCH,\n  url: BASE + '/node/' + nid + '?_format=json',\n  headers: {\n    'Authorization': 'Bearer ' + token,\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    type:               [{ target_id: 'resource' }],\n    field_product_code: [{ value: '4901-A' }]\n  })\n});\nif (res.code !== 200) {\n  throw new Error('Update failed: ' + res.code + ' ' + res.body);\n}\n```\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ResourceNodeFields"
                  },
                  {
                    "$ref": "#/components/schemas/ProjectNodeFields"
                  }
                ]
              },
              "examples": {
                "minimal": {
                  "summary": "Change one field",
                  "value": {
                    "type": [
                      {
                        "target_id": "resource"
                      }
                    ],
                    "field_product_code": [
                      {
                        "value": "4901-A"
                      }
                    ]
                  }
                },
                "clearField": {
                  "summary": "Clear a field",
                  "value": {
                    "type": [
                      {
                        "target_id": "resource"
                      }
                    ],
                    "field_subtitle": [

                    ]
                  }
                },
                "unpublish": {
                  "summary": "Unpublish",
                  "value": {
                    "type": [
                      {
                        "target_id": "resource"
                      }
                    ],
                    "status": [
                      {
                        "value": false
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated. The full node is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No node with that ID."
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        }
      },
      "put": {
        "tags": [
          "Content"
        ],
        "summary": "Update a node (legacy alias for PATCH)",
        "operationId": "putNode",
        "deprecated": true,
        "description": "Kept only for existing NetSuite scripts. `wrf_restapi_put` rewrites the\nmethod to `PATCH` at the kernel-request level, so behaviour, body format,\nand responses are **identical to PATCH** — including the requirement to\ninclude `type`.\n\nDo not use for new work.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ResourceNodeFields"
                  },
                  {
                    "$ref": "#/components/schemas/ProjectNodeFields"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated, handled as PATCH.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        }
      }
    },
    "/taxonomy/term": {
      "post": {
        "tags": [
          "Taxonomy"
        ],
        "summary": "Create a taxonomy term",
        "operationId": "createTerm",
        "description": "Creates a term in a vocabulary. Terms back the `field_access`,\n`field_resource_type`, and `field_principle_investigator` references on\ncontent, so a term usually has to exist before the node that points at it.\n\n```bash\ncurl -sS -X POST 'https://e3test.waterrf.org/taxonomy/term?_format=json' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"vid\":  [{\"target_id\": \"topics\"}],\n    \"name\": [{\"value\": \"Nutrient Recovery\"}]\n  }'\n```\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/format"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaxonomyTerm"
              },
              "example": {
                "vid": [
                  {
                    "target_id": "topics"
                  }
                ],
                "name": [
                  {
                    "value": "Nutrient Recovery"
                  }
                ],
                "description": [
                  {
                    "value": "<p>Recovery of nitrogen and phosphorus.</p>",
                    "format": "full_html"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Read `tid[0].value` for the reference target."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        }
      }
    },
    "/taxonomy/term/{tid}": {
      "parameters": [
        {
          "name": "tid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "$ref": "#/components/parameters/format"
        }
      ],
      "get": {
        "tags": [
          "Taxonomy"
        ],
        "summary": "Read a taxonomy term",
        "operationId": "getTerm",
        "description": "Confirm a `target_id` before referencing it from a node. A node write\npointing at a non-existent term returns `422`, not `404` — so verify\nhere first when a create fails with a validation error you cannot place.\n",
        "responses": {
          "200": {
            "description": "The term."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "No term with that ID."
          },
          "406": {
            "$ref": "#/components/responses/NotAcceptable"
          }
        }
      },
      "patch": {
        "tags": [
          "Taxonomy"
        ],
        "summary": "Update a taxonomy term",
        "operationId": "patchTerm",
        "description": "Partial update. Include `vid` alongside the changed fields, for the same\nreason nodes need `type`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaxonomyTerm"
              },
              "example": {
                "vid": [
                  {
                    "target_id": "topics"
                  }
                ],
                "name": [
                  {
                    "value": "Nutrient Recovery and Reuse"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "`Authorization: Bearer <access_token>` from `POST /oauth/token`.\nValid for 300 seconds.\n"
      },
      "oauth2": {
        "type": "oauth2",
        "description": "Client credentials grant. Documented for completeness — the token\nendpoint cannot be exercised from a browser because of the CORS policy\ndescribed in the introduction.\n",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "/oauth/token",
            "scopes": {
              "default": "Full API access, bound to the `administrator` role."
            }
          }
        }
      }
    },
    "parameters": {
      "format": {
        "name": "_format",
        "in": "query",
        "required": true,
        "description": "**Mandatory on every request.** Drupal's REST layer selects its\nserializer from this parameter, not from the `Accept` header. Omit it\nand you get `406 Not Acceptable` with an empty body — the single most\nfrequently misdiagnosed error on this API.\n",
        "schema": {
          "type": "string",
          "enum": [
            "json"
          ],
          "default": "json"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, malformed, or expired bearer token.\n\nGiven the 300-second lifetime, an integration that worked five minutes\nago and now returns `401` has an **expired token**, not revoked\ncredentials. Re-request before investigating anything else.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/DrupalError"
            },
            "example": {
              "message": ""
            }
          }
        }
      },
      "Forbidden": {
        "description": "**First, check whether Drupal answered at all.** If the body is HTML and\nthe response carries `cf-mitigated: challenge`, this is the Cloudflare\nWAF, not Drupal — see \"Is it the WAF or is it Drupal?\" in the\nintroduction. A JSON `{\"message\": …}` body means Drupal answered.\n\nFor a genuine Drupal `403`: the token is valid but the user behind it may\nnot perform this operation.\n\nCheck, in order:\n1. `GET /oauth/debug` — does `roles` include `administrator`?\n2. Is the requested scope `default`? That scope is role-bound to\n   `administrator`; a different scope resolves to a user without\n   content permissions.\n3. Does the target node's content type allow the operation?\n\nA `403` on writes while `GET /entity/node_type/resource` returns `200`\nisolates the fault to content permissions rather than the token.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/DrupalError"
            },
            "example": {
              "message": "The 'administer nodes' permission is required."
            }
          }
        }
      },
      "NotAcceptable": {
        "description": "`?_format=json` was omitted. The response body is typically empty, which\nmakes this look like a network fault. It is not — add the parameter.\n"
      },
      "UnsupportedMediaType": {
        "description": "`Content-Type: application/json` was missing on a POST/PATCH. Some HTTP\nclients drop the header when the body is empty or when a helper method\nsets it implicitly; SuiteScript's `https.request()` requires you to set\nit yourself.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/DrupalError"
            },
            "example": {
              "message": "No route found that matches \"Content-Type: text/plain\""
            }
          }
        }
      },
      "Unprocessable": {
        "description": "The body reached Drupal but failed entity validation. The `message`\nnames the offending field — read it before changing anything.\n\nFrequent causes:\n- `type` (or `vid`) missing from the body.\n- A scalar sent bare instead of as `[{\"value\": …}]`.\n- `target_id` pointing at a node or term that does not exist.\n- A `list_string` value outside its allowed set — e.g.\n  `field_project_status` accepts only `CANCELLED`, `CLOSED`, `FUNDED`,\n  `UNFUNDED`, `PUBLISHED`.\n- A datetime not in `YYYY-MM-DDTHH:MM:SS` form.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/DrupalError"
            },
            "examples": {
              "missingBundle": {
                "value": {
                  "message": "Entity type 'node' does not support bundles, or no bundle was provided."
                }
              },
              "badValue": {
                "value": {
                  "message": "Unprocessable Entity: validation failed.\nfield_project_status.0.value: The value you selected is not a valid choice."
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "TokenResponse": {
        "type": "object",
        "properties": {
          "token_type": {
            "type": "string",
            "example": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "example": 300,
            "description": "Seconds until expiry. Configured at 300 on this site."
          },
          "access_token": {
            "type": "string",
            "description": "JWT to send as the bearer credential."
          }
        },
        "example": {
          "token_type": "Bearer",
          "expires_in": 300,
          "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIyZjljMWI3ZS00ZDNh..."
        }
      },
      "OAuthError": {
        "type": "object",
        "description": "RFC 6749 error body returned by the token endpoint.",
        "properties": {
          "error": {
            "type": "string"
          },
          "error_description": {
            "type": "string"
          },
          "hint": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "DrupalError": {
        "type": "object",
        "description": "Drupal REST error body. `message` is the field to read; it is often the\nonly diagnostic available.\n",
        "properties": {
          "message": {
            "type": "string"
          }
        }
      },
      "FieldValue": {
        "type": "object",
        "description": "A plain field item — string, number, boolean, or datetime.",
        "properties": {
          "value": {
            "description": "The scalar value."
          }
        },
        "required": [
          "value"
        ]
      },
      "FormattedTextValue": {
        "type": "object",
        "description": "A formatted-text field item.",
        "properties": {
          "value": {
            "type": "string",
            "description": "HTML markup."
          },
          "format": {
            "type": "string",
            "description": "Text format machine name, e.g. `full_html`. Deprecated `<i>` and\n`<b>` tags in `field_description`, `field_abstract`, `body`, and\n`field_access_only_body` are rewritten to `<em>` and `<strong>` on\nsave — expect the stored markup to differ from what you sent.\n"
          }
        },
        "required": [
          "value"
        ]
      },
      "ReferenceValue": {
        "type": "object",
        "description": "An entity-reference field item.",
        "properties": {
          "target_id": {
            "description": "Numeric entity ID for content/taxonomy references; machine name\nfor bundle references such as `type` and `vid`.\n",
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string"
              }
            ]
          }
        },
        "required": [
          "target_id"
        ]
      },
      "ResourceNode": {
        "description": "Resource node as sent on **create**. Same fields as\n`ResourceNodeFields`, with `title` additionally required.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/ResourceNodeFields"
          },
          {
            "type": "object",
            "required": [
              "title"
            ]
          }
        ]
      },
      "ResourceNodeFields": {
        "type": "object",
        "description": "Resource field catalogue — a published deliverable (report, webcast,\ncase study). NetSuite is the source of truth for the commerce fields;\nDrupal treats them as a read-mostly mirror.\n\nOnly `type` is required, which makes this the correct shape for a\npartial `PATCH`.\n",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "array",
            "description": "Bundle. Required on POST **and** PATCH: `[{\"target_id\": \"resource\"}]`.",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          },
          "title": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "status": {
            "type": "array",
            "description": "Published flag: `[{\"value\": true}]` or `[{\"value\": false}]`.",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_netsuite_id": {
            "type": "array",
            "description": "NetSuite internal ID, as a string. The correlation key — always send it.",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_product_code": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_product_id": {
            "type": "array",
            "description": "Integer.",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_werf_product_code": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_project_number": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_subtitle": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_description": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormattedTextValue"
            }
          },
          "body": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormattedTextValue"
            }
          },
          "field_available_date": {
            "type": "array",
            "description": "`YYYY-MM-DDTHH:MM:SS`, stored as UTC.",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_members_only": {
            "type": "array",
            "description": "Boolean.",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_outbound_water_category": {
            "type": "array",
            "description": "Multi-value string.",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_resource_type": {
            "type": "array",
            "description": "Taxonomy term reference.",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          },
          "field_access": {
            "type": "array",
            "description": "Taxonomy term reference that drives gated-content access. Changing\nit changes who can see the asset — treat writes here as sensitive.\n",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          },
          "field_project": {
            "type": "array",
            "description": "Reference to the parent Project **node** (not a term).",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          },
          "field_topics": {
            "type": "array",
            "description": "Multi-value reference to Topic **nodes**.",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          }
        }
      },
      "ProjectNode": {
        "description": "Project node as sent on **create**. Same fields as\n`ProjectNodeFields`, with `title` additionally required.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/ProjectNodeFields"
          },
          {
            "type": "object",
            "required": [
              "title"
            ]
          }
        ]
      },
      "ProjectNodeFields": {
        "type": "object",
        "description": "Project field catalogue — a research effort, with Resources hanging off\nit. Only `type` is required, so this is also the `PATCH` shape.\n",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "array",
            "description": "`[{\"target_id\": \"project\"}]`.",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          },
          "title": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "status": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_netsuite_id": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_project_number": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_werf_id": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_abstract": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormattedTextValue"
            }
          },
          "field_project_status": {
            "type": "array",
            "description": "Constrained list. Allowed values: `CANCELLED`, `CLOSED`, `FUNDED`,\n`UNFUNDED`, `PUBLISHED`. Anything else returns `422`.\n",
            "items": {
              "type": "object",
              "properties": {
                "value": {
                  "type": "string",
                  "enum": [
                    "CANCELLED",
                    "CLOSED",
                    "FUNDED",
                    "UNFUNDED",
                    "PUBLISHED"
                  ]
                }
              }
            }
          },
          "field_start_date": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_project_completed_date": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_projected_completed_date": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_funding_amount": {
            "type": "array",
            "description": "Float.",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_research_investment_amount": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_research_program_type": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_web_enabled": {
            "type": "array",
            "description": "String, not boolean.",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "field_principle_investigator": {
            "type": "array",
            "description": "Taxonomy term reference.",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          },
          "field_topics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          }
        }
      },
      "TaxonomyTerm": {
        "type": "object",
        "required": [
          "vid",
          "name"
        ],
        "properties": {
          "vid": {
            "type": "array",
            "description": "Vocabulary machine name, e.g. `[{\"target_id\": \"topics\"}]`.",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          },
          "name": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "description": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormattedTextValue"
            }
          },
          "weight": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          }
        }
      },
      "NodeResponse": {
        "type": "object",
        "description": "A fully serialized node. Every field is an array of items, matching the\nrequest format. `nid[0].value` is the ID to store in NetSuite.\n",
        "properties": {
          "nid": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "uuid": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "type": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReferenceValue"
            }
          },
          "title": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "status": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "changed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          },
          "revision_log": {
            "type": "array",
            "description": "Stamped by the integration as\n`Automated sync from NetSuite via REST API (OAuth user: …)`.\n",
            "items": {
              "$ref": "#/components/schemas/FieldValue"
            }
          }
        },
        "additionalProperties": true,
        "example": {
          "nid": [
            {
              "value": 1893
            }
          ],
          "uuid": [
            {
              "value": "8c1f4d2a-71b3-4e9c-a0d5-6f2e3b4c5d6e"
            }
          ],
          "type": [
            {
              "target_id": "resource"
            }
          ],
          "title": [
            {
              "value": "Nutrient Recovery from Wastewater Sidestreams"
            }
          ],
          "status": [
            {
              "value": true
            }
          ],
          "changed": [
            {
              "value": "2026-09-01T15:22:41+00:00"
            }
          ],
          "revision_log": [
            {
              "value": "Automated sync from NetSuite via REST API (OAuth user: netsuite_service)"
            }
          ]
        }
      }
    }
  }
}
