Upload an image with Go by building a multipart/form-data body with the field name file. Set the part media type to the real image type. Send the request with net/http, accept HTTP 200 or 202, decode the JSON, and close every response body.
Before you upload
imgd.dev costs $1 per GB as a one-time storage purchase. It has no free tier. Every imgd.dev image is public to anyone with its URL. Do not upload a private, confidential, or secret image.
Use Go 1.22 or later. This example uses only the standard library.
Load IMGD_KEY from a secret manager. Confirm that it exists without printing it:
test -n "$IMGD_KEY"
Complete Go example
Save this module file as examples/core/go/go.mod:
module example.com/imgd/core/go-upload
go 1.22
Save this code as examples/core/go/main.go:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"path/filepath"
"strings"
"time"
)
const (
uploadURL = "https://imgd.dev/v1/upload"
metadataURL = "https://imgd.dev/v1/images"
maxUploadBytes = 20 * 1024 * 1024
requestTimeout = 60 * time.Second
maxResponseBody = 1024 * 1024
)
var mimeTypes = map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
}
var statusGuidance = map[int]string{
http.StatusUnauthorized: "Load a valid IMGD_KEY from the secret manager.",
http.StatusPaymentRequired: "Buy storage or delete unused images, then retry.",
http.StatusRequestEntityTooLarge: "Reduce the image below 20 MB, then retry.",
http.StatusUnsupportedMediaType: "Use JPEG, PNG, GIF, WebP, or AVIF with the correct media type.",
}
type uploadResponse struct {
Hash string `json:"hash"`
URL string `json:"url"`
Status string `json:"status"`
MIME string `json:"mime"`
Bytes int64 `json:"bytes"`
Width *int `json:"width"`
Height *int `json:"height"`
AltText *string `json:"alt_text"`
UnpublishAt *string `json:"unpublish_at"`
Deduplicated bool `json:"deduplicated,omitempty"`
Note string `json:"note"`
}
type apiError struct {
Error string `json:"error"`
Fix string `json:"fix"`
}
type metadataResponse struct {
Hash string `json:"hash"`
}
func buildMultipartBody(imagePath string) (*bytes.Buffer, string, error) {
info, err := os.Stat(imagePath)
if err != nil {
return nil, "", fmt.Errorf("inspect image: %w", err)
}
if info.Size() == 0 {
return nil, "", fmt.Errorf("the image file is empty")
}
if info.Size() > maxUploadBytes {
return nil, "", fmt.Errorf("the image is larger than the 20 MB upload limit")
}
extension := strings.ToLower(filepath.Ext(imagePath))
mediaType, ok := mimeTypes[extension]
if !ok {
return nil, "", fmt.Errorf("use a JPEG, PNG, GIF, WebP, or AVIF file name")
}
file, err := os.Open(imagePath)
if err != nil {
return nil, "", fmt.Errorf("open image: %w", err)
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition", mime.FormatMediaType("form-data", map[string]string{
"name": "file",
"filename": filepath.Base(imagePath),
}))
header.Set("Content-Type", mediaType)
part, err := writer.CreatePart(header)
if err != nil {
return nil, "", fmt.Errorf("create multipart file field: %w", err)
}
if _, err := io.Copy(part, file); err != nil {
return nil, "", fmt.Errorf("copy image into multipart body: %w", err)
}
if err := writer.Close(); err != nil {
return nil, "", fmt.Errorf("close multipart body: %w", err)
}
return body, writer.FormDataContentType(), nil
}
func responseError(response *http.Response) error {
var payload apiError
if err := json.NewDecoder(io.LimitReader(response.Body, maxResponseBody)).Decode(&payload); err != nil {
return fmt.Errorf("request failed with HTTP %d and invalid JSON", response.StatusCode)
}
if payload.Error == "" {
payload.Error = "unknown_error"
}
if payload.Fix == "" {
payload.Fix = statusGuidance[response.StatusCode]
}
if payload.Fix == "" {
payload.Fix = "Read the response and correct the request."
}
return fmt.Errorf("request failed with HTTP %d (%s): %s", response.StatusCode, payload.Error, payload.Fix)
}
func uploadImage(client *http.Client, imagePath, apiKey string) (uploadResponse, error) {
body, contentType, err := buildMultipartBody(imagePath)
if err != nil {
return uploadResponse{}, err
}
request, err := http.NewRequest(http.MethodPost, uploadURL, body)
if err != nil {
return uploadResponse{}, fmt.Errorf("create upload request: %w", err)
}
request.Header.Set("Authorization", "Bearer "+apiKey)
request.Header.Set("Content-Type", contentType)
response, err := client.Do(request)
if err != nil {
return uploadResponse{}, fmt.Errorf("send upload request: %w", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusAccepted {
return uploadResponse{}, responseError(response)
}
var upload uploadResponse
if err := json.NewDecoder(io.LimitReader(response.Body, maxResponseBody)).Decode(&upload); err != nil {
return uploadResponse{}, fmt.Errorf("decode upload response: %w", err)
}
if upload.Hash == "" || upload.URL == "" || upload.Status == "" || upload.MIME == "" {
return uploadResponse{}, fmt.Errorf("the upload response does not match the expected success contract")
}
return upload, nil
}
func verifyUpload(client *http.Client, upload uploadResponse, apiKey string) error {
request, err := http.NewRequest(http.MethodGet, metadataURL+"/"+upload.Hash, nil)
if err != nil {
return fmt.Errorf("create metadata request: %w", err)
}
request.Header.Set("Authorization", "Bearer "+apiKey)
metadataHTTPResponse, err := client.Do(request)
if err != nil {
return fmt.Errorf("send metadata request: %w", err)
}
defer metadataHTTPResponse.Body.Close()
if metadataHTTPResponse.StatusCode != http.StatusOK {
return responseError(metadataHTTPResponse)
}
var metadata metadataResponse
if err := json.NewDecoder(io.LimitReader(metadataHTTPResponse.Body, maxResponseBody)).Decode(&metadata); err != nil {
return fmt.Errorf("decode metadata response: %w", err)
}
if metadata.Hash != upload.Hash {
return fmt.Errorf("the metadata response did not confirm the upload hash")
}
publicHTTPResponse, err := client.Get(upload.URL)
if err != nil {
return fmt.Errorf("request public URL: %w", err)
}
defer publicHTTPResponse.Body.Close()
if publicHTTPResponse.StatusCode != http.StatusOK {
return fmt.Errorf("the public URL returned HTTP %d", publicHTTPResponse.StatusCode)
}
contentType := publicHTTPResponse.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "image/") {
return fmt.Errorf("the public URL returned the unexpected type %q", contentType)
}
firstByte := make([]byte, 1)
if _, err := io.ReadFull(publicHTTPResponse.Body, firstByte); err != nil {
return fmt.Errorf("read public image body: %w", err)
}
return nil
}
func run() error {
if len(os.Args) != 2 {
return fmt.Errorf("usage: go run . IMAGE_PATH")
}
apiKey := os.Getenv("IMGD_KEY")
if apiKey == "" {
return fmt.Errorf("set IMGD_KEY in the environment")
}
client := &http.Client{Timeout: requestTimeout}
upload, err := uploadImage(client, os.Args[1], apiKey)
if err != nil {
return err
}
if err := verifyUpload(client, upload, apiKey); err != nil {
return err
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(upload); err != nil {
return fmt.Errorf("write upload response: %w", err)
}
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Format, build, and run the example
Format and build it before use:
gofmt -w examples/core/go/main.go
cd examples/core/go
go test ./...
go build ./...
Run the program from the example directory:
go run . ../../image.png
Use a path that points to your actual image. The program accepts JPEG, PNG, GIF, WebP, and AVIF.
Expected JSON
A new PNG can produce this output after the verification requests succeed:
{
"hash": "0000000000000000000000000000000000000000000000000000000000000000",
"url": "https://i.imgd.dev/i/0000000000000000000000000000000000000000000000000000000000000000",
"status": "processing",
"mime": "image/png",
"bytes": 71680,
"width": 1280,
"height": 720,
"alt_text": null,
"unpublish_at": null,
"note": "the url works immediately, serving a blurred placeholder until the moderation check finishes and it sharpens to the real image"
}
New bytes return HTTP 202. Identical bytes return HTTP 200 and can include deduplicated: true.
The public URL works immediately. It returns a blurred placeholder while the status is processing.
Why the code creates a custom file part
multipart.Writer creates the boundary and closing marker. CreatePart lets the program set the real image media type.
The server reads the media type from the multipart file part. A generic application/octet-stream part would receive HTTP 415.
mime.FormatMediaType safely builds the file part disposition. The field name is exactly file.
The 20 MB limit makes an in-memory multipart body practical for this example. Use the service limit before you choose a streaming design.
How JSON and status checks work
The upload function accepts only HTTP 200 and 202. It decodes the body into uploadResponse and checks required fields.
For non-2xx responses, responseError decodes error and fix. It uses the server instruction when that value is present.
Each JSON decoder has a 1 MB response limit. Upload and error bodies are much smaller than that limit.
Verify the result and close bodies
verifyUpload requests the owner metadata with the bearer key. It confirms that the metadata hash equals the upload hash.
It then requests the public URL without authentication. It requires HTTP 200, an image content type, and at least one byte.
The upload, metadata, and public HTTP responses each call defer response.Body.Close() immediately after a successful request.
The Go net/http documentation requires the caller to close each response body. The example follows that rule on success and error paths.
Recover from API errors
Use the HTTP status, stable error code, and server fix value.
| Status | Cause | Action |
|---|---|---|
401 |
IMGD_KEY is missing or invalid. |
Load the correct key from the secret manager. Do not print it. |
402 |
The account is unpaid or lacks quota. | Buy storage or delete unused images. Then run the program again. |
413 |
The upload is larger than 20 MB. | Resize or compress the image below 20 MB. |
415 |
The multipart part has an unsupported type. | Use JPEG, PNG, GIF, WebP, or AVIF and set its real media type. |
Do not retry these responses without a change. You can retry a network error with the same bytes. Content addressing returns the same hash and URL.
A valid error body can look like this:
{
"error": "unsupported_media_type",
"fix": "'application/octet-stream' is not accepted; send jpeg, png, gif, webp, or avif and set the matching Content-Type",
"accepted": [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/avif"
]
}
Security rules
Keep IMGD_KEY in the process environment. Do not put it in a URL, source file, error message, or normal output.
The program prints only the success JSON. That JSON contains a public URL and no key.
Inspect image content before upload. Public storage does not protect private text, customer records, or production data.
Decide if Go fits
This example fits Go tools that need a standard-library upload client. It also fits callers that require explicit body cleanup and status checks.
It does not fit private images, access control, unsupported files, images above 20 MB, or a free tier. Read the service evaluation guide first.
Result
The program builds a valid multipart request, decodes success or error JSON, closes every response body, and verifies the public image.
Compare it with the Python Requests guide or the TypeScript upload guide.
Give an agent the integration procedure. A person can start from the home page.