How to Use the Stripe API in Go

Web
Initialize the Stripe Go client with your secret key and call API methods like Charge.New to process payments securely.

The checkout flow

You are building a payment endpoint. The user clicks a button, your server receives a request, and now you need to talk to Stripe. You could write raw HTTP requests, manually serialize JSON, manage API version headers, and parse error responses yourself. Or you can use the official Stripe Go client. The SDK handles the network boilerplate so you can focus on business logic.

Think of the Stripe API as a specialized kitchen. You are the waiter taking orders. You could walk into the kitchen, shout ingredients, and hope the chef understands your phrasing. Or you can use a standardized order ticket. The Stripe Go SDK is that ticket. It translates your Go structs into the exact JSON the API expects, attaches authentication headers, pins the API version, and parses the response back into strongly typed Go objects. You do not need to memorize HTTP verbs or JSON field names. You fill out the struct and send it.

The SDK is a translator, not a magic wand. You still need to understand what you are asking for.

How the SDK translates your code

Stripe's Go library follows a consistent pattern. Every resource lives in its own subpackage. Charges live in charge, customers in customer, invoices in invoice. Each subpackage exports a New function, an Update function, and a List function. The functions accept a pointer to a Params struct. The struct fields map directly to the API documentation.

Optional fields use pointers. Required fields use values. This design lets the SDK distinguish between "the user did not provide this field" and "the user explicitly set this field to zero or empty." If you pass a zero value for an optional pointer, the SDK omits it from the JSON payload. If you pass a non-nil pointer, the SDK includes it. This prevents accidental overwrites of server-side defaults.

Go's error handling is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. You check err != nil immediately after every SDK call. You do not defer error checking. You do not swallow errors in a blank identifier unless you have a documented reason. The compiler will not stop you from ignoring an error, but your code review will.

Pointers for optional fields. Values for required ones. Trust the struct definitions.

Minimal example

Here is the simplest way to charge a test card. You set your secret key, build a parameter struct, and call the SDK method.

package main

import (
	"fmt"
	"log"

	"github.com/stripe/stripe-go/v80"
	"github.com/stripe/stripe-go/v80/charge"
)

func main() {
	// Load from environment in production. Hardcoded keys leak in version control.
	stripe.Key = "sk_test_51ABC123..."

	// The SDK expects pointers for optional fields to distinguish unset from zero.
	params := &stripe.ChargeParams{
		Amount:   stripe.Int64(2000), // Amount in smallest currency unit, not dollars
		Currency: stripe.String("usd"),
		Source:   stripe.String("tok_visa"), // Test token guarantees predictable success
	}

	// New marshals the struct, sends POST /v1/charges, and unmarshals the response.
	c, err := charge.New(params)
	if err != nil {
		// Log the full error. Stripe returns machine-readable codes like "card_declined".
		log.Fatal(err)
	}

	fmt.Printf("Charge ID: %s\n", c.ID)
}

What happens under the hood

When you call charge.New, the SDK performs a sequence of deterministic steps. It grabs the global stripe.Key and attaches it as a Bearer token in the Authorization header. It reads the API version from the module definition and adds it to the Stripe-Version header. It walks the ChargeParams struct, skips nil pointers, and serializes the remaining fields into JSON. It opens a TCP connection to api.stripe.com, sends the request, and waits for the response.

If Stripe returns a 200 OK, the SDK unmarshals the JSON into a Charge struct. The ID field contains the unique transaction identifier. The Status field tells you whether the payment succeeded, failed, or is pending. If Stripe returns a 4xx or 5xx, the SDK returns an error. The error type implements the standard error interface and usually exposes the HTTP status code, the Stripe error code, and a human-readable message.

You check the error immediately. If you forget, the compiler will not stop you, but your runtime will panic when you try to access c.ID on a nil pointer. Go does not use exceptions. Control flow stays linear. You handle the failure where it happens.

Context is plumbing. Run it through every long-lived call site.

Production-ready handler

Real applications need timeouts, proper HTTP status codes, and context propagation. Here is how a payment endpoint looks when it respects Go conventions.

import (
	"context"
	"encoding/json"
	"net/http"
	"time"

	"github.com/stripe/stripe-go/v80"
	"github.com/stripe/stripe-go/v80/charge"
)

// HandleCharge processes a payment request and returns the result.
func HandleCharge(w http.ResponseWriter, r *http.Request) {
	// Context carries the request lifecycle. Timeout prevents hanging goroutines.
	ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
	defer cancel()

	params := &stripe.ChargeParams{
		Amount:   stripe.Int64(1500),
		Currency: stripe.String("usd"),
		Source:   stripe.String("tok_visa"),
	}

	// Pass context explicitly. The SDK respects cancellation and deadlines.
	c, err := charge.NewWithContext(ctx, params)
	if err != nil {
		// Map Stripe errors to appropriate HTTP status codes for the client.
		w.WriteHeader(http.StatusPaymentRequired)
		json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"charge_id": c.ID})
}

The context.Context always goes as the first parameter, conventionally named ctx. Functions that take a context should respect cancellation and deadlines. The SDK's NewWithContext variant checks the context before sending the request and aborts if the deadline passes. This prevents your server from holding open connections to Stripe when the client disconnects.

You also notice the receiver naming convention in the SDK source code. Methods on structs use one or two letter names matching the type. (c *Charge) Update(...) instead of (this *Charge). It is a small detail, but it keeps the codebase consistent across thousands of files. Most editors run gofmt on save, so you never argue about indentation. You argue about logic, not formatting.

The worst payment bug is the one that silently drops a transaction. Log the error code, not just the message.

Common traps and compiler feedback

Stripe integrations fail in predictable ways. The first trap is currency units. Stripe expects amounts in the smallest currency unit. Two dollars is 200, not 2.00. If you pass a float, the compiler rejects it with cannot use 2.0 (untyped float constant) as *int64 value in struct literal. You must use stripe.Int64(200). The SDK enforces this at compile time, which saves you from silent rounding errors in production.

The second trap is ignoring API version pinning. Stripe's API evolves. New fields appear, old fields deprecate, and behavior changes. The Go SDK pins a specific API version in its module definition. When you upgrade the module, you are also upgrading the API version. Test every upgrade in your sandbox environment. Do not assume backward compatibility. Stripe documents breaking changes, but your integration tests catch the edge cases.

The third trap is treating all errors the same. Stripe returns structured errors with machine-readable codes. card_declined, insufficient_funds, rate_limit_exceeded, invalid_request_error. You should parse the error type and route it to the appropriate handler. A rate limit error needs a retry with exponential backoff. A declined card needs a user-facing message. An invalid request needs a developer alert. If you just print err.Error() and return a 500, you are hiding useful diagnostics from your users and your team.

Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path. The same principle applies to HTTP handlers. If you spawn a background goroutine to reconcile a payment, pass a context that cancels when the server shuts down. Do not leave orphaned workers polling Stripe indefinitely.

Pick the tool that matches your integration depth. Do not over-engineer a checkout button.

When to reach for the SDK

Use the official Stripe Go SDK when you need type safety, automatic retry logic, and built-in pagination helpers for listing resources. Use raw net/http when you are building a lightweight proxy that forwards requests without parsing the payload or when you need to call a Stripe endpoint that the SDK has not yet wrapped. Use a third-party payment abstraction when you need to switch between Stripe, PayPal, and Adyen behind a single interface for your application. Use plain sequential code when you do not need concurrency: the simplest thing that works is usually the right thing.

Where to go next