> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Go

> Use the Dodo Payments Go SDK to create checkout sessions, customers, subscriptions, and usage events, with context support, retries, and middleware.

The Go SDK gives Go applications typed access to the Dodo Payments REST API. Every method takes a `context.Context`, request parameters use a `Field` wrapper that separates zero values from omitted fields, and you can add middleware to every request.

## Installation

Add the module to your project:

```bash theme={null}
go get github.com/dodopayments/dodopayments-go
```

To pin a specific version:

```bash theme={null}
go get -u 'github.com/dodopayments/dodopayments-go@v1.118.0'
```

<Info>
  The SDK requires Go 1.22 or later.
</Info>

## Quick Start

Create a client, then create a checkout session:

```go expandable theme={null}
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/dodopayments/dodopayments-go"
	"github.com/dodopayments/dodopayments-go/option"
)

func main() {
	client := dodopayments.NewClient(
		option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")), // This is the default and can be omitted
		option.WithEnvironmentTestMode(),                            // defaults to option.WithEnvironmentLiveMode()
	)

	checkoutSessionResponse, err := client.CheckoutSessions.New(context.TODO(), dodopayments.CheckoutSessionNewParams{
		CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{
			ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{{
				ProductID: dodopayments.F("pdt_123"),
				Quantity:  dodopayments.F(int64(1)),
			}}),
		},
	})
	if err != nil {
		panic(err.Error())
	}
	fmt.Printf("Session ID: %s\n", checkoutSessionResponse.SessionID)
}
```

If you omit `option.WithBearerToken`, `NewClient` reads the `DODO_PAYMENTS_API_KEY` environment variable. If you omit `option.WithEnvironmentTestMode()`, the client connects to live mode. A test mode API key works only in test mode.

<Warning>
  Keep API keys in environment variables or a secrets manager. Never hardcode them in your source code.
</Warning>

## Core Features

<CardGroup cols={2}>
  <Card title="Context Support" icon="clock">
    Every method takes a `context.Context` for cancellation and timeouts.
  </Card>

  <Card title="Strong Typing" icon="shield-check">
    Typed request parameters and response structs for compile-time checks.
  </Card>

  <Card title="Middleware" icon="layer-group">
    Add middleware with `option.WithMiddleware` for logging, metrics, and custom logic.
  </Card>

  <Card title="Goroutine Safe" icon="bolt">
    Share one client across goroutines.
  </Card>
</CardGroup>

## Configuration

`NewClient` reads `DODO_PAYMENTS_API_KEY`, `DODO_PAYMENTS_WEBHOOK_KEY` (your webhook signing secret), and `DODO_PAYMENTS_BASE_URL` from the environment. Options you pass, such as `option.WithBearerToken`, `option.WithWebhookKey`, and `option.WithBaseURL`, override them.

To verify a webhook, pass the raw request body and headers to `client.Webhooks.Unwrap(rawBody, r.Header)`. It checks the signature with your webhook key and returns the parsed event. `client.Webhooks.UnsafeUnwrap(rawBody)` parses the body without verifying it, so use it only for testing. See [Webhooks](/developer-resources/webhooks).

The examples on this page use the `client` from [Quick Start](#quick-start).

### Context and Timeouts

Requests don't time out by default. A context deadline limits the whole call, including retries. To limit each attempt, add `option.WithRequestTimeout()`:

```go expandable theme={null}
import (
	"context"
	"log"
	"time"
	
	"github.com/dodopayments/dodopayments-go"
	"github.com/dodopayments/dodopayments-go/option"
)

// Create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// Note: POST /payments is deprecated — prefer checkout sessions for new
// integrations. Shown here only to illustrate context handling.
_, err := client.Payments.New(ctx, dodopayments.PaymentNewParams{
	Billing: dodopayments.F(dodopayments.BillingAddressParam{
		Country: dodopayments.F(dodopayments.CountryCodeUs),
		City:    dodopayments.F("San Francisco"),
		State:   dodopayments.F("CA"),
		Street:  dodopayments.F("1 Market St"),
		Zipcode: dodopayments.F("94105"),
	}),
	Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
		dodopayments.AttachExistingCustomerParam{
			CustomerID: dodopayments.F("cus_123"),
		},
	),
	ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{{
		ProductID: dodopayments.F("pdt_456"),
		Quantity:  dodopayments.F(int64(1)),
	}}),
}, option.WithRequestTimeout(5*time.Second)) // Per-attempt timeout
if err != nil {
	if ctx.Err() == context.DeadlineExceeded {
		log.Println("Request timed out")
	} else {
		log.Fatal(err)
	}
}
```

### Retry Configuration

The SDK retries connection errors and responses with status 408, 409, 429, or 500 and above. It retries twice by default, with exponential backoff. Set `option.WithMaxRetries` on the client or on a single request:

```go theme={null}
// Configure default for all requests (default is 2)
client := dodopayments.NewClient(
	option.WithMaxRetries(0), // disable retries
)

// Override per-request
client.CheckoutSessions.New(
	context.TODO(),
	dodopayments.CheckoutSessionNewParams{
		CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{
			ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{{
				ProductID: dodopayments.F("pdt_123"),
				Quantity:  dodopayments.F(int64(1)),
			}}),
		},
	},
	option.WithMaxRetries(5),
)
```

## Common Operations

The examples in this section also use a context, for example `ctx := context.Background()`.

### Create a Checkout Session

Create a checkout session, then redirect the customer to the returned `CheckoutURL`:

```go theme={null}
session, err := client.CheckoutSessions.New(ctx, dodopayments.CheckoutSessionNewParams{
	CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{
		ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{{
			ProductID: dodopayments.F("pdt_123"),
			Quantity:  dodopayments.F(int64(1)),
		}}),
		ReturnURL: dodopayments.F("https://yourdomain.com/return"),
	},
})
if err != nil {
	log.Fatal(err)
}

fmt.Printf("Checkout URL: %s\n", session.CheckoutURL)
```

Each checkout URL works once and expires after 24 hours. For every session option, see [Checkout Sessions](/developer-resources/checkout-session).

### Manage Customers

Create a customer with an email address and name, then retrieve it by ID. Metadata values use the union types from the `shared` package:

```go theme={null}
import "github.com/dodopayments/dodopayments-go/shared"

// Create a customer
customer, err := client.Customers.New(ctx, dodopayments.CustomerNewParams{
	Email: dodopayments.F("customer@example.com"),
	Name:  dodopayments.F("John Doe"),
	Metadata: dodopayments.F(dodopayments.MetadataParam{
		"user_id": shared.UnionString("12345"),
	}),
})
if err != nil {
	log.Fatal(err)
}

// Retrieve customer
customer, err = client.Customers.Get(ctx, "cus_123")
if err != nil {
	log.Fatal(err)
}

fmt.Printf("Customer: %s (%s)\n", customer.Name, customer.Email)
```

### Handle Subscriptions

Create a subscription, charge an on-demand subscription, and read a subscription's usage history.

<Warning>
  `POST /subscriptions` (the SDK's `Subscriptions.New` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session).
</Warning>

```go expandable theme={null}
import "time"

// Create a subscription
subscription, err := client.Subscriptions.New(ctx, dodopayments.SubscriptionNewParams{
	Billing: dodopayments.F(dodopayments.BillingAddressParam{
		Country: dodopayments.F(dodopayments.CountryCodeUs),
		City:    dodopayments.F("San Francisco"),
		State:   dodopayments.F("CA"),
		Street:  dodopayments.F("1 Market St"),
		Zipcode: dodopayments.F("94105"),
	}),
	Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
		dodopayments.AttachExistingCustomerParam{
			CustomerID: dodopayments.F("cus_123"),
		},
	),
	ProductID: dodopayments.F("pdt_456"),
	Quantity:  dodopayments.F(int64(1)),
})
if err != nil {
	log.Fatal(err)
}

// Charge an on-demand subscription
// ProductPrice is in the lowest currency denomination (e.g., 2500 = $25.00 USD)
chargeResponse, err := client.Subscriptions.Charge(ctx, subscription.SubscriptionID,
	dodopayments.SubscriptionChargeParams{
		ProductPrice: dodopayments.F(int64(2500)),
	},
)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Payment ID: %s\n", chargeResponse.PaymentID)

// Get usage history (for metered subscriptions)
usageHistory, err := client.Subscriptions.GetUsageHistory(
	ctx,
	subscription.SubscriptionID,
	dodopayments.SubscriptionGetUsageHistoryParams{
		StartDate: dodopayments.F(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),
		EndDate:   dodopayments.F(time.Date(2024, 3, 31, 23, 59, 59, 0, time.UTC)),
	},
)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Usage periods on this page: %d\n", len(usageHistory.Items))
```

<Info>
  `Billing` requires only `Country`, a two-letter ISO country code. `Customer` is a `CustomerRequestUnionParam`: pass `AttachExistingCustomerParam{CustomerID: ...}` for an existing customer or `NewCustomerParam{Email: ..., Name: ...}` to create one. `Charge` is for [on-demand subscriptions](/developer-resources/ondemand-subscriptions), and `ProductPrice` is in the smallest currency unit. `GetUsageHistory` returns one page of results; `GetUsageHistoryAutoPaging` iterates every page.
</Info>

## Usage-Based Billing

### Ingest Usage Events

Send usage events for a customer:

```go theme={null}
import (
	"time"

	"github.com/dodopayments/dodopayments-go"
)

response, err := client.UsageEvents.Ingest(ctx, dodopayments.UsageEventIngestParams{
	Events: dodopayments.F([]dodopayments.EventInputParam{{
		EventID:    dodopayments.F("api_call_12345"),
		CustomerID: dodopayments.F("cus_abc123"),
		EventName:  dodopayments.F("api_request"),
		Timestamp:  dodopayments.F(time.Now()),
	}}),
})
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Ingested %d events\n", response.IngestedCount)
```

<Info>
  The `EventID` is the idempotency key, so give each event a unique value. If the same `EventID` appears twice in one request, the whole request is rejected. If an `EventID` was already ingested, the new event is ignored. A request accepts up to 1,000 events. `Timestamp` defaults to the current time and is rejected if it's more than 1 hour in the past or more than 5 minutes in the future.
</Info>

### List Usage Events

List events filtered by customer and event name:

```go theme={null}
// List events with filters
params := dodopayments.UsageEventListParams{
	CustomerID: dodopayments.F("cus_abc123"),
	EventName:  dodopayments.F("api_request"),
}

events, err := client.UsageEvents.List(ctx, params)
if err != nil {
	log.Fatal(err)
}

for _, event := range events.Items {
	fmt.Printf("Event %s: %s at %s\n", event.EventID, event.EventName, event.Timestamp)
}
```

`List` returns one page. To iterate every page, call `client.UsageEvents.ListAutoPaging(ctx, params)` and loop with `iter.Next()`, `iter.Current()`, and `iter.Err()`. Other list methods have the same `AutoPaging` variant, and each page has a `GetNextPage()` method.

## Error Handling

When the API returns a non-success status code, the SDK returns an error of type `*dodopayments.Error`. It has the `StatusCode`, the `*http.Request` and `*http.Response`, and the JSON of the error body. Use `errors.As` to inspect it, and branch on `StatusCode` to handle specific cases:

```go expandable theme={null}
// Note: POST /payments is deprecated — prefer checkout sessions for new
// integrations. Shown here only to illustrate error handling.
_, err := client.Payments.New(ctx, dodopayments.PaymentNewParams{
	Billing: dodopayments.F(dodopayments.BillingAddressParam{
		Country: dodopayments.F(dodopayments.CountryCodeUs),
		City:    dodopayments.F("San Francisco"),
		State:   dodopayments.F("CA"),
		Street:  dodopayments.F("1 Market St"),
		Zipcode: dodopayments.F("94105"),
	}),
	Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
		dodopayments.AttachExistingCustomerParam{CustomerID: dodopayments.F("cus_123")},
	),
	ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{{
		ProductID: dodopayments.F("pdt_456"),
		Quantity:  dodopayments.F(int64(1)),
	}}),
})
if err != nil {
	var apiErr *dodopayments.Error
	if errors.As(err, &apiErr) {
		fmt.Printf("Status Code: %d\n", apiErr.StatusCode)
		fmt.Println(string(apiErr.DumpResponse(true))) // Serialized HTTP response

		// Handle specific status codes
		switch apiErr.StatusCode {
		case 401:
			log.Println("Authentication failed")
		case 422:
			log.Println("Invalid request parameters")
		case 429:
			log.Println("Rate limit exceeded")
		default:
			log.Printf("API error: %s", apiErr.Error())
		}
	} else {
		log.Fatal(err)
	}
}
```

Other errors are returned unwrapped. For example, if the HTTP transport fails, you might receive a `*url.Error` that wraps a `*net.OpError`. `apiErr.DumpRequest(true)` returns the serialized request.

## Middleware

Add middleware with `option.WithMiddleware`. A middleware receives each request and a `next` function that sends it:

```go theme={null}
func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
	// Before the request
	start := time.Now()
	log.Printf("Request: %s %s\n", req.Method, req.URL)

	// Forward the request to the next handler
	res, err = next(req)

	// After the request (res is nil when err is not nil)
	if err != nil {
		log.Printf("Request failed after %v: %v\n", time.Since(start), err)
		return res, err
	}
	log.Printf("Response: %d in %v\n", res.StatusCode, time.Since(start))

	return res, err
}

client := dodopayments.NewClient(
	option.WithMiddleware(Logger),
)
```

Multiple middleware in one `option.WithMiddleware` call run left to right. Middleware passed to `NewClient` runs before middleware passed to a single request.

## Concurrency

The client is safe for concurrent use, so you can share one client across goroutines:

```go theme={null}
package main

import (
	"context"
	"sync"
	"log"

	"github.com/dodopayments/dodopayments-go"
)

func main() {
	client := dodopayments.NewClient()
	
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			
			// Note: POST /payments is deprecated — prefer checkout sessions
			// for new integrations. Shown here only to illustrate concurrency.
			payment, err := client.Payments.New(context.Background(), dodopayments.PaymentNewParams{
				Billing: dodopayments.F(dodopayments.BillingAddressParam{
					Country: dodopayments.F(dodopayments.CountryCodeUs),
					City:    dodopayments.F("San Francisco"),
					State:   dodopayments.F("CA"),
					Street:  dodopayments.F("1 Market St"),
					Zipcode: dodopayments.F("94105"),
				}),
				Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
					dodopayments.AttachExistingCustomerParam{CustomerID: dodopayments.F("cus_123")},
				),
				ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{{
					ProductID: dodopayments.F("pdt_456"),
					Quantity:  dodopayments.F(int64(1)),
				}}),
			})
			if err != nil {
				log.Printf("Failed to create payment %d: %v", idx, err)
				return
			}
			
			log.Printf("Created payment %d: %s", idx, payment.PaymentID)
		}(i)
	}
	
	wg.Wait()
}
```

## Resources

<CardGroup cols={2}>
  <Card title="GitHub Repository" icon="github" href="https://github.com/dodopayments/dodopayments-go">
    Source code, releases, and the full method list.
  </Card>

  <Card title="API Reference" icon="book" href="/api-reference/introduction">
    Every endpoint, parameter, and response.
  </Card>

  <Card title="Discord Community" icon="discord" href="https://discord.gg/bYqAp4ayYh">
    Ask questions and talk with other developers.
  </Card>

  <Card title="Report Issues" icon="bug" href="https://github.com/dodopayments/dodopayments-go/issues">
    Report bugs or request features.
  </Card>
</CardGroup>

## Support

For help with the Go SDK:

* **Discord**: Join the [community server](https://discord.gg/bYqAp4ayYh) for real-time help.
* **Email**: Contact [support@dodopayments.com](mailto:support@dodopayments.com).
* **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-go).

## Contributing

To contribute, read the [contributing guidelines](https://github.com/dodopayments/dodopayments-go/blob/main/CONTRIBUTING.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.