Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /x402-payments-overview-04a296ff/go-x402-errors.md.
Verified · 8/11/2026

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:

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, Registering a Payout Address, Creating a Payment Challenge, and x402 Spend Policy.

X402Error fields#

FieldTypeDescription
StatusintThe HTTP status code, or 0 for a client-side/transport error that never reached the server.
MessagestringHuman-readable error message (from Error()).
Bodyparsed error envelope or nilThe parsed error response body, when the server returned one.
RetryAfterpointer or nilThe 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:

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.

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.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer