Self-hosted REST API wrapping grok.com into an OpenAI-compatible endpoint. Pure Rust, single binary, no headless browser, no JS engine. Reverse-engineers Grok's anti-bot challenge cryptographically so requests look like a real browser session.
Runs on your grok.com cookies, so inference bills against your own grok.com quota (free, SuperGrok, or Heavy). No xAI API key needed.
- Rust 1.85 or newer (rustup)
- NASM
- LLVM and libclang (needed by BoringSSL in wreq)
git clone https://github.com/imjustprism/grok-web-api.git
cd grok-web-api
cp .env.example .envEdit .env. Five required values come in two groups.
- Log in to grok.com
- Open DevTools (F12) and go to Application (Chrome or Edge) or Storage (Firefox)
- In the sidebar open Cookies and pick
https://grok.com - Copy the
Valueof the row namedssointoGROK_SSO_COOKIE - Copy the
Valueof the row namedsso-rwintoGROK_SSO_RW_COOKIE
Both rows typically hold the same JWT. Cookies rotate when you log out or clear storage.
Grok's web client signs every POST with an x-statsig-id header. This server needs three constants to reproduce the signature.
With Void installed, paste this snippet in grok.com's browser console:
var m=Void.findByProps("chatApi"),p=m.chatApi.configuration.middleware[0].pre,r=Math.random,d=Date.now,g=crypto.subtle.digest.bind(crypto.subtle),h;Math.random=()=>0;Date.now=()=>1e12;crypto.subtle.digest=async(a,b)=>{h=new TextDecoder().decode(b);return g(a,b)};var s=await p({url:"https://grok.com/rest/app-chat/x",init:{method:"POST",headers:{}}});Math.random=r;Date.now=d;crypto.subtle.digest=g;var t=new Uint8Array([...atob(s.init.headers["x-statsig-id"])].map(c=>c.charCodeAt(0)));console.log(`CHALLENGE_HEADER_HEX=${[...t.slice(0,49)].map(b=>b.toString(16).padStart(2,"0")).join("")}\nCHALLENGE_SUFFIX=${h.split("!").slice(2).join("!").replace(/^-?\d+/,"")}\nCHALLENGE_TRAILER=${t[69]}`)Copy the three output lines into .env. Re-run the snippet whenever Grok ships a new build.
To skip manual editing, pipe the values straight into .env with the built-in subcommand:
cargo run -- update-keys CHALLENGE_HEADER_HEX=... CHALLENGE_SUFFIX=... CHALLENGE_TRAILER=3It rewrites the matching lines in .env (creating missing ones) and leaves everything else untouched. Keys are validated against the known config names. Any of the variables in the configuration table can be set this way, e.g. cargo run -- update-keys GROK_SSO_COOKIE=.... Alias update-keys to grok-server update-keys and a clipboard-extraction bookmarklet can drop a ready-to-run command straight into your terminal.
Without Void (manual extraction)
- DevTools Network tab on grok.com, pick any POST carrying
x-statsig-id - Base64-decode the header into 70 raw bytes
- Bytes 0 through 48 (hex-encoded) become
CHALLENGE_HEADER_HEX - Byte 69 becomes
CHALLENGE_TRAILER - Breakpoint the middleware that calls
crypto.subtle.digest, grab the string passed in, take everything after the second!, strip the leading counter digits. That'sCHALLENGE_SUFFIX
cargo run --releaseOr Docker:
docker compose up -dServer binds http://0.0.0.0:3000 by default.
Five modes map to the options grok.com exposes in its UI.
| Model ID | UI label | Notes |
|---|---|---|
auto |
Auto | Picks Fast or Expert per query |
fast |
Fast | Quick responses |
expert |
Expert | Deeper reasoning |
heavy |
Heavy | Multi-agent orchestration. Requires Heavy plan |
grok-43 |
Grok 4.3 | Early access. Sent as Grok's modelMode; may require account access |
Unknown model IDs fall back to auto with a warning log. The server does not expose grok-2, grok-3, grok-4, grok-4-mini, or other legacy names because the web client no longer routes to them.
curl http://localhost:3000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"hello"}]}'curl -N http://localhost:3000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"auto","stream":true,"messages":[{"role":"user","content":"hello"}]}'SSE format matches OpenAI chunks. Final event is data: [DONE].
from openai import OpenAI
client = OpenAI(base_url="http://localhost:3000/v1", api_key="unused")
resp = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "hello"}],
)
print(resp.choices[0].message.content)Any OpenAI-shaped client works: LiteLLM, Open WebUI, Cursor, Continue, aider, llm, etc.
Grok's web API has no equivalent for these, so the server accepts and drops them without error:
temperaturetop_pmax_tokensmax_completion_tokensresponse_format
Implemented as a prompted protocol. The server injects a system block describing the tools and a required XML call format, then parses <tool_call>{...}</tool_call> out of Grok's output and re-emits them as OpenAI tool_calls.
Supported request fields:
tools(array of{"type":"function","function":{...}})tool_choice("auto","none","required", or{"type":"function","function":{"name":"X"}})messages[].tool_callson assistant turnsmessages[].tool_call_idonrole:"tool"turns
Supported response fields:
choices[0].message.tool_callsin non-streaming modechoices[0].delta.tool_callsin streaming modefinish_reason:"tool_calls"when a call was emitted
Reliability notes based on live testing:
expertwithtool_choice:"required"is the most reliable pathautounder defaulttool_choiceoften refuses to call tools and answers the user directlytool_choice:"none"skips the tool-block injection entirely
Example request:
curl http://localhost:3000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "expert",
"messages": [{"role": "user", "content": "What is the weather in Paris?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"tool_choice": "required"
}'Example response:
{
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"tool_calls": [{
"id": "call_...",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}
}]
},
"finish_reason": "tool_calls"
}]
}Second turn feeds the result back:
{
"model": "expert",
"messages": [
{"role": "user", "content": "What is the weather in Paris?"},
{"role": "assistant", "content": "",
"tool_calls": [{"id": "call_1", "type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}}]},
{"role": "tool", "tool_call_id": "call_1",
"content": "{\"temp_c\":18,\"conditions\":\"partly cloudy\"}"}
]
}The server renders tool-call and tool-result history back into Grok's turn as text, so multi-step agent loops work.
prompt_tokens, completion_tokens, and total_tokens are always 0. Grok's web API does not expose token counts.
OpenClaw is an open-source local agent runtime. Point it at this server as an OpenAI-compatible provider and Grok drives OpenClaw's full tool surface (shell, browser, filesystem, 50+ integrations) on your real host.
Add to ~/.openclaw/openclaw.json:
{
models: {
providers: {
grok_web: {
baseUrl: "http://localhost:3000/v1",
apiKey: "unused",
api: "openai-completions",
models: [
{ id: "auto", name: "Grok Auto", contextWindow: 131072, maxTokens: 8192 },
{ id: "fast", name: "Grok Fast", contextWindow: 131072, maxTokens: 8192 },
{ id: "expert", name: "Grok Expert", contextWindow: 131072, maxTokens: 8192 },
{ id: "heavy", name: "Grok Heavy", contextWindow: 131072, maxTokens: 8192 },
{ id: "grok-43", name: "Grok 4.3", contextWindow: 131072, maxTokens: 8192 },
],
},
},
},
}Notes:
apiKeyis a placeholder. It only matters if you setAPI_KEYin this server's.env. Otherwise OpenClaw does not authenticate against this server and the real auth happens through your grok.com cookies.contextWindowandmaxTokensdepend on your grok.com plan. 131072 is a safe default for SuperGrok. Free tier is lower, Heavy is higher.- For agent loops, prefer
expert. Tell OpenClaw to usetool_choice:"required"when it needs a forced tool call.
Everything under /v1/* not covered by the OpenAI surface is a typed wrapper around grok.com's internal REST endpoints.
curl -X POST http://localhost:3000/v1/chat \
-H "Content-Type: application/json" \
-d '{"message":"hello","temporary":true}'
curl http://localhost:3000/v1/conversations
curl http://localhost:3000/v1/models| Method | Path | Description |
|---|---|---|
POST |
/v1/chat/completions |
OpenAI-compatible chat, tools, streaming |
GET |
/v1/models |
List supported models |
GET |
/v1/models/:id |
Get a model |
| Method | Path | Description |
|---|---|---|
POST |
/v1/chat |
New conversation (streaming) |
POST |
/v1/chat/quick |
Quick answer without persisting a conversation |
POST |
/v1/chat/:id/message |
Continue a conversation |
POST |
/v1/chat/:id/stop |
Stop generation |
POST |
/v1/chat/:id/cancel |
Cancel a response |
GET |
/v1/chat/:id/reconnect |
Reconnect to an in-flight response |
| Method | Path | Description |
|---|---|---|
GET |
/v1/conversations |
List conversations |
DELETE |
/v1/conversations |
Delete all conversations |
GET |
/v1/conversations/deleted |
List soft-deleted |
GET |
/v1/conversations/:id |
Get a conversation |
PUT |
/v1/conversations/:id |
Update a conversation |
DELETE |
/v1/conversations/:id |
Delete a conversation |
GET |
/v1/conversations/:id/exists |
Check existence |
POST |
/v1/conversations/:id/restore |
Restore deleted |
POST |
/v1/conversations/:id/title |
Generate title |
GET |
/v1/conversations/:id/responses |
List responses |
GET |
/v1/conversations/:id/artifacts |
Get artifact metadata |
| Method | Path | Description |
|---|---|---|
POST |
/v1/files |
Upload file (max 64 MB) |
GET |
/v1/files/:id/metadata |
Get file metadata |
POST |
/v1/code/run |
Execute code |
| Method | Path | Description |
|---|---|---|
GET |
/v1/voice/read/:id |
TTS stream |
GET |
/v1/voice/audio/:id |
TTS audio file |
POST |
/v1/voice/tts |
Text-to-speech |
POST |
/v1/voice/livekit/token |
Issue LiveKit JWT for realtime voice against wss://livekit.grok.com |
Realtime voice flow: call POST /v1/voice/livekit/token, take the returned token, connect any LiveKit SDK to wss://livekit.grok.com with it. The server-assigned room auto-admits the prod voice agent, which subscribes to your mic track and publishes synthesized audio back. The server proxies only the token issuance; media flows directly from client to LiveKit over WebRTC.
curl -X POST http://localhost:3000/v1/voice/livekit/token \
-H "Authorization: Bearer $GROK_API_KEY"| Method | Path | Description |
|---|---|---|
GET |
/v1/memory/blurb |
Memory summary |
GET |
/v1/memory/v2/:id |
Get memory |
PUT |
/v1/memory/v2/:id |
Update memory |
DELETE |
/v1/memory/v2/:id |
Delete memory |
DELETE |
/v1/memory/v2/all/:companion_id |
Delete all memories for a companion |
GET |
/v1/artifacts/:id |
Get artifact |
PUT |
/v1/artifacts/:id |
Update artifact |
GET |
/v1/artifacts/:id/content/:version_id |
Get artifact content |
| Method | Path | Description |
|---|---|---|
POST |
/v1/sharing/:id |
Share a conversation |
POST |
/v1/sharing/:id/artifact |
Share an artifact |
GET |
/v1/sharing/links |
List share links |
GET |
/v1/sharing/links/:id |
Get share link |
DELETE |
/v1/sharing/links/:id |
Delete share link |
POST |
/v1/sharing/links/:id/clone |
Clone share link |
GET |
/v1/sharing/artifacts/:id |
Get shared artifact |
| Method | Path | Description |
|---|---|---|
GET |
/v1/suggestions |
Search suggestions |
GET |
/v1/suggestions/starters |
Conversation starters |
POST |
/v1/suggestions/follow-up |
Follow-up suggestions |
GET |
/v1/images |
List image generations |
| Method | Path | Description |
|---|---|---|
GET |
/v1/google-drive/files |
List files |
GET |
/v1/google-drive/files/:id |
Read file |
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness check |
GET |
/health/session |
Cookie validity |
GET |
/status |
Request counters |
GET |
/setup |
Challenge extraction helper |
ANY |
/raw/* |
Raw passthrough to the Grok web API |
| Variable | Required | Default | Description |
|---|---|---|---|
GROK_SSO_COOKIE |
yes | sso cookie |
|
GROK_SSO_RW_COOKIE |
yes | sso-rw cookie |
|
CHALLENGE_HEADER_HEX |
yes | 49-byte anti-bot header, hex | |
CHALLENGE_SUFFIX |
yes | Anti-bot suffix string | |
CHALLENGE_TRAILER |
no | 3 |
Anti-bot trailer byte |
GROK_EXTRA_COOKIES |
no | Extra cookies to forward (rarely needed) | |
GROK_BASE_URL |
no | https://grok.com |
Override upstream base |
TOKEN_PROVIDER_URL |
no | External HTTP token provider instead of in-process challenge | |
API_KEY |
no | Bearer token required by this server's clients | |
HOST |
no | 0.0.0.0 |
Bind address |
PORT |
no | 3000 |
Listen port |
SESSION_CHECK_INTERVAL_SECS |
no | 300 |
Background cookie validity poll, minimum 30 |
LOG_LEVEL |
no | info |
Log level filter |
Every error response is RFC 7807 JSON:
{"type":"bad_request","title":"Bad Request","status":400,"detail":"messages must contain at least one non-system message"}Common types:
bad_request(400): malformed inputunauthorized(401): missing or wrongAPI_KEYnot_found(404): unknown route or resourceauth_expired(503): grok.com session cookies expiredupstream_error(502): Grok returned an error
Streaming errors surface as SSE events with shape {"error":{"message":"...","type":"..."}}.
Grok's web client runs obfuscated JS that generates a per-request x-statsig-id. Requests without it get rejected.
Token layout (70 bytes, base64-encoded):
header[49] + counter_le32[4] + sha256(method + "!" + path + "!" + counter + suffix)[0..16] + trailer[1]
Every byte is then XOR'd with a random key.
header: 49-byte static fingerprint extracted once from the browsercounter: seconds since a hardcoded epoch (May 1, 2023)suffix: static string baked into the challenge JStrailer: single constant byte
This server rebuilds that exact algorithm in ~60 lines of Rust with sha2. Tokens are cryptographically identical to browser output. No JS engine, no eval, no WebDriver.
On top, wreq (a reqwest fork using BoringSSL) produces a TLS fingerprint matching Chrome. Cloudflare JA3 and JA4 checks pass without a real browser.
grok-client works standalone.
[dependencies]
grok-client = { git = "https://github.com/imjustprism/grok-web-api.git" }
futures = "0.3"
tokio = { version = "1", features = ["full"] }use futures::StreamExt;
use grok_client::{ChallengeConfig, GrokAuth, GrokClient};
use grok_client::types::chat::NewConversationRequest;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let auth = GrokAuth::new("sso_cookie", "sso_rw_cookie")?;
let challenge = ChallengeConfig::new("header_hex", "suffix", 3)?;
let client = GrokClient::new(auth)?.with_token_provider(challenge);
let mut request = NewConversationRequest::new("Explain the Raft consensus algorithm");
request.temporary = Some(true);
let mut stream = client.create_conversation(&request).await?;
while let Some(chunk) = stream.next().await {
let _chunk = chunk?;
}
Ok(())
}crates/
grok-client/ Typed HTTP client, challenge token generator, streaming parser
grok-server/ Axum REST API with OpenAI compatibility layer and tool-calling bridge
Split so grok-client is usable without pulling Axum.
Other wrappers either shell out to a real browser or skip the anti-bot challenge and break within days. This one solves it at the crypto layer. Single ~5 MB binary, full endpoint coverage, no runtime dependencies.
Reverse-engineers Grok's internal web API. Not affiliated with or endorsed by xAI. May violate xAI's Terms of Service. Use at your own risk.