Golang HTTP POST JSON with headers and a Bearer token

http.Post can't set headers. Marshal the JSON, build it with http.NewRequest, set headers, send with a Client that has a Timeout, and close the body.

5 min readSume
All posts

To send an HTTP POST with JSON in Go, json.Marshal the body, build the request with http.NewRequest(http.MethodPost, url, bytes.NewReader(b)), set Content-Type: application/json and any other headers on req.Header, and send it with client.Do(req). http.Post works for a bare JSON body, but it has no header argument, so you can't add Authorization or an Idempotency-Key with it.

Go facts come from the net/http package docs. The example API is Sume's, from Authentication, Video Generation, Errors and rate limits and Jobs and results, all read on 2026-09-29. Sume's official client is a TypeScript SDK; from Go this is a plain HTTPS call.

How do I POST JSON with custom headers in Go?

This starts a Sume video job. The key is read from the environment on the server; Sume's docs say API keys don't belong in frontend JavaScript or mobile apps.

var client = &http.Client{Timeout: 30 * time.Second} // reuse one client

func startVideo(ctx context.Context) (map[string]any, error) {
	b, _ := json.Marshal(map[string]any{
		"model": "sume/auto", "prompt": "A vertical product clip on a desk",
		"aspect_ratio": "9:16", "duration": 5,
	})
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		"https://api.sume.com/v1/videos", bytes.NewReader(b))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SUME_API_KEY"))
	req.Header.Set("Idempotency-Key", "order-8823-clip-v1")

	resp, err := client.Do(req) // a non-2xx status is not an error
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var out map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	if resp.StatusCode >= 300 {
		return out, fmt.Errorf("status %d: %v", resp.StatusCode, out["error"])
	}
	return out, nil // 202: out["id"], out["polling_url"]
}

What does net/http do by default?

Four net/http defaults matter for a paid API call: no timeout on the default client, no error on a 4xx, a body you must close, and a client you should reuse. A Client.Timeout covers connection time, redirects and reading the response body.

From the Go net/http package docs, read 2026-09-29.
BehaviorWhat the docs sayWhat to do
http.Post headersTo set custom headers, use NewRequest and DefaultClient.DoBuild the request yourself
TimeoutDefaultClient is &Client{}, used by Get, Head and Post; a Timeout of zero means no timeoutCreate your own Client with a Timeout
Status codesA non-2xx response doesn't cause an errorCheck resp.StatusCode
Response bodyThe caller must close the response bodydefer resp.Body.Close()
Client reuseClients should be reused instead of created as needed; they're safe for concurrent useOne package-level client
*bytes.Reader bodyContentLength is set exactly and GetBody lets 307 and 308 redirects replay the bodyUse bytes.NewReader(b)

How do I read the JSON error from a failed POST?

Decode the body whatever the status. Sume's errors share one envelope, {"error": {"code", "message", "request_id", "details"}}, and the request id is safe to share with support. The codes you'll meet on a create:

  • 401 unauthorized: the key is missing or invalid, or both Authorization: Bearer and x-api-key were sent. Send exactly one.
  • 415 unsupported_media_type: the body wasn't application/json. Set the Content-Type header.
  • 402 insufficient_credits: the balance can't cover the generation.
  • 429 rate_limited or 429 queue_full: back off and wait retry-after when present.

Why does the POST return before the work is done?

Video generation is asynchronous. POST /v1/videos answers 202 Accepted with id, polling_url and status: "pending"; you poll GET /v1/videos/{id} with backoff until the status is completed. So keep the client Timeout short: it bounds one request, not the render. A client-side timeout doesn't cancel the job, which keeps running and still bills.

The Idempotency-Key makes a retry safe. On /v1/videos, a replay with the key returns the original job instead of starting a second paid one, and Sume's docs say not to retry unsafe submit requests without one. Build the key from what you're making, not uuid.New() per attempt. Golang HTTP client retry for POST covers the retry loop, and Golang webhook signature verification covers the callback side.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume