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.

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.
| Behavior | What the docs say | What to do |
|---|---|---|
http.Post headers | To set custom headers, use NewRequest and DefaultClient.Do | Build the request yourself |
| Timeout | DefaultClient is &Client{}, used by Get, Head and Post; a Timeout of zero means no timeout | Create your own Client with a Timeout |
| Status codes | A non-2xx response doesn't cause an error | Check resp.StatusCode |
| Response body | The caller must close the response body | defer resp.Body.Close() |
| Client reuse | Clients should be reused instead of created as needed; they're safe for concurrent use | One package-level client |
*bytes.Reader body | ContentLength is set exactly and GetBody lets 307 and 308 redirects replay the body | Use 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 bothAuthorization: Bearerandx-api-keywere sent. Send exactly one.415 unsupported_media_type: the body wasn'tapplication/json. Set theContent-Typeheader.402 insufficient_credits: the balance can't cover the generation.429 rate_limitedor429 queue_full: back off and waitretry-afterwhen 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
- Guzzle retry middleware: retry a paid POST without paying twice
Guzzle's Middleware::retry takes a decider and a delay in milliseconds. Retry only transient answers, honor Retry-After, and keep one idempotency key.
- HubSpot custom coded actions: call an outside API safely
A HubSpot custom code action runs Node.js or Python in a workflow for 20 seconds, with secrets as env vars. Start slow API jobs there; don't wait.
- IFTTT webhooks: call an API that needs an API key
IFTTT's Webhooks service can send any web request with custom headers, on Pro plans. Route paid API calls through your own endpoint.
- Looping by Zapier: send one API request per item in a list
Looping by Zapier runs every later action once per list value, all in parallel, up to 500 times. Pace and key paid API calls to match.
Written by Sume