---
title: "Error Handling"
canonical: "https://test.abhinandan.one/go-error-handling"
markdown_url: "https://test.abhinandan.one/go-error-handling.md"
publisher: "Primitive SDKs"
kind: "reference"
content_type: "reference"
category: "Go SDK"
description: "Go SDK errors carry a StatusCode, Code, RequestID, and RetryAfter on APIError, plus five distinct webhook error types for signature and payload failures."
keywords: ["APIError Go SDK", "PrimitiveWebhookError", "WebhookVerificationError", "WebhookPayloadError", "WebhookValidationError", "RawEmailDecodeError"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:01.750303+00:00"
source_files:
  - "sdk-go/client.go"
sections:
  - {anchor: "apierror", title: "APIError"}
  - {anchor: "webhook-errors", title: "Webhook errors"}
  - {anchor: "primitivewebhookerror", title: "PrimitiveWebhookError"}
  - {anchor: "webhookverificationerror", title: "WebhookVerificationError"}
  - {anchor: "webhookpayloaderror", title: "WebhookPayloadError"}
  - {anchor: "webhookvalidationerror", title: "WebhookValidationError"}
  - {anchor: "rawemaildecodeerror", title: "RawEmailDecodeError"}
  - {anchor: "distinguishing-error-types", title: "Distinguishing error types"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Error Handling

Reference for every error type the Go SDK raises, APIError, PrimitiveWebhookError, WebhookVerificationError, WebhookPayloadError, WebhookValidationError, and RawEmailDecodeError, and how to inspect each one.

## APIError

`*primitive.APIError` is returned by `Client.Send`, `Client.Reply`, `Client.Forward`, and `Client.SemanticSearch` on any non-2xx response from the Primitive API.

```go
type APIError struct {
	StatusCode int
	Code       string
	Message    string
	RetryAfter *int
	Gates      []primitiveapi.GateDenial
	RequestID  string
	Details    *primitiveapi.ErrorResponseErrorDetails
	Payload    any
}

func (e *APIError) Error() string
```

| Field | Type | Description |
|---|---|---|
| `StatusCode` | `int` | The HTTP status code (`400`, `401`, `403`, `404`, `422`, `429`, `500`, `502`, `503`). |
| `Code` | `string` | The server's machine-readable error code (e.g. `validation_error`, `recipient_not_allowed`, `inbound_not_repliable`). |
| `Message` | `string` | Human-readable error message. Returned verbatim by `Error()`. |
| `RetryAfter` | `*int` | Seconds to wait before retrying, populated only on `429` responses that carry a `Retry-After` header. `nil` otherwise. |
| `Gates` | `[]primitiveapi.GateDenial` | Present when a send/reply was denied by a policy gate (e.g. sending to an unconfirmed recipient). Empty when not applicable. |
| `RequestID` | `string` | The server's request id, when present in the error envelope. Useful when filing a support request. |
| `Details` | `*primitiveapi.ErrorResponseErrorDetails` | Additional structured error detail, when the server includes it. |
| `Payload` | `any` | The raw decoded error response, for cases not covered by the typed fields above. |

```go
import (
	"errors"
	"log"
	"time"

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

result, err := client.Send(ctx, primitive.SendParams{
	From:    "Support <support@example.com>",
	To:      "alice@example.com",
	Subject: "Hello",
	BodyText: "Hi there",
})
if err != nil {
	var apiErr *primitive.APIError
	if errors.As(err, &apiErr) {
		switch apiErr.StatusCode {
		case 429:
			if apiErr.RetryAfter != nil {
				time.Sleep(time.Duration(*apiErr.RetryAfter) * time.Second)
			}
		case 422:
			log.Printf("send rejected: %s (%s)", apiErr.Message, apiErr.Code)
		default:
			log.Printf("send failed: %s", apiErr.Error())
		}
	}
}
```

> **Note:** `Client.Reply` returns `inbound_not_repliable` (`Code`, HTTP 422) when the inbound row is not in a state that can be replied to: it was rejected at ingestion, its content was discarded, or it has no recipient recorded. A missing `Message-Id` does not trigger this error; it only omits the threading headers.

A canceled or timed-out `context.Context` surfaces as `context.Canceled` or `context.DeadlineExceeded`, not as `*primitive.APIError`. Check for these separately to distinguish a client-side abort from a server response, see [Context, Timeouts, and Cancellation](https://test.abhinandan.one/go-context-timeouts.md).

## Webhook errors

Five error types cover webhook verification, parsing, and validation failures, each carrying a stable `Code` you can branch on.

### PrimitiveWebhookError

The shared webhook error type the SDK exposes alongside the specific verification, payload, validation, and raw-email variants.

### WebhookVerificationError

Raised when signature verification fails: bad HMAC, malformed header, expired timestamp, or a missing secret.

| Code | Cause |
|---|---|
| `MISSING_SECRET` | The webhook secret argument was empty or not provided. |
| `INVALID_SIGNATURE_HEADER` | The `Primitive-Signature` (or Standard Webhooks) header is missing or malformed. |
| `TIMESTAMP_OUT_OF_RANGE` | The signed timestamp is older than the tolerance window (default 300s) or too far in the future. |
| `SIGNATURE_MISMATCH` | The computed HMAC does not match any signature in the header. Usually caused by verifying a re-serialized body instead of the raw request bytes. |

```go
import (
	"errors"
	"log"
	"os"

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

event, err := primitive.HandleWebhookEvent(primitive.HandleWebhookOptions{
	Body:    rawBody,
	Headers: req.Header,
	Secret:  os.Getenv("PRIMITIVE_WEBHOOK_SECRET"),
})
if err != nil {
	var verifyErr *primitive.WebhookVerificationError
	if errors.As(err, &verifyErr) {
		log.Printf("signature verification failed: %s", verifyErr.Error())
		// respond 400, do not process the payload
	}
}
```

### WebhookPayloadError

Raised when the raw request body cannot be turned into a JSON object at all, before any schema validation runs.

| Code | Cause |
|---|---|
| `PAYLOAD_NULL` | The body decoded to `null`. |
| `PAYLOAD_IS_ARRAY` | The body is a JSON array instead of an object. |
| `PAYLOAD_EMPTY_BODY` | The request body was empty. |
| `JSON_PARSE_FAILED` | The body is not valid JSON. |
| `PAYLOAD_MISSING_EVENT` | No `X-Webhook-Event` header and no `event` field in the body, so the event family cannot be classified. |
| `PAYLOAD_WRONG_TYPE` | A required field is missing or has the wrong type (used by the raw-email and download helpers, e.g. `email.content.download.expires_at`). |
| `PAYLOAD_UNKNOWN_EVENT` | `HandleWebhook` received a known, verified event that is not `email.received` (it is hard-typed to that event only). |

### WebhookValidationError

Raised when a payload parses as JSON but fails schema validation against the canonical `email.received` shape, or when `ValidateEmailAuth` is given a malformed auth object.

### RawEmailDecodeError

Raised by `DecodeRawEmail` and `VerifyRawEmailDownload` when the raw MIME bytes cannot be decoded or verified.

| Code | Cause |
|---|---|
| `NOT_INCLUDED` | The raw email was not included inline; the caller must download it from `email.content.download.url` instead. |
| `INVALID_BASE64` | The inline `email.content.raw.data` field is not valid base64. |
| `HASH_MISMATCH` | The decoded (or downloaded) bytes' SHA-256 does not match `email.content.raw.sha256`. Indicates corrupted or tampered content. |

See [Raw Email and Attachment Downloads](https://test.abhinandan.one/go-raw-email-downloads.md) for the functions that raise this error.

## Distinguishing error types

Use `errors.As` to branch on the concrete type:

```go
import (
	"context"
	"errors"

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

var (
	apiErr        *primitive.APIError
	verifyErr     *primitive.WebhookVerificationError
	payloadErr    *primitive.WebhookPayloadError
	validationErr *primitive.WebhookValidationError
	rawErr        *primitive.RawEmailDecodeError
)

switch {
case errors.As(err, &apiErr):
	// *primitive.APIError — non-2xx API response
case errors.As(err, &verifyErr):
	// *primitive.WebhookVerificationError — bad signature/timestamp
case errors.As(err, &payloadErr):
	// *primitive.WebhookPayloadError — malformed body
case errors.As(err, &validationErr):
	// *primitive.WebhookValidationError — schema mismatch
case errors.As(err, &rawErr):
	// *primitive.RawEmailDecodeError — bad raw-email bytes
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
	// client-side abort, not a server response
}
```

x402 payment errors (`*primitive.X402Error`) are a separate type with its own status/retry-after semantics; see [x402 Errors](https://test.abhinandan.one/x402-payments-overview-04a296ff/go-x402-errors.md).
