{"slug": "the-official-typescript-sdk-for-the-spacexai-api", "title": "The Official TypeScript SDK for the SpaceXAI API", "summary": "XAI released the official TypeScript SDK for its SpaceXAI API, published as the npm package @xai-official/sdk and requiring Node.js 22.13 or later. The SDK is experimental, covers the Responses API plus image and video generation, Files, Batch, Voice, tokenization, and model and account lookup, and its interfaces may change before version 1.0, so xAI advises pinning an exact version. The client reads the XAI_API_KEY environment variable, has no runtime dependencies, and blocks browser and Worker use by default to keep API keys server-side.", "body_md": "Use Grok from TypeScript with a typed, ESM client built on the SpaceXAI REST API. The SDK has no runtime dependencies and includes streaming, structured output, function tools, image input, image and video generation, file uploads, batch processing, text to speech and transcription, multi-turn conversations, and access to usage and HTTP metadata.\n\n**Experimental.** This SDK is in early development. It currently covers the Responses API, image and video generation, the Files, Batch, and Voice APIs, tokenization, and model and account lookup, and its interfaces may change between releases before 1.0. Pin an exact version and read the [changelog](https://github.com/xai-org/xai-sdk-ts/blob/main/CHANGELOG.md) when upgrading. Feedback and bug reports are welcome in [issues](https://github.com/xai-org/xai-sdk-ts/issues).\n\n- Node.js 22.13 or later\n- A [SpaceXAI API key](https://console.x.ai)\n- An ESM project\n\nInstall the package with your preferred package manager:\n\n```\nnpm install @xai-official/sdk\npnpm add @xai-official/sdk\n```\n\nSet your API key in the environment. The client reads `XAI_API_KEY` automatically.\n\n```\nexport XAI_API_KEY=\"your-api-key\"\njs\nimport { SpaceXAI } from \"@xai-official/sdk\";\n\nconst client = new SpaceXAI();\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"Explain why the sky is blue in one sentence.\",\n});\n\nconsole.log(response.toText());\n```\n\nKeep API keys on the server. The SDK blocks browser and Worker use by default because shipping a secret key to client-side code exposes it to users.\n\nSet `stream: true` to receive output as it's generated. Listen for answer text with `on(\"text\")`, then `await stream.done()` for the final response:\n\n``` js\nconst stream = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"Write a short story about a curious robot.\",\n  stream: true,\n});\n\nconst response = await stream\n  .on(\"text\", (text) => process.stdout.write(text))\n  .done();\n\nconsole.log(`\\n${response.usage.total_tokens} tokens`);\n```\n\n`done()` resolves to the same response object a non-streamed request returns. It rejects if the stream fails or closes before the response completes.\n\nBesides `\"text\"`, `on()` has helper events for the rest of a response:\n\n- `\"reasoning\"` for each chunk of reasoning text or reasoning summary\n- `\"tool_call\"` for each tool call once its arguments are complete, whether your code or SpaceXAI runs it. Check`call.type` to tell them apart\n- `\"client_tool_call\"` for each call that your code runs: your function tools and shell commands\n- `\"server_tool_call\"` for each call to a tool that SpaceXAI runs, such as web search or code execution\n- `\"image\"` for each finished image generation call, with the base64 image in`result`\n- `\"citation\"` for each URL citation in the answer\n\nEach tool call fires once, when its arguments are complete. For your function tools, that's when to run the function, since nothing has run it yet. To show that a call has started before its arguments arrive, listen for `\"response.output_item.added\"`.\n\n``` js\nconst response = await stream\n  .on(\"reasoning\", (text) => process.stderr.write(text))\n  .on(\"text\", (text) => process.stdout.write(text))\n  .on(\"tool_call\", (call) => console.error(`\\n${call.type} ${call.status}`))\n  .done();\n```\n\n`on()` also takes any server-sent event type, such as `\"response.completed\"`, and passes the listener the typed event. Events the SDK doesn't recognize arrive as `\"unknown\"`, with the original payload in `event.raw`.\n\nYou can also iterate over the stream to handle events in a loop. After the loop, `done()` resolves right away with the final response:\n\n``` js\nfor await (const event of stream) {\n  if (event.type === \"response.function_call_arguments.delta\") {\n    process.stdout.write(event.delta);\n  }\n}\n\nconst response = await stream.done();\nconsole.log(response.toText());\n```\n\n`stream.http` has the HTTP status, headers, and request IDs as soon as `create()` returns.\n\nIf you stop consuming a stream early, call `await stream.close()` to cancel its response body.\n\nResponses are not stored by default. Use `toInput()` to carry the model output, including encrypted reasoning content, into the next turn. Reuse one `prompt_cache_key` across the conversation to improve prompt cache routing:\n\n``` js\nimport { randomUUID } from \"node:crypto\";\nimport { type InputItem, SpaceXAI } from \"@xai-official/sdk\";\n\nconst client = new SpaceXAI();\nconst promptCacheKey = `conversation:${randomUUID()}`;\nconst input: Array<InputItem> = [\n  { role: \"user\", content: \"My name is Ada. Remember it.\" },\n];\n\nconst first = await client.responses.create({\n  model: \"grok-4.7\",\n  input,\n  prompt_cache_key: promptCacheKey,\n});\n\nconst second = await client.responses.create({\n  model: \"grok-4.7\",\n  input: [\n    ...input,\n    ...first.toInput(),\n    { role: \"user\", content: \"What is my name?\" },\n  ],\n  prompt_cache_key: promptCacheKey,\n});\n\nconsole.log(second.toText());\n```\n\nUse a different cache key for each unrelated conversation.\n\nTo continue a stored response by ID, opt in to storage:\n\n``` js\nconst first = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"My name is Ada. Remember it.\",\n  store: true,\n});\n\nconst second = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"What is my name?\",\n  previous_response_id: first.id,\n  store: true,\n});\n```\n\nEvery turn resends the whole conversation, so input tokens grow as it gets longer. Compact the conversation into a single encrypted item with `responses.compact()`, then start the next input with the compacted `output`. Continuing the `toInput()` example:\n\n``` js\nconst compacted = await client.responses.compact({\n  model: \"grok-4.7\",\n  input: [\n    ...input,\n    ...first.toInput(),\n    { role: \"user\", content: \"What is my name?\" },\n    ...second.toInput(),\n  ],\n});\n\nconst third = await client.responses.create({\n  model: \"grok-4.7\",\n  input: [\n    ...compacted.output,\n    { role: \"user\", content: \"Spell my name backwards.\" },\n  ],\n  prompt_cache_key: promptCacheKey,\n});\n\nconsole.log(third.toText());\n```\n\nPass `compacted.output` unchanged and add new turns after it. The conversation must still fit in the model's context window when you compact it. `compacted.usage` reports the tokens the compaction used and `dropped_message_count`, the number of messages it replaced.\n\nPass an image URL alongside text:\n\n``` js\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: [\n    {\n      role: \"user\",\n      content: [\n        { type: \"input_text\", text: \"Describe this image.\" },\n        {\n          type: \"input_image\",\n          image_url: \"https://example.com/image.jpg\",\n          detail: \"high\",\n        },\n      ],\n    },\n  ],\n});\n\nconsole.log(response.toText());\n```\n\nFor a local image, pass a `Blob` or `File` as `image`. The SDK converts it to a data URL before sending the request, and detects JPEG, PNG, or WebP from the bytes when the `Blob` has no MIME type.\n\n``` js\nimport { readFile } from \"node:fs/promises\";\n\nconst bytes = new Uint8Array(await readFile(\"./image.png\"));\nconst image = new Blob([bytes], { type: \"image/png\" });\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: [\n    {\n      role: \"user\",\n      content: [\n        { type: \"input_text\", text: \"What is in this image?\" },\n        { type: \"input_image\", image },\n      ],\n    },\n  ],\n});\n```\n\nProvide a JSON Schema through `text.format`. Call `toJson()` to parse the completed text output.\n\n``` js\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"Give me a city to visit in Japan.\",\n  text: {\n    format: {\n      type: \"json_schema\",\n      name: \"travel_suggestion\",\n      schema: {\n        type: \"object\",\n        properties: {\n          city: { type: \"string\" },\n          reason: { type: \"string\" },\n        },\n        required: [\"city\", \"reason\"],\n        additionalProperties: false,\n      },\n    },\n  },\n});\n\nconst suggestion = response.toJson();\nconsole.log(suggestion);\n```\n\n`toJson()` returns `unknown`. Validate the result before using it at a trust boundary. The non-throwing `response.parsed` getter returns `null` when the output is incomplete or is not valid JSON.\n\nDescribe functions with JSON Schema, run the requested function in your application, then return its output to the model:\n\n``` js\nimport { type InputItem, type Tool, isFunctionCall, SpaceXAI } from \"@xai-official/sdk\";\n\nconst client = new SpaceXAI();\nconst prompt = \"What is the weather in San Francisco?\";\nconst input: Array<InputItem> = [{ role: \"user\", content: prompt }];\nconst getWeatherTool: Tool = {\n  type: \"function\",\n  name: \"get_weather\",\n  description: \"Get the current weather for a location.\",\n  parameters: {\n    type: \"object\",\n    properties: {\n      location: { type: \"string\" },\n    },\n    required: [\"location\"],\n    additionalProperties: false,\n  },\n};\n\nasync function getWeather(args: unknown) {\n  if (\n    typeof args !== \"object\" ||\n    args === null ||\n    !(\"location\" in args) ||\n    typeof args.location !== \"string\"\n  ) {\n    throw new Error(\"get_weather expects a location string\");\n  }\n  return { location: args.location, temperatureC: 18, conditions: \"sunny\" };\n}\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input,\n  parallel_tool_calls: false,\n  tools: [getWeatherTool],\n});\n\nconst call = response.output.find(isFunctionCall);\nif (!call) throw new Error(\"The model did not call get_weather\");\nconst result = await getWeather(JSON.parse(call.arguments));\n\nconst answer = await client.responses.create({\n  model: \"grok-4.7\",\n  input: [\n    ...input,\n    ...response.toInput(),\n    { type: \"function_call_output\", call_id: call.call_id, output: JSON.stringify(result) },\n  ],\n});\n\nconsole.log(answer.toText());\n```\n\n`parallel_tool_calls: false` limits the model to one function call per turn, so this example only has to handle one. By default the model can ask for several at once, which the [tool call loop](#tool-call-loop) handles.\n\nTreat function names and arguments as untrusted input. Only dispatch functions you have explicitly allowed, and validate arguments before executing them, as `getWeather()` does.\n\nTo let the model call tools until it has an answer, run a loop. Stream a turn, run each function call as soon as it finishes streaming, then send the outputs back along with the model's output. Stop when a turn makes no function calls, and cap the number of turns so a model that keeps calling tools can't loop forever. This reuses `getWeatherTool` and `getWeather` from the example above:\n\n``` js\nimport { type FunctionToolCall, type InputItem, type Tool, SpaceXAI } from \"@xai-official/sdk\";\n\nconst client = new SpaceXAI();\nconst tools: Array<Tool> = [getWeatherTool];\nconst handlers: Record<string, (args: unknown) => Promise<unknown>> = {\n  get_weather: getWeather,\n};\n\nasync function runTool(call: FunctionToolCall): Promise<InputItem> {\n  let output: unknown;\n  try {\n    const handler = handlers[call.name];\n    if (!handler) throw new Error(`Unknown tool: ${call.name}`);\n    output = await handler(JSON.parse(call.arguments));\n  } catch (err) {\n    output = { error: err instanceof Error ? err.message : String(err) };\n  }\n  return {\n    type: \"function_call_output\",\n    call_id: call.call_id,\n    output: JSON.stringify(output),\n  };\n}\n\nconst input: Array<InputItem> = [\n  { role: \"user\", content: \"Compare the weather in Paris and Tokyo.\" },\n];\n\nfor (let turn = 0; turn < 10; turn++) {\n  const toolRuns: Array<Promise<InputItem>> = [];\n  const stream = await client.responses.create({\n    model: \"grok-4.7\",\n    input,\n    tools,\n    stream: true,\n  });\n  const response = await stream\n    .on(\"text\", (text) => process.stdout.write(text))\n    .on(\"client_tool_call\", (call) => {\n      if (call.type === \"function_call\") toolRuns.push(runTool(call));\n    })\n    .done();\n  if (toolRuns.length === 0) break;\n\n  const toolOutputs = await Promise.all(toolRuns);\n  input.push(...response.toInput(), ...toolOutputs);\n}\n```\n\nThe `handlers` map is the list of functions the model may call. `runTool()` returns errors to the model instead of throwing, so the model can recover, and a failing tool can't crash the loop while the stream is still running.\n\nWith the `shell` tool, the model writes shell commands and your application runs them. Use it for agents that work on your machine, such as exploring a repository, running tests, or checking disk space. Each call arrives through the `client_tool_call` listener as a `shell_call`, with the commands in `action.commands`. The model writes these commands, so run them in a sandbox or container, or check each one before running it:\n\n``` js\nimport { exec } from \"node:child_process\";\nimport { SpaceXAI } from \"@xai-official/sdk\";\n\nconst client = new SpaceXAI();\n\nconst stream = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"How much free disk space does this machine have?\",\n  tools: [{ type: \"shell\", environment: { type: \"local\" } }],\n  stream: true,\n});\n\nawait stream\n  .on(\"client_tool_call\", (call) => {\n    if (call.type === \"shell_call\") {\n      for (const command of call.action.commands) {\n        exec(command, (error, stdout, stderr) => console.log(stdout || stderr));\n      }\n    }\n  })\n  .done();\n```\n\nTo let the model use the results, send them back in the next request, like the function outputs in the [tool call loop](#tool-call-loop): `{ type: \"shell_call_output\", call_id: call.call_id, output: [{ stdout, stderr, outcome: { type: \"exit\", exit_code: 0 } }] }`.\n\nTo give the model skills, list them in the tool's `environment`. A skill is a directory with a `SKILL.md` file of instructions. The model sees each skill's name and description, and when a task matches one, it reads the `SKILL.md` through your shell tool and follows it. If you only allow certain commands, allow reading the skill's directory:\n\n``` js\nconst shell: Tool = {\n  type: \"shell\",\n  environment: {\n    type: \"local\",\n    skills: [\n      {\n        name: \"release-notes\",\n        description: \"Write release notes from this repo's git log in our house style.\",\n        path: \"./skills/release-notes\",\n      },\n    ],\n  },\n};\n```\n\nAsked to write release notes, the model reads `./skills/release-notes/SKILL.md`, runs the `git log` command it describes, and writes the notes in the format it specifies.\n\nBesides your own functions, the API has built-in tools. Create them with the helpers from `@xai-official/sdk/tools`, which check each tool's options as you type. SpaceXAI runs these tools and includes their results in the response:\n\n- `webSearch()` (`web_search` ) searches the web. Options include`allowed_domains` ,`excluded_domains` ,`user_location` , and`search_context_size` .\n- `xSearch()` (`x_search` ) searches posts on X. Options include`allowed_x_handles` ,`excluded_x_handles` ,`from_date` , and`to_date` .\n- `codeExecution()` (`code_interpreter` ) writes and runs Python code to answer the prompt.\n- `collectionsSearch()` (`file_search` ) searches the[collections](https://docs.x.ai/developers/files/collections) listed in`vector_store_ids` .\n- `imageGeneration()` (`image_generation` ) creates or edits images.\n- `mcp()` (`mcp` ) calls tools on the remote MCP server at`server_url` , identified by`server_label` .\n- `toolSearch()` (`tool_search` ) loads the definitions of tools marked`defer_loading: true` when the model needs them, instead of putting every definition in the prompt.\n\nTwo kinds of tools run in your application instead: `function` for your own functions, as shown in [Tools](#tools), and `shell`, where the model writes shell commands for your application to run, as shown in [Shell commands](#shell-commands). When you stream, calls to both arrive through the `client_tool_call` listener.\n\nThe helpers return plain tool objects, so you can also write `{ type: \"web_search\" }` yourself. `Tool` autocompletes the known types and accepts any other `type`, such as a tool released after this SDK version, but it doesn't check options the way the helpers do. See the [SpaceXAI documentation](https://docs.x.ai) for each tool's options.\n\nAdd the web search tool when a prompt needs current information:\n\n``` js\nimport { SpaceXAI } from \"@xai-official/sdk\";\nimport { webSearch } from \"@xai-official/sdk/tools\";\n\nconst client = new SpaceXAI();\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"What are the latest developments in commercial spaceflight?\",\n  tools: [webSearch()],\n});\n\nconsole.log(response.toText());\nconsole.log(response.usage.num_server_side_tools_used);\n```\n\nSearch posts on X, optionally limited to certain accounts and dates:\n\n``` js\nimport { xSearch } from \"@xai-official/sdk/tools\";\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"What has SpaceXAI announced on X this month?\",\n  tools: [xSearch({ allowed_x_handles: [\"xai\"], from_date: \"2026-09-01\" })],\n});\n```\n\n`allowed_x_handles` and `excluded_x_handles` each take up to 20 handles and can't be used together. Set `enable_image_understanding` or `enable_video_understanding` to let the model look at media in posts.\n\nWhen you stream, each finished search reaches the `server_tool_call` listener as a `custom_tool_call`. Its `name` is the search that ran, such as `x_keyword_search`, and `input` holds the search arguments as a JSON string:\n\n``` js\nawait stream\n  .on(\"server_tool_call\", (call) => {\n    if (call.type === \"custom_tool_call\") console.log(call.name, call.input);\n  })\n  .done();\n```\n\nLet the model write and run Python for calculations and data analysis:\n\n``` js\nimport { codeExecution } from \"@xai-official/sdk/tools\";\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"What is the standard deviation of 12, 15, 19, 22, and 31?\",\n  tools: [codeExecution()],\n});\n```\n\nThe code runs in a sandbox with common libraries installed. The tool takes no options.\n\nSearch documents you've added to [collections](https://docs.x.ai/developers/files/collections):\n\n``` js\nimport { collectionsSearch } from \"@xai-official/sdk/tools\";\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"What does our refund policy say about digital purchases?\",\n  tools: [collectionsSearch({ vector_store_ids: [\"your-collection-id\"], max_num_results: 10 })],\n});\n```\n\nGive the model the tools of a remote MCP server. SpaceXAI connects to the server and calls its tools during the response:\n\n``` js\nimport { mcp } from \"@xai-official/sdk/tools\";\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"What is the modelcontextprotocol/typescript-sdk repository for?\",\n  tools: [mcp({ server_url: \"https://mcp.deepwiki.com/mcp\", server_label: \"deepwiki\" })],\n});\n```\n\nThe server must use the Streaming HTTP or SSE transport. Limit the model to some of the server's tools with `allowed_tools`, and pass credentials with `authorization` or `headers`. `require_approval` and `connector_id` aren't supported yet. For a server with many tools, set `defer_loading: true` on it and add `toolSearch()`, so the model loads only the tool definitions it needs.\n\nAdd the image generation tool to let the model create or edit images as one step of a response. Each image arrives as an `image_generation_call` output item whose `result` holds base64 image data:\n\n``` js\nimport { writeFile } from \"node:fs/promises\";\nimport { isImageGenerationCall, SpaceXAI } from \"@xai-official/sdk\";\nimport { imageGeneration } from \"@xai-official/sdk/tools\";\n\nconst client = new SpaceXAI();\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"Generate an image of a corgi surfing a big wave, in the style of a Japanese woodblock print.\",\n  tools: [imageGeneration()],\n});\n\nconsole.log(response.toText());\n\nconst call = response.output.find(isImageGenerationCall);\nif (call?.result) {\n  await writeFile(\"corgi.jpg\", Buffer.from(call.result, \"base64\"));\n}\n```\n\nPass `action: \"generate\"` or `action: \"edit\"` to `imageGeneration()` to allow only one of those capabilities. When streaming, each call emits `response.image_generation_call.in_progress`, `response.image_generation_call.generating`, and `response.image_generation_call.completed` events, then a `response.output_item.done` event carries the finished item.\n\nTo generate or edit an image directly with full control over its size and format, use the [image generation](#image-generation) and [image editing](#image-editing) methods instead.\n\nEvery completed response provides:\n\n- `response.toText()` to concatenate output text\n- `response.toInput()` to carry all output items into a later request\n- `response.toJson()` to parse completed JSON output\n- `response.parsed` for non-throwing JSON parsing\n- `response.output` for typed output items\n- `response.usage` for token counts, server-side tool use, and cost when available\n- `response.http` for the HTTP status, headers, SpaceXAI request ID, and client request ID\n- `response.raw` for the response object as the API sent it, including fields this SDK doesn't know yet\n\nUse the exported type guards when inspecting output items:\n\n``` js\nimport {\n  isFunctionCall,\n  isMessage,\n  isReasoning,\n} from \"@xai-official/sdk\";\n\nfor (const item of response.output) {\n  if (isMessage(item)) {\n    console.log(\"message\", item.content);\n  } else if (isFunctionCall(item)) {\n    console.log(\"function\", item.name, item.arguments);\n  } else if (isReasoning(item)) {\n    console.log(\"reasoning item\", item.id);\n  }\n}\n```\n\nGenerate images from a text prompt with a Grok Imagine model. Images are returned as temporary URLs by default, so download or process them promptly:\n\n``` js\nconst result = await client.images.generate({\n  model: \"grok-imagine-image-2.0\",\n  prompt: \"A collage of London landmarks in a stenciled street-art style\",\n});\n\nconsole.log(result.data[0]?.url);\nconsole.log(result.usage?.cost_usd);\n```\n\nRequest up to 10 images with `n`, and shape the output with `aspect_ratio`, `resolution`, and `quality`. Only `grok-imagine-image-2.0` supports `quality`. Set `response_format: \"b64_json\"` to receive base64 data instead of URLs:\n\n``` js\nimport { writeFile } from \"node:fs/promises\";\n\nconst result = await client.images.generate({\n  model: \"grok-imagine-image-2.0\",\n  prompt: \"A futuristic city skyline at night\",\n  n: 4,\n  aspect_ratio: \"16:9\",\n  resolution: \"2k\",\n  response_format: \"b64_json\",\n});\n\nfor (const [index, image] of result.data.entries()) {\n  if (image.b64_json) {\n    await writeFile(`skyline-${index}.jpg`, Buffer.from(image.b64_json, \"base64\"));\n  }\n}\n```\n\nBase64 output is about a third larger than the image file, so large batches of high-resolution images can approach the default 32 MiB response size limit. Raise `maxResponseBodyBytes` for those requests:\n\n``` js\nconst result = await client.images.generate(\n  {\n    model: \"grok-imagine-image-2.0\",\n    prompt: \"A futuristic city skyline at night\",\n    n: 10,\n    resolution: \"2k\",\n    response_format: \"b64_json\",\n  },\n  { maxResponseBodyBytes: 128 * 1024 * 1024 },\n);\n```\n\nEach result provides `data`, `usage`, and `http`. `usage.cost_usd` converts the reported `cost_in_usd_ticks` to US dollars, and `usage` is `null` when the API omits it.\n\nPass a source image with your prompt to edit it. `image` accepts a public URL, a base64 data URL, a Files API `file_id`, or a `Blob` or `File`, which the SDK converts to a data URL before sending the request:\n\n``` js\nimport { openAsBlob } from \"node:fs\";\n\nconst photo = await openAsBlob(\"./photo.png\");\n\nconst result = await client.images.edit({\n  model: \"grok-imagine-image-2.0\",\n  prompt: \"Render this as a pencil sketch with detailed shading\",\n  image: photo,\n});\n\nconsole.log(result.data[0]?.url);\n```\n\nWhen a `Blob` or `File` has no MIME type, as with `openAsBlob()` or `new File([bytes], \"photo.png\")`, the SDK detects JPEG, PNG, or WebP from its first bytes. The API rejects image data URLs that are not typed as one of those formats.\n\nTo combine up to five source images, pass `images` instead of `image` and refer to them in the prompt as `<IMAGE_0>`, `<IMAGE_1>`, and so on. The output follows the first image's aspect ratio unless you set `aspect_ratio`:\n\n``` js\nconst result = await client.images.edit({\n  model: \"grok-imagine-image-2.0\",\n  prompt: \"Place the cat from <IMAGE_0> on the sofa from <IMAGE_1>\",\n  images: [\n    { url: \"https://example.com/cat.png\" },\n    { file_id: \"file_abc123\" },\n  ],\n  aspect_ratio: \"16:9\",\n});\n```\n\nVideo generation runs as a background job. `generate()` starts the job and returns its `request_id`, and `wait()` polls until the job finishes:\n\n```\nconst { request_id } = await client.videos.generate({\n  model: \"grok-imagine-video-1.5\",\n  prompt: \"A paper boat drifting down a rain-soaked street\",\n  duration: 8,\n  aspect_ratio: \"16:9\",\n  resolution: \"720p\",\n});\n\nconst result = await client.videos.wait(request_id);\nif (result.status === \"done\") {\n  console.log(result.video?.url);\n  console.log(result.usage?.cost_usd);\n} else {\n  console.error(result.status, result.error?.code, result.error?.message);\n}\n```\n\n`wait()` resolves once the status is no longer `pending`: `done`, `failed`, or `expired`. A failed result includes an `error` with a `code` and `message`. If `video.respect_moderation` is `false`, the video did not pass moderation and has no URL. Video URLs are temporary, so download the file promptly.\n\n`wait()` polls every 5 seconds for up to 10 minutes. Pass `interval` and `timeout` in milliseconds to change this, and a `signal` to stop waiting. A timeout rejects with `TimeoutError` but does not cancel the job, so you can call `wait()` again. To check once without waiting, call `client.videos.get(request_id)`, which returns `status: \"pending\"` until the video is ready.\n\nTo animate a still image, pass it as `image`. `image`, `reference_images`, and keyframe images accept a public URL, a base64 data URL, a Files API `file_id`, or a `Blob` or `File`, which the SDK converts to a data URL before sending the request:\n\n``` js\nimport { openAsBlob } from \"node:fs\";\n\nconst { request_id } = await client.videos.generate({\n  model: \"grok-imagine-video-1.5\",\n  prompt: \"Make the water crash down and slowly pan out the camera\",\n  image: await openAsBlob(\"./waterfall.png\"),\n});\n```\n\nEdit a video with `edit()`, or continue it from its last frame with `extend()`. Both return a `request_id` for `wait()`. The source `video` must be an MP4, given as a public URL, a base64 data URL, a Files API `file_id`, or a `Blob` or `File`, which the SDK converts to a data URL before sending the request. For extensions, `duration` sets the length of the new segment only:\n\n``` js\nimport { openAsBlob } from \"node:fs\";\n\nconst edit = await client.videos.edit({\n  model: \"grok-imagine-video\",\n  prompt: \"Give the woman a silver necklace\",\n  video: await openAsBlob(\"portrait.mp4\"),\n});\n\nconst extension = await client.videos.extend({\n  model: \"grok-imagine-video\",\n  prompt: \"The camera slowly zooms out to reveal the city skyline\",\n  video: { file_id: \"file_abc123\" },\n  duration: 6,\n});\n```\n\nA `Blob` or `File` without a MIME type is sent as `video/mp4`. Because the video travels inside the request as base64, which is a third larger than the file, upload large videos with `client.files.upload()` (up to 50 MB) and pass `{ file_id }` instead.\n\nList the video generation models available to your API key with `client.models.video.list()`, or look one up by ID with `client.models.video.get()`.\n\nUpload a document, image, or video once and refer to it by ID. A file ID works wherever the API accepts a `file_id`, such as an `input_file` part in the Responses API or an image or video input:\n\n``` js\nimport { openAsBlob } from \"node:fs\";\n\nconst file = await client.files.upload({\n  file: await openAsBlob(\"./report.pdf\"),\n  filename: \"report.pdf\",\n});\n\nconst response = await client.responses.create({\n  model: \"grok-4.7\",\n  input: [\n    {\n      role: \"user\",\n      content: [\n        { type: \"input_text\", text: \"Summarize the key findings in this report.\" },\n        { type: \"input_file\", file_id: file.id },\n      ],\n    },\n  ],\n});\n\nconsole.log(response.toText());\n```\n\nThe API records the upload's filename as the file's `filename`. A `File` uses its own name, and a plain `Blob`, such as one from `openAsBlob()`, needs `filename`. Files are kept until you delete them; set `expires_after` to between 3,600 and 2,592,000 seconds (1 hour to 30 days) to have one deleted automatically.\n\nList, download, share, and delete stored files:\n\n``` js\nimport { writeFile } from \"node:fs/promises\";\n\nfor await (const stored of client.files.list()) {\n  console.log(stored.id, stored.filename, stored.bytes);\n}\n\nconst content = await client.files.content(file.id);\nawait writeFile(\"report-copy.pdf\", await content.bytes());\n\nconst { public_url } = await client.files.createPublicUrl(file.id, {\n  expires_after: 86_400,\n});\nconsole.log(public_url);\n\nawait client.files.revokePublicUrl(file.id);\nawait client.files.delete(file.id);\n```\n\n`list()` returns the newest files first and fetches further pages as the loop needs them. `content()` returns an `BinaryResponse`: stream its `body` or read it with `bytes()`, `text()`, or `blob()`.\n\nAnyone with a public URL can download the file without an API key. Only images, videos, and PDFs up to 50 MiB can be made public. A file has at most one public URL, so calling `createPublicUrl()` again returns the existing URL and updates its expiry if you pass a new `expires_after`. Without `expires_after`, the URL lasts as long as the file unless you revoke it. After revoking, copies already cached by the CDN can still be served briefly.\n\nThe Batch API processes large volumes of requests asynchronously at a reduced price. Most requests complete within 24 hours. Create a batch, then add requests to it:\n\n``` js\nconst batch = await client.batches.create({ name: \"feedback_sentiment\" });\n\nconst feedback = [\n  { id: \"feedback_001\", text: \"The product exceeded my expectations!\" },\n  { id: \"feedback_002\", text: \"Shipping took way too long.\" },\n];\n\nawait client.batches.requests.add(batch.batch_id, {\n  batch_requests: feedback.map((item) => ({\n    batch_request_id: item.id,\n    batch_request: {\n      responses: {\n        model: \"grok-4.7\",\n        input: [\n          { role: \"system\", content: \"Classify the sentiment as positive, negative, or neutral.\" },\n          { role: \"user\", content: item.text },\n        ],\n      },\n    },\n  })),\n});\n```\n\nEach `batch_request` holds one request. `responses` takes the same `CreateParams` as `client.responses.create()`, including the `store: false` default, and its result comes back as a `chat_get_completion` response. `image_generation`, `image_edit`, `video_generation`, and `video_extension` take the request body of the matching REST endpoint. Results can come back in any order, so give each request a `batch_request_id` that is unique within the batch. Not every model accepts batch requests; each [model page](https://docs.x.ai/developers/models) lists its Batch API support.\n\nWait until no requests are pending, then read the results:\n\n```\nawait client.batches.wait(batch.batch_id);\n\nfor await (const { batch_request_id, batch_result } of client.batches.results(batch.batch_id)) {\n  if (\"error\" in batch_result) {\n    console.error(batch_request_id, batch_result.error);\n  } else {\n    console.log(batch_request_id, batch_result.response);\n  }\n}\n```\n\n`wait()` polls every 5 seconds and rejects with `TimeoutError` after 24 hours. Pass `interval`, `timeout`, or `signal` to change that. Results are available as soon as each request finishes, so you can read them before the whole batch completes. Use `client.batches.requests.list()` to check the state of individual requests, `client.batches.list()` to list your team's batches, and `client.batches.cancel()` to stop the remaining requests. Finished results stay available after cancelling.\n\nConvert text to speech with `client.voice.speak()`. The audio comes back as an `BinaryResponse`, encoded as MP3 unless you set `output_format`:\n\n``` js\nimport { writeFile } from \"node:fs/promises\";\n\nconst speech = await client.voice.speak({\n  text: \"Welcome to SpaceX. [pause] How can I help you today?\",\n  language: \"en\",\n  voice_id: \"eve\",\n});\n\nawait writeFile(\"welcome.mp3\", await speech.bytes());\n```\n\nShape the delivery with [speech tags](https://docs.x.ai/developers/model-capabilities/audio/text-to-speech#speech-tags) in the text. Inline tags such as `[pause]`, `[long-pause]`, and `[laugh]` go where the sound should happen, and wrapping tags such as `<whisper>It's a secret.</whisper>` change how the enclosed text is spoken. The API doesn't report mistakes in tags, so TypeScript checks string literals as you type: it flags unknown tags such as `[laff]` and suggests the closest one, and it catches wrapping tags that are never closed, closed without being opened, or closed in the wrong order. To use a tag released after this SDK version, add `as UnsafeSpeechText` to the text, which skips the check. Searching for `UnsafeSpeechText` then finds every tag to clean up once the SDK knows it:\n\n``` js\nimport { type UnsafeSpeechText } from \"@xai-official/sdk\";\n\nawait client.voice.speak({\n  text: \"Hello [new-tag] there.\" as UnsafeSpeechText,\n  language: \"en\",\n});\n```\n\n`voice_id` autocompletes the built-in voices and accepts any other string, such as a custom voice ID or a voice added after this SDK version. List the built-in voices with `client.voice.list()`. To start playback before synthesis finishes, read `speech.body` as a stream. Set `with_timestamps: true` to receive JSON with base64 `audio` and per-character `audio_timestamps` instead of audio bytes.\n\nTranscribe a recording with `client.voice.transcribe()`. Pass the audio as a `Blob` or `File`, or pass `url` to have the API download it:\n\n``` js\nimport { openAsBlob } from \"node:fs\";\n\nconst transcript = await client.voice.transcribe({\n  file: await openAsBlob(\"./meeting.mp3\"),\n  language: \"en\",\n  format: true,\n});\n\nconsole.log(transcript.text);\n```\n\n`format: true` writes spoken numbers, currencies, and units in written form, and requires `language`. Word-level timings are in `transcript.words`.\n\nClone a voice from a reference clip of up to 120 seconds with `client.voice.custom.create()`. Creating custom voices through the API requires an Enterprise plan:\n\n``` js\nimport { openAsBlob } from \"node:fs\";\n\nconst voice = await client.voice.custom.create({\n  file: await openAsBlob(\"./reference.wav\"),\n  name: \"Friendly Narrator\",\n  language: \"en\",\n});\n\nconsole.log(voice.voice_id);\n```\n\nPass the returned `voice_id` to `speak()` or a realtime session like a built-in voice. `client.voice.custom` also provides `list()`, `get()`, `update()`, `delete()`, and `getAudio()`, which downloads the reference clip.\n\nRealtime voice sessions in a browser should authenticate with a short-lived client secret instead of your API key. Create one on your server:\n\n``` js\nconst secret = await client.voice.clientSecrets.create({\n  expires_after: { seconds: 300 },\n});\n```\n\nSend `secret.value` to the browser, which passes `xai-client-secret.<value>` as the WebSocket subprotocol when it connects to `wss://api.x.ai/v1/realtime`. Secrets expire after 10 minutes by default, and `expires_after.seconds` can be at most 3600.\n\nEncode text with a language model's tokenizer to count its tokens or see how it is split:\n\n```\nconst { token_ids } = await client.tokenizer.encode({\n  model: \"grok-4.7\",\n  text: \"Hello world!\",\n});\n\nconsole.log(token_ids.length);\nfor (const token of token_ids) {\n  console.log(token.token_id, token.string_token);\n}\n```\n\nInference requests add tokens of their own, so `usage.input_tokens` for a prompt can be higher than this count.\n\nThe API has no decode endpoint, but each token carries its bytes, so you can turn encoded tokens back into text. Decode `token_bytes` rather than joining `string_token`, because a token can hold part of a multi-byte character:\n\n``` js\nconst text = new TextDecoder().decode(\n  new Uint8Array(token_ids.flatMap((token) => token.token_bytes)),\n);\n```\n\nUse model IDs directly. `ModelId` suggests known string literals while still accepting models released after the installed SDK version:\n\n``` js\nimport { type KnownModelId } from \"@xai-official/sdk\";\n\nconst model: KnownModelId = \"grok-4.7\";\n\nconst available = await client.models.list();\nfor (const availableModel of available.data) {\n  console.log(availableModel.id);\n}\n\nconst modelInfo = await client.models.get(model);\nconsole.log(modelInfo);\n```\n\n`KnownModelId` is generated from the [SpaceXAI model documentation](https://docs.x.ai/developers/models). Use it when you want strict validation against the models known to this SDK release.\n\nImage generation models have their own catalog, which includes modalities, aliases, and pricing. `ImageModelId` and `KnownImageModelId` work the same way as the text model types:\n\n``` js\nimport { type KnownImageModelId } from \"@xai-official/sdk\";\n\nconst imageModel: KnownImageModelId = \"grok-imagine-image-2.0\";\n\nconst imageModels = await client.models.image.list();\nfor (const availableImageModel of imageModels.models) {\n  console.log(availableImageModel.id, availableImageModel.aliases);\n}\n\nconst imageModelInfo = await client.models.image.get(imageModel);\nconsole.log(imageModelInfo);\n```\n\nChat and image understanding models have their own catalog, which includes modalities, aliases, token pricing, and supported reasoning efforts:\n\n``` js\nconst languageModels = await client.models.language.list();\nfor (const languageModel of languageModels.models) {\n  console.log(languageModel.id, languageModel.input_modalities);\n}\n\nconst languageModelInfo = await client.models.language.get(\"grok-4.7\");\nconsole.log(languageModelInfo.capabilities?.reasoning_effort);\n```\n\nToken prices such as `prompt_text_token_price` are in USD cents per 100 million tokens. Divide them by 10,000 for US dollars per million tokens.\n\n`client.account.apiKey()` returns the name, status, and permissions of the API key the client is using:\n\n``` js\nconst apiKeyInfo = await client.account.apiKey();\nconsole.log(apiKeyInfo.name, apiKeyInfo.acls, apiKeyInfo.api_key_disabled);\n```\n\nThe SDK sends `store: false` unless you opt in. This differs from the API wire default. With storage disabled, the SDK requests encrypted reasoning content so `response.toInput()` can preserve context between turns.\n\nPass `store: true` when you need to retrieve, continue, inspect, or delete a response by ID:\n\n``` js\nconst stored = await client.responses.create({\n  model: \"grok-4.7\",\n  input: \"Save this response.\",\n  store: true,\n});\n\nconst fetched = await client.responses.get(stored.id);\nconst inputItems = await client.responses.inputItems.list(stored.id);\nawait client.responses.delete(stored.id);\n```\n\nList methods that return results in pages fetch the next page for you in a `for await` loop:\n\n``` js\nfor await (const file of client.files.list()) {\n  console.log(file.id);\n}\n```\n\nThis works for `files.list()`, `batches.list()`, `batches.results()`, `batches.requests.list()`, `voice.custom.list()`, and `responses.inputItems.list()`. Awaiting one of these calls instead returns a single page.\n\nConfigure defaults on the client:\n\n``` js\nconst client = new SpaceXAI({\n  timeout: 60_000,\n  idleTimeout: 30_000,\n  maxRetries: 2,\n});\n```\n\nOverride them for one request and pass an `AbortSignal` when needed:\n\n``` js\nconst controller = new AbortController();\n\nconst pending = client.responses.create(\n  {\n    model: \"grok-4.7\",\n    input: \"Write a detailed report.\",\n  },\n  {\n    signal: controller.signal,\n    timeout: 120_000,\n    maxRetries: 0,\n  },\n);\n\ncontroller.abort();\nawait pending;\n```\n\nRequests that generate content, such as `responses.create`, `images.generate`, and `images.edit`, retry only explicit `429` responses by default. Read-only requests may also retry transient HTTP failures. Retry delays honor `Retry-After` and use jittered exponential backoff.\n\nWhen you leave out `stream`, `responses.create()` streams the response under the hood and resolves to the final response. Streamed responses send headers right away, so long reasoning requests aren't cut off by limits on waiting for headers, such as the 5 minutes that Node's built-in `fetch` allows whatever `timeout` is set to. Reasoning can also go quiet for minutes, so these requests only apply `idleTimeout` when you pass it on the request. Set `stream: false` to send a plain JSON request instead.\n\nAll SDK errors extend `APIError`. Status-specific classes are exported for common API failures.\n\n``` js\nimport {\n  APIError,\n  AuthenticationError,\n  RateLimitError,\n} from \"@xai-official/sdk\";\n\ntry {\n  await client.responses.create({\n    model: \"grok-4.7\",\n    input: \"Hello\",\n  });\n} catch (error) {\n  if (error instanceof AuthenticationError) {\n    console.error(\"Check XAI_API_KEY\");\n  } else if (error instanceof RateLimitError) {\n    console.error(\"Rate limited. Retry later.\");\n  } else if (APIError.is(error)) {\n    console.error(error.status, error.code, error.param, error.requestId, error.message);\n  } else {\n    throw error;\n  }\n}\n```\n\nThe SDK exports `APIConnectionError`, `APIProtocolError`, `APIStatusError`, `AbortError`, `AuthenticationError`, `NotFoundError`, `OverloadedError`, `PermissionDeniedError`, `RateLimitError`, and `TimeoutError`.\n\nSet `XAI_DEBUG=1` to print each request's method, URL, and headers as a cURL command:\n\n```\nXAI_DEBUG=1 node app.js\n```\n\nAuthentication headers and common credential fields are redacted. Request bodies are always omitted because prompts and tool outputs may contain sensitive data.\n\nStructured API failures expose `error.type`, `error.code`, and `error.param` when the server returns them. The SpaceXAI request ID is also available at `response.http.requestId` and `error.requestId`. Include it when reporting an API problem.\n\nEvery request also sends an `x-client-request-id` header with a UUID generated by the SDK. The ID stays the same across retries and is available at `response.http.clientRequestId` and `error.clientRequestId`, even when a request fails before the API responds. To use your own ID, set `x-client-request-id` in the request `headers`.\n\nInstall dependencies and run the checks:\n\n```\npnpm install\npnpm check\npnpm pack:check\npnpm pack:smoke\n```\n\nRun every release gate, including the dependency audit:\n\n```\npnpm release:check\n```\n\nCreate a distributable tarball and SHA-256 checksum in `artifacts/`:\n\n```\npnpm pack:artifact\n```\n\nGenerated API types live in `src/generated/types.ts`, speech tags, voice IDs, and voice model IDs in `src/generated/voice.ts`, and the model ID union in `src/models.ts`. Run `pnpm generate:types` for API types, `pnpm generate:voice` for the Voice API values, and `pnpm generate:models` for model IDs instead of editing those files by hand.\n\nWe aren't accepting outside contributions yet, but plan to later. Please [open an issue](https://github.com/xai-org/xai-sdk-ts/issues) for bugs and feature requests. [CONTRIBUTING.md](https://github.com/xai-org/xai-sdk-ts/blob/main/CONTRIBUTING.md) covers local development.\n\nReport suspected vulnerabilities privately as described in [SECURITY.md](https://github.com/xai-org/xai-sdk-ts/blob/main/SECURITY.md). Do not include credentials, confidential data, or unredacted request bodies in public issues.\n\nLicensed under the [Apache License 2.0](https://github.com/xai-org/xai-sdk-ts/blob/main/LICENSE).", "url": "https://wpnews.pro/news/the-official-typescript-sdk-for-the-spacexai-api", "canonical_source": "https://github.com/xai-org/xai-sdk-ts", "published_at": "2026-10-02 21:50:04+00:00", "updated_at": "2026-10-02 22:06:12.058341+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "developer-tools", "large-language-models", "generative-ai"], "entities": ["xAI", "SpaceXAI API", "@xai-official/sdk", "TypeScript", "Node.js", "Grok", "grok-4.7", "XAI_API_KEY"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-official-typescript-sdk-for-the-spacexai-api", "markdown": "https://wpnews.pro/news/the-official-typescript-sdk-for-the-spacexai-api.md", "text": "https://wpnews.pro/news/the-official-typescript-sdk-for-the-spacexai-api.txt", "jsonld": "https://wpnews.pro/news/the-official-typescript-sdk-for-the-spacexai-api.jsonld"}}