{"slug": "calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-params-4-3", "title": "Calling the NovelAI V5 API directly: nai-diffusion-5-full request body, params_version 4/3/1/0 identical, missing v4_prompt is a 500", "summary": "A developer testing NovelAI's V5 image generation API found that the params_version field — which third-party tools set inconsistently to 4, 3, 1, or 0 — produces pixel-for-pixel identical images when calling nai-diffusion-5-full at image.novelai.net. The testing also showed that omitting either v4_prompt or v4_negative_prompt returns a 500 Internal Server Error instead of a validation error, while posting to the old api.novelai.net host returns a misleading 'model must be a valid enum value' 400.", "body_md": "`nai-diffusion-5-full` takes the same body skeleton V4 took, posted to `https://image.novelai.net/ai/generate-image`, and `params_version` — the one field every third-party tool fills in differently — returns a pixel-for-pixel identical image whether you send 4, 3, 1, or 0 with this body skeleton. JSON and multipart bodies gave the same result too. What does break it: drop either `v4_prompt` or `v4_negative_prompt` and you get `500 Internal Server Error` rather than a 400, set `Content-Type` by hand on a `FormData` body and you get a `missing multipart boundary` 400, and post to the old host and you get a `model must be a valid enum value` 400.\n\nThis is for people calling NovelAI V5 from their own scripts rather than through a ComfyUI node or a GUI like NAIA (a community-built desktop front end for NovelAI). On 2026-09-14, on macOS 26.5.2 with Node v22.22.3 and an Opus account (`tier: 3`), I generated six images inside the free band and compared request formats, error responses, and balance changes; I also read the strings out of the official web client bundle the same day. Two terms if NovelAI is new to you: Anlas is NovelAI's generation credit, and Opus is the top subscription tier, which generates one image at a time at the common sizes and up to 28 steps without spending any, as long as no base image is involved and the allowance has not run out. The policy and the per-size cost table are in [the earlier post](https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l), the measurements through a ComfyUI node are in [the one after it](https://dev.to/ilan_kim/running-novelai-v5-in-comfyui-nai-diffusion-5-full-works-and-the-error-fetching-anlas-400-is-a-18gl), and this post covers only the request contract.\n\nIf you look at third-party tools' request bodies to work out how to call V5 yourself, the `params_version` value is different in every one of them. ComfyUI_NAIDGenerator, a ComfyUI node for NovelAI, sends `1`. The README of ComfyUI_RS_NAI_API_Request, another ComfyUI node, says \"V5 requests use params_version: 4 while older models keep params_version: 3\". The changelog of NAIWeaver, a standalone NovelAI client, says \"send params_version: 4\" (all checked 2026-09-14). The official OpenAPI document has no `required` array and no `nai-diffusion-5` string, so it cannot settle which one is right.\n\nFrom there, nudging the body around gets you three responses. All three are verbatim from this round of testing:\n\n```\nPOST https://api.novelai.net/ai/generate-image  (old host)\n400 {\"statusCode\":400,\"message\":\"model must be a valid enum value\"}\n\nPOST https://image.novelai.net/ai/generate-image  (v4_prompt or v4_negative_prompt omitted)\n500 {\"statusCode\":500,\"message\":\"Internal Server Error\"}\n\nPOST https://image.novelai.net/ai/generate-image  (FormData + Content-Type: multipart/form-data set by hand)\n400 {\"statusCode\":400,\"message\":\"missing multipart boundary\"}\n```\n\nThe first one has different wording from the \"update to the image URL\" 400 the balance lookup returns. The old host accepts the generation request itself but does not know the V5 model ID, so from the message alone it looks like you typed the model ID wrong.\n\nThe second is the confusing one. It is a 500 rather than a 400-class validation error, so it reads like a server outage, but the cause is your request body. The third one appears when you hand Node's `fetch` a `FormData` body and set the header yourself, because the boundary `undici` would have attached automatically disappears.\n\n`params_version` does not change the result with this body skeleton. Same prompt, seed `1234567890`, 832×1216, 28 steps, one image per value:\n\n| `params_version` | Response | Response time | PNG `Source` | Anlas | \n|---|---|---|---|---|\n| 4 (official web client) | 200 | 2.5 s | `NovelAI Diffusion V5 0ADF9AB7` | 9,930 → 9,930 | \n| 3 (the value said to be for older models) | 200 | 1.8 s | same | no change | \n| 1 (ComfyUI_NAIDGenerator) | 200 | 1.9 s | same | no change | \n| 0 (a value no tool uses) | 200 | 1.8 s | same | no change | \n\nComparing the four images pixel by pixel with Pillow gives a mean absolute difference of 0 and 0% of pixels changed, so they are completely identical. The `Comment` metadata of the returned PNGs has no `params_version` field either. If the value were read and acted on, 0 should at least have been rejected, so with this skeleton it has to be treated as ignored. Whether that also holds for other field combinations — character prompts, inpainting and so on — I did not check.\n\nSo where does `4` come from? In the novelai.net bundle I pulled on 2026-09-14 (`_app-b7172cc1a6a0b340.js`), the default parameters in the `nai-diffusion-5-full` branch start with `params_version:4`, and there is migration code that raises stored settings to 4 if `params_version<4`. The string `v5_prompt` appears 0 times; `v4_prompt` appears twice.\n\nThe transport format makes no difference to the result either. Sending the `params_version: 1` body as JSON and sending it as a JSON blob in the `request` part of a `FormData` both returned 200 with identical pixels. The debug toggle text in that same bundle describes the default as \"Image generation requests will be sent as multipart form data.\" and the checked state as \"JSON (pre-multipart-form format)\", so multipart is the official client's default and JSON survives as the legacy format.\n\n| Transport | `Content-Type` handling | Response | \n|---|---|---|\n| JSON | `application/json` set explicitly | 200, 917,294-byte zip | \n| multipart | header not set (automatic boundary) | 200, pixels identical to JSON | \n| multipart | `multipart/form-data` set by hand | 400 `missing multipart boundary` | \n\nYou cannot leave out `v4_prompt` or `v4_negative_prompt`. With everything else left alone, a body with both keys removed, a body with only `v4_prompt` removed, and a body with only `v4_negative_prompt` removed all came back with the same 500, and none of them moved the balance. Together with the absence of `v5_prompt` in the bundle, I take this to mean V5 still requires the V4 structure as it is.\n\nAuthentication errors all arrive lumped into a single 401. Leave the `Authorization` header out entirely, or put a space inside the token value, and the response is the same `401 {\"statusCode\":401,\"message\":\"Unauthorized\"}` either way. Since the response cannot tell you whether the token string is mangled or the header is missing, don't print the value: check its length and whether it is unchanged by `trim()`.\n\nFor the free band, this adds the landscape size to the table in the earlier post. 1216×832 (pixel product 1,011,712) at 28 steps, one image, was also 0 Anlas, and across six generations `usage.percent` did not move from 87. The battery reading is an integer, which means per-image consumption below 1% is invisible in it.\n\nI did not find an API that quotes the price up front this time. `POST /ai/generate-image/request-price`, which appears in the web bundle, returned `404 page not found` on `image.novelai.net` and `404 Cannot POST /ai/generate-image/request-price` on `api.novelai.net`. The web UI does show an estimated Anlas cost on the generate button, but I could not get the same number out of this path, and whether another path exists I could not confirm.\n\n`https://image.novelai.net`. Generation (`/ai/generate-image`) and balance lookup (`/user/subscription`) are both on it, while `api.novelai.net` rejects generation with `model must be a valid enum value` and lookups with \"update to the image URL\".`params_version: 4` row of the table above, and the prompt goes in the same string in three places: top-level `input`, `parameters.prompt`, and `v4_prompt.caption.base_caption`. Use `4` for `params_version` to match the official web client, but don't read any meaning into the value:\n\n```\n{\n  \"input\": \"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality\",\n  \"model\": \"nai-diffusion-5-full\",\n  \"action\": \"generate\",\n  \"parameters\": {\n    \"params_version\": 4,\n    \"width\": 832, \"height\": 1216, \"scale\": 5, \"sampler\": \"k_euler_ancestral\", \"steps\": 28,\n    \"seed\": 1234567890, \"n_samples\": 1, \"ucPreset\": 3, \"qualityToggle\": false,\n    \"sm\": false, \"sm_dyn\": false, \"dynamic_thresholding\": false, \"controlnet_strength\": 1,\n    \"legacy\": false, \"add_original_image\": false, \"cfg_rescale\": 0, \"noise_schedule\": \"karras\",\n    \"legacy_v3_extend\": false, \"uncond_scale\": 1, \"negative_prompt\": \"\",\n    \"prompt\": \"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality\",\n    \"reference_image_multiple\": [], \"reference_information_extracted_multiple\": [], \"reference_strength_multiple\": [],\n    \"extra_noise_seed\": 1234567890,\n    \"v4_prompt\": { \"use_coords\": false, \"use_order\": false, \"caption\": { \"base_caption\": \"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality\", \"char_captions\": [] } },\n    \"v4_negative_prompt\": { \"use_coords\": false, \"use_order\": false, \"caption\": { \"base_caption\": \"\", \"char_captions\": [] } }\n  }\n}\n```\n\nSave that JSON as `body.json`, put the file below in the same folder, and it runs as is. The key file is a text file with a `pst-` token on the line after a line labeled \"novelai\", and the token stays in memory only. Running it with `node nai-v5-direct.mjs <key file>` got me a 200 and an `image_0.png` whose pixels match the table above:\n\n```\n// node nai-v5-direct.mjs <key file> — the key file has a pst- token on the line after the \"novelai\" label\nimport { readFile, writeFile } from 'node:fs/promises';\n\nconst lines = (await readFile(process.argv[2], 'utf8')).split(/\\r?\\n/).map((l) => l.trim());\nconst token = lines[lines.findIndex((l) => /novelai/i.test(l)) + 1];\nconst body = JSON.parse(await readFile('body.json', 'utf8'));\n\nconst r = await fetch('https://image.novelai.net/ai/generate-image', {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },\n  body: JSON.stringify(body),\n});\nconsole.log(r.status, r.headers.get('content-type'));\nif (r.ok) await writeFile('out.zip', Buffer.from(await r.arrayBuffer())); // image_0.png is inside\nelse console.log(await r.text());\n```\n\n`Content-Type: application/json` explicitly if you send JSON, and do not set a `Content-Type` header yourself if you use `FormData`. Node 22's `fetch` builds the header with the boundary on its own for a `binary/octet-stream` zip with `image_0.png` inside. Check the model from the `Source` value in the PNG's `tEXt` chunk (`NovelAI Diffusion V5 0ADF9AB7`).` n_samples: 1`), no base image, a pixel product of 1,048,576 or less, and 28 steps or fewer — then read `trainingStepsLeft.fixedTrainingStepsLeft + trainingStepsLeft.purchasedTrainingSteps`. `trainingStepsLeft` is an object rather than an integer, so you cannot subtract it directly. `usage.percent` is too coarse to use for per-image checks.\nImage: the sample outputs in this post are AI images generated with NovelAI Diffusion V5.\n\nWith this body skeleton, `params_version` is ignored — 4, 3, 1, and 0 all return the same pixels — and JSON and multipart bodies produced the same image. The parts that actually matter are the host, `image.novelai.net`, and keeping both `v4_prompt` and `v4_negative_prompt` in the body, since dropping either one returns a 500 rather than a validation 400.\n\n`director_reference_*` fields are sent to V5: V5 doesn't support Precise Reference, but the schema doesn't block these fields. Whether that is a 400 or \"ignored and billed\", I didn't run, because of the billing risk.\n| Version | Description | \n|---|---|\n| 1.0 | 2026-09-14 first version (macOS 26.5.2 · Node v22.22.3 · Opus, 6 free-tier generations) |", "url": "https://wpnews.pro/news/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-params-4-3", "canonical_source": "https://dev.to/ilan_kim/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-paramsversion-4310-133a", "published_at": "2026-09-14 01:09:22+00:00", "updated_at": "2026-09-14 01:25:33.789745+00:00", "lang": "en", "topics": ["generative-ai", "ai-products", "ai-tools", "developer-tools"], "entities": ["NovelAI", "nai-diffusion-5-full", "ComfyUI_NAIDGenerator", "ComfyUI_RS_NAI_API_Request", "NAIWeaver", "ComfyUI", "NAIA"], "alternates": {"html": "https://wpnews.pro/news/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-params-4-3", "markdown": "https://wpnews.pro/news/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-params-4-3.md", "text": "https://wpnews.pro/news/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-params-4-3.txt", "jsonld": "https://wpnews.pro/news/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-params-4-3.jsonld"}}