Claude API in Go: HTTP requests with SSE streaming
A Claude and GPT client on the Go standard library: a plain request, error handling, SSE streaming, and bounded parallel requests.
3 min · · development
If your backend is written in Go, you don't need an official SDK to work with the Claude API: the standard net/http and encoding/json packages are enough. Below are working examples for Zukko: a plain request, error handling, a streaming (SSE) response, and parallel requests with goroutines. Everything goes through the OpenAI-compatible endpoint, so the same code works with Claude, GPT, and GLM — only the model field changes.
A basic Claude API request in Go
The key comes from the ZUKKO_API_KEY environment variable; the endpoint is https://api.zukko.pro/v1/chat/completions.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
context.WithTimeout bounds the whole request: if no response arrives within 60 seconds, client.Do returns context deadline exceeded, and a hung request won't block your handler. Raise the timeout for long answers. The usage.prompt_tokens and usage.completion_tokens fields are what you pay for, so they are worth logging.
Error handling
The gateway returns clear status codes, and the code tells you whether a retry makes sense. A custom error type helps:
type APIError struct {
Status int
Body string
}
func (e *APIError) Error() string {
switch e.Status {
case 401:
return "invalid API key (401)"
case 402:
return "insufficient balance (402)"
case 403:
return "model not allowed for this key (403)"
case 413:In ask, replace fmt.Errorf("API %d: %s", ...) with &APIError{Status: resp.StatusCode, Body: string(data)} and check the type with errors.As in the caller. A 403 means the model is not in the key's allowed list, which you set when creating the key in the dashboard. The error body has the shape {"error": {"message": ..., "code": ...}}.
Streaming responses over SSE
With "stream": true the server sends the answer in pieces as Server-Sent Events: lines like data: {...} separated by blank lines, ending with data: [DONE]. The last chunk may carry usage statistics and an empty choices array, so always check the array length.
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"A common mistake is comparing the whole line with [DONE]: the server actually sends data: [DONE], so strip the prefix first. The scanner buffer is raised to 1 MB so a long event doesn't abort reading with token too long.
Parallel requests with goroutines
Go is handy for batch processing: each task gets its own goroutine. But firing a hundred requests at once will earn you 429s. Cap concurrency with a semaphore built on a buffered channel. Replace main from the first example with this code and add "sync" to the imports:
func main() {
apiKey := os.Getenv("ZUKKO_API_KEY")
client := &http.Client{Timeout: 90 * time.Second}
ctx := context.Background()
prompts := []string{
"What is a goroutine?",
"What is a channel in Go?",
"How does a buffered channel differ from an unbuffered one?",
}
results := make([]string, len(prompts)) // answers keep the order of the questions
sem := make(chan struct{}, 3) // at most 3 requests in flight
var wg sync.WaitGroupEach goroutine writes to its own slice element, so no mutex is needed and results don't get mixed up. Tune the semaphore size empirically: start with 3–5 and raise it until you start seeing 429 responses.
Request cost from response headers
On regular (non-streaming) responses the gateway adds X-Zukko-Cost-Micros — the cost of the request in microdollars (1,000,000 = $1) — and X-Fetch-Balance-Cents with the remaining balance. Details are in the key limits and cache section. Inside ask, after the status check (import strconv):
if micros, err := strconv.ParseInt(resp.Header.Get("X-Zukko-Cost-Micros"), 10, 64); err == nil {
log.Printf("request cost: $%.6f", float64(micros)/1e6)
}This lets a service track its own spend and stop a batch job before the balance runs out.
FAQ
Where do I get an API key?
Create a key in the Zukko dashboard and store it in the ZUKKO_API_KEY environment variable. The full key is shown only once, at creation. A separate key with a daily limit for each service is a good habit.
Why do I get 404 on /v1/chat/completions?
Check the address: the full path is https://api.zukko.pro/v1/chat/completions. If you build the URL from a base address, the base for the OpenAI format is https://api.zukko.pro/v1. Request format details are in the Chat Completions docs.
How do I limit the response length?
Use the max_tokens field in the request body. The model stops at that limit even mid-thought; in that case finish_reason in the response is length.
Which models can I use?
Any available one: claude-sonnet-5, claude-opus-5-5, gpt-5.6-sol, gpt-5.6-luna, glm-5.3, glm-5.3-flash. The current list is at GET https://api.zukko.pro/v1/models or on the "Models" page in the dashboard. Check which model fits your task on your own data: run 20–30 typical requests through two or three models and compare the output and the cost.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.