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.

bash
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:

bash
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.

bash
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:

bash
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.WaitGroup

Each 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):

bash
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.

Get API keyDocumentation

api.zukko.pro · fetch_*

Claude API in Go: net/http for chat and streaming · Zukko