HiAPI
Blog

Tutorial

How to Use grok-4.7 via the HiAPI API: curl, Python, and a Working Request

A hands-on walkthrough from first request to a verified response

Nov 3, 2026 14 min read
HiAPI 博客教程封面图:多层半透明玻璃平面在浅色背景上折射出克制的冷蓝色带。

What you need before the first request

Before you send anything to the API, make sure four things are in place. Skipping any of them is the most common reason a first call fails for reasons that have nothing to do with the model.

A HiAPI account

You need an active HiAPI.ai account. Sign in to the dashboard to manage projects, keys, and usage. Everything in this tutorial goes through the same unified API surface, so the key you create here works across the models available on the platform.

An API key

Create an API key in the dashboard and store it somewhere safe — a shell environment variable for testing, and a secrets manager for anything that will run in production. Never commit a key to a repository or paste it into a client-side application.

For local testing, export it once per shell session:

export HIAPI_API_KEY="your-api-key-here"

The base URL

The base URL is the root of the HiAPI API. All requests in this tutorial are relative to it. Consult your dashboard for the exact base URL your account should use, and keep it in a variable so your scripts and your application code refer to the same value:

export HIAPI_BASE_URL="https://api.hiapi.ai"

The model identifier

The model identifier for this tutorial is grok-4.7. It is passed as a plain string in the request body. If the identifier is wrong, you will generally get a clear error back rather than a silent fallback, so a typo is easy to catch.

Once those four items exist — account, key, base URL, model ID — you have everything the request shape needs. There are no SDKs to install for the curl path, and the Python path only needs an HTTP client you probably already have.

A quick environment check

Confirm the key and base URL are visible to the shell you are about to use:

[ -n "$HIAPI_API_KEY" ] && echo "key set"
[ -n "$HIAPI_BASE_URL" ] && echo "base url set"

If both lines print, you are ready for the next section.

The shape of a grok-4.7 request on HiAPI

Every request in this tutorial shares the same four ingredients: an endpoint, an auth header, a model field, and a messages array. Understanding them separately makes streaming and tool calls much easier to reason about later, because those features change only parts of this shape.

Endpoint

The call is a POST to the chat completions endpoint under your base URL. In this tutorial we refer to it as:

POST {HIAPI_BASE_URL}/v1/chat/completions

Use the path shown in your dashboard if it differs; the request body is the same either way.

Auth header

Authentication is a bearer token in the Authorization header:

Authorization: Bearer $HIAPI_API_KEY

Because it is a header and not a query parameter, the key never appears in URLs, access logs, or browser history.

Content type

The body is JSON, so send Content-Type: application/json. Both curl and any Python HTTP client can set this for you, but it is worth stating explicitly so the header set is unambiguous.

Body: model and messages

The body is a JSON object with at least two fields:

  • model — the model identifier string, grok-4.7 here.
  • messages — an ordered array of message objects. Each object has a role and content. The usual roles are system for instructions that frame the conversation, user for the request itself, and assistant for prior model turns when you are continuing a conversation.

A minimal body looks like this:

{
  "model": "grok-4.7",
  "messages": [
    { "role": "system", "content": "You are a concise technical assistant." },
    { "role": "user", "content": "Explain what an idempotent HTTP method is in two sentences." }
  ]
}

That is the whole contract for a basic call. Anything else — streaming, tool definitions — is an additional field on this same object, which is why the earlier sections matter: once this shape is correct, the rest is additive.

What the response looks like

The response is a JSON object. The part you almost always want is under choices, which is an array; each entry has a message with a role and content. There is also metadata such as the model that served the request and a usage block describing token counts. Read choices[0].message.content and you have the answer text. The other fields are useful for logging and cost tracking, but you do not need them to get started.

A working curl request

With the key exported and the request shape understood, the curl call is short. This is the version to copy when you want to isolate a problem from application code.

curl -sS "$HIAPI_BASE_URL/v1/chat/completions" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "grok-4.7",
    "messages": [
      { "role": "system", "content": "You are a concise technical assistant." },
      { "role": "user", "content": "In one sentence, what is a bearer token?" }
    ]
  }'

What each flag does

  • -sS keeps output quiet but still prints errors. Without it, a failing request can look like an empty response.
  • The URL is built from the base URL variable plus the chat completions path, so you never hardcode a host in a script.
  • -H "Authorization: ..." sends the bearer token. The variable is expanded by the shell, which is why exporting it earlier matters.
  • -H "Content-Type: application/json" tells the server the body is JSON.
  • -d '...' is the request body. Single quotes around the JSON prevent the shell from interpreting the braces and quotes inside.

If you prefer to keep the body in a file, write it to body.json and replace the -d argument with -d @body.json. That is often cleaner once the messages array grows.

Reading the raw JSON

The response arrives as a JSON object on standard output. Pipe it through a formatter when you want to inspect it by eye:

curl -sS "$HIAPI_BASE_URL/v1/chat/completions" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json | python -m json.tool

You should see a choices array whose first element contains a message object with a content string — that string is the model's answer. Alongside it you will typically find an identifier for the response, the model name echoed back, and a usage object with prompt and completion token counts. Write down one successful response now: it is the reference you will compare against when you verify the Python client.

Common failures at this stage

A 401 almost always means the bearer token is missing, malformed, or not actually exported in the current shell. A 400 with a message about the body usually means the JSON is malformed — a trailing comma or a smart quote pasted from a document is the usual culprit. Fix those two before looking at anything else.

The same request in Python

The Python version does the same thing with the same body. The point of rewriting it is to move from a shell test into code you can put in an application, with the body as a data structure rather than a quoted string.

With the standard library

If you do not want to add a dependency, urllib is enough:

import json
import os
import urllib.request

api_key = os.environ["HIAPI_API_KEY"]
base_url = os.environ["HIAPI_BASE_URL"]

payload = {
    "model": "grok-4.7",
    "messages": [
        {"role": "system", "content": "You are a concise technical assistant."},
        {"role": "user", "content": "In one sentence, what is a bearer token?"},
    ],
}

request = urllib.request.Request(
    f"{base_url}/v1/chat/completions",
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    body = json.loads(response.read().decode("utf-8"))

print(body["choices"][0]["message"]["content"])

With requests

If requests is already in your environment, the call is shorter:

import os
import requests

response = requests.post(
    f"{os.environ['HIAPI_BASE_URL']}/v1/chat/completions",
    headers={"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}"},
    json={
        "model": "grok-4.7",
        "messages": [
            {"role": "system", "content": "You are a concise technical assistant."},
            {"role": "user", "content": "In one sentence, what is a bearer token?"},
        ],
    },
    timeout=60,
)
response.raise_for_status()
print(response.json()["choices"][0]["message"]["content"])

Why this version is safer for applications

Note three deliberate choices. The key and base URL come from environment variables, never from literals in the source. The body is a Python dictionary, so a misplaced quote produces an immediate SyntaxError instead of a confusing server-side rejection. And the call has a timeout: without one, a stalled connection can hang a worker indefinitely.

Run the Python version with the same prompt you used in curl. The wording of the answer may differ slightly between runs — models are not deterministic — but the structure of the response should match exactly: a choices array, a message with content, and a usage block. If the structure matches, your client is wired correctly and the remaining sections are optional enhancements.

Turning on streaming

Streaming changes how tokens arrive, not what you ask for. Instead of waiting for the full answer, you receive it incrementally, which is what makes chat interfaces feel responsive.

The request change

Add a single field to the body you already have:

{
  "model": "grok-4.7",
  "stream": true,
  "messages": [
    { "role": "user", "content": "Count from one to five, one number per line." }
  ]
}

Nothing else about the endpoint, headers, or messages changes. That is the whole reason for learning the base shape first: streaming is an additive flag, not a second API.

What comes back

With stream enabled, the response is no longer a single JSON object. It is a sequence of server-sent events. Each event carries a data: line whose payload is a JSON fragment shaped like the normal response, except that choices[0] contains a delta object rather than a full message. Early fragments may contain only a role; middle fragments carry pieces of content; the stream ends with a sentinel data: [DONE] line.

Your job as a client is to concatenate the delta.content strings in order and stop at the sentinel. Treat any fragment that lacks content as a no-op rather than an error.

Consuming chunks in Python

import json
import os
import requests

with requests.post(
    f"{os.environ['HIAPI_BASE_URL']}/v1/chat/completions",
    headers={"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}"},
    json={
        "model": "grok-4.7",
        "stream": True,
        "messages": [{"role": "user", "content": "Count from one to five."}],
    },
    stream=True,
    timeout=60,
) as response:
    response.raise_for_status()
    for line in response.iter_lines():
        if not line:
            continue
        line = line.decode("utf-8")
        if not line.startswith("data: "):
            continue
        data = line[len("data: "):]
        if data == "[DONE]":
            break
        delta = json.loads(data)["choices"][0].get("delta", {})
        text = delta.get("content")
        if text:
            print(text, end="", flush=True)

Two details make the difference between a stream that works and one that seems to hang. Pass stream=True to the HTTP client so it does not buffer the whole body, and iterate over lines rather than reading the response in one go. The flush=True on print is only about what you see on screen; without it, output can appear to arrive in a burst at the end even though the chunks were processed as they came.

Verifying streaming separately

Do not verify streaming and tool calls in the same test. First confirm the concatenated text equals a sane answer for a simple prompt, then add features one at a time. If a stream ends without the sentinel, that is a signal to inspect the connection rather than the model.

Tool-call options

Tool calls let the model request a function your code defines, rather than only producing prose. The request side is additive in exactly the same way streaming was.

Where the options go

Tool definitions are a tools array on the request body. Each entry describes one callable: a name, a description of when to use it, and a JSON-schema description of its parameters. The description matters more than it looks — it is the model's only guidance on when the tool is appropriate.

{
  "model": "grok-4.7",
  "messages": [
    { "role": "user", "content": "What is the weather in Lisbon right now?" }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Return the current weather for a named city.",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string", "description": "City name, e.g. Lisbon" }
          },
          "required": ["city"]
        }
      }
    }
  ]
}

The shape to remember is type plus function, with name, description, and parameters nested inside. If you have existing JSON-schema definitions, the parameters block is the place they go.

How the model replies

When the model decides a tool is needed, the assistant message in choices[0] is not ordinary text. Its content is typically empty and it carries a tool_calls array. Each entry has an identifier, a function name matching one of your definitions, and an arguments string containing JSON. Parse the arguments string — it is a string, not an object, and treating it as one is a frequent source of errors.

Your code then runs the function, and you send a follow-up request: the original conversation, the assistant message with its tool_calls, and a new message with role tool containing the call identifier and the result. The model uses that result to produce the final answer. The tool_call_id must match the identifier from the assistant message; without it, the follow-up is ambiguous.

Practical guardrails

  • Validate the parsed arguments against the same schema you sent. The model is guided by the schema, not bound to it.
  • Treat the returned function name as untrusted input and route only to an explicit allowlist of functions.
  • Do not enable stream and tools on your very first test. Confirm the non-streaming tool round trip end to end first, then combine if your use case needs both.

Verifying the response before you ship

A working call in a terminal is not the same as a verified integration. Work through this checklist once, then keep the script around as a smoke test.

Confirm the transport layer

Check that the HTTP status is 200 before parsing anything. A client that parses first will produce a confusing KeyError when the real problem was a 401. Assert the status explicitly, and log it on failure along with the response body — servers usually say what was wrong.

Confirm the response structure

Assert, don't assume. On a successful non-streaming call, verify three things: choices exists and is non-empty, choices[0].message.content is a non-empty string, and the model echoed in the response matches what you sent. That last check catches a surprising number of configuration mistakes where a default model overrides the one you requested.

Exercise the error paths on purpose

Do not wait for an outage to learn how your code behaves. Run three quick negatives:

  • Call with the auth header removed and confirm your code surfaces a clear authentication error rather than retrying forever.
  • Send a body with a missing model field and confirm you get a client-side error, not a hang.
  • Send a deliberately long prompt and confirm your timeout and truncation handling behave as intended.

Each of these should produce a specific, logged failure — not a generic "something went wrong."

Verify the enhancements separately

If you enabled streaming, confirm the concatenated chunks form a coherent answer and that the sentinel ends the loop. If you enabled tools, confirm the round trip completes: tool call parsed, function executed, result returned with the matching identifier, and a final text answer produced. Keep each verification in its own small script so a failure points at one feature.

Make the check repeatable

Put the whole sequence in one file that exits non-zero on any failed assertion, and run it against your staging configuration before each deploy. It should take a few seconds and cover the basic call, one streaming call, and one tool round trip if you use tools. When it passes, you can trust that a failure in production is about the request you are sending, not the plumbing underneath it.

Run the curl call one more time after the checks pass. If the raw JSON and your application output agree, the integration is finished.

Related articles

View all articles →

Start building with persistent AI artifacts

Get your API key and call image, video and audio models through one task-based API.

Read the API docs