---
title: "x402 Errors"
canonical: "https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-errors"
markdown_url: "https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-errors.md"
publisher: "Primitive SDKs"
kind: "reference"
content_type: "reference"
category: "Go SDK"
parent: "x402-payments-overview-04a296ff"
description: "A Status of 0 on *primitive.X402Error means the request may never have reached the server, so a Pay() failure at that status is an indeterminate outcome."
keywords: ["X402Error", "errors.As", "Status 0 indeterminate payment", "RetryAfter x402", "sdk-go x402 errors", "Pay() error handling"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:00.393517+00:00"
source_files:
  - "sdk-go/README.md"
  - "sdk-go/x402.go"
sections:
  - {anchor: "what-error-type-x402-methods-return", title: "What error type x402 methods return"}
  - {anchor: "x402error-fields", title: "X402Error fields"}
  - {anchor: "status-0-the-request-may-never-have-been-sent", title: "Status 0: the request may never have been sent"}
  - {anchor: "retry-after", title: "Retry-After"}
  - {anchor: "non-2xx-server-errors", title: "Non-2xx server errors"}
  - {anchor: "signing-and-validation-errors", title: "Signing and validation errors"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# x402 Errors

Reference for the Go SDK's *primitive.X402Error type: how to inspect its Status, Body, and RetryAfter fields, and how to handle a Status 0 error where the payment outcome is indeterminate.

## What error type x402 methods return

Every method on `primitive.X402Client` returns a `*primitive.X402Error` on a client-side, transport, or non-2xx server error. Use `errors.As` to pull it out of the returned `error`:

```go
import (
	"errors"
	"fmt"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

receipt, err := client.Pay(ctx, challenge, payer)
if err != nil {
	var x402Err *primitive.X402Error
	if errors.As(err, &x402Err) {
		fmt.Println(x402Err.Status, x402Err.Message)
	}
	return err
}
```

This covers `Charge`, `Pay`, `RegisterPayoutAddress`, `GetChallenge`, `ListPayoutAddresses`, `SetSpendPolicy`, `CreateEmailChallenge`, and the other `X402Client` methods described in [x402 Payments Overview](https://test.abhinandan.one/x402-payments-overview.md), [Registering a Payout Address](https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-register-payout.md), [Creating a Payment Challenge](https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-create-charge.md), and [x402 Spend Policy](https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-spend-policy.md).

## X402Error fields

| Field | Type | Description |
| --- | --- | --- |
| `Status` | `int` | The HTTP status code, or `0` for a client-side/transport error that never reached the server. |
| `Message` | `string` | Human-readable error message (from `Error()`). |
| `Body` | parsed error envelope or `nil` | The parsed error response body, when the server returned one. |
| `RetryAfter` | pointer or `nil` | The `Retry-After` response header value, when the server sent one (e.g. on a 429). |

`Error()` returns `Message`, so `*primitive.X402Error` satisfies the standard `error` interface directly.

## Status 0: the request may never have been sent

A `Status` of `0` means the error is client-side or transport-level: DNS failure, connection refused, TLS error, a client-side timeout, or any other failure before a response was received.

> **Warning:** On `Pay`, a `Status == 0` error means the request may not have been sent, so the payment outcome is **indeterminate**. Do not assume the payment failed and retry blindly, since the original request may have reached the server and be settling.

Recovery for a `Status == 0` error on `Pay`:

1. Call `client.GetChallenge(ctx, id)` to re-hydrate the challenge and check its state before retrying `Pay`.
2. If the challenge shows as already paid or settled, do not resubmit.
3. If the challenge is still open, retry `Pay` with the re-hydrated challenge. `GetChallenge` exists for exactly this: re-hydrating a challenge by id to retry `Pay`, for example after a restart.

## Retry-After

When the server returns a `429` (or any response carrying a `Retry-After` header), that value is available on `RetryAfter`:

```go
var x402Err *primitive.X402Error
if errors.As(err, &x402Err) && x402Err.RetryAfter != nil {
	time.Sleep(time.Duration(*x402Err.RetryAfter) * time.Second)
	// retry
}
```

## Non-2xx server errors

For any non-2xx response the server did return, `Body` carries the parsed error envelope (when the response body was JSON) so you can inspect the server's error code/message beyond `Message`.

```go
receipt, err := client.Pay(ctx, challenge, payer)
if err != nil {
	var x402Err *primitive.X402Error
	if errors.As(err, &x402Err) {
		switch {
		case x402Err.Status == 0:
			// transport-level; outcome indeterminate, see above
		case x402Err.Status == 429:
			// rate limited; consult x402Err.RetryAfter
		case x402Err.Status >= 400 && x402Err.Status < 500:
			// rejected by the server (e.g. payment_declined, validation error)
		case x402Err.Status >= 500:
			// server error; safe to retry
		}
	}
}
```

## Signing and validation errors

Methods that validate input locally before making a network call (for example, a malformed challenge passed to `Pay`, or an invalid signer) also return `*primitive.X402Error` with `Status: 0`, so the same `errors.As` pattern catches both "never reached the server" transport failures and "rejected before any request was made" validation failures. Check `Message` to distinguish them.
