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.
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. |
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())
}
}
}
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.
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. |
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 for the functions that raise this error.
Distinguishing error types#
Use errors.As to branch on the concrete type:
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.
Next steps#
Distinguish a canceled or timed-out context from an API error on any network call.
Receiving and Verifying WebhooksSee where WebhookVerificationError and WebhookPayloadError originate in the receive flow.
Raw Email and Attachment DownloadsUnderstand the raw-email decode path that raises RawEmailDecodeError.
x402 ErrorsLook up X402Error status codes and retry-after handling for payment calls.
Was this page helpful?