{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/go-error-handling","markdown_url":"https://test.abhinandan.one/go-error-handling.md","article":{"id":"eddfedef-c033-4147-be60-ea7837d2833d","article_slug":"go-error-handling","parent_article_slug":null,"parent_article_title":null,"kind":"reference","published_at":"2026-08-11T18:55:01.750303+00:00","keywords":["APIError Go SDK","PrimitiveWebhookError","WebhookVerificationError","WebhookPayloadError","WebhookValidationError","RawEmailDecodeError"],"meta_description":"Go SDK errors carry a StatusCode, Code, RequestID, and RetryAfter on APIError, plus five distinct webhook error types for signature and payload failures.","og_image_url":null,"source_file_paths":["sdk-go/client.go"],"recording_id":null,"replayable":false,"task_name":"Error Handling","category":"Go SDK","summary":null,"description":"Reference for every error type the Go SDK raises, APIError, PrimitiveWebhookError, WebhookVerificationError, WebhookPayloadError, WebhookValidationError, and RawEmailDecodeError, and how to inspect each one.","content_kind":"repo_page","content_markdown":"## APIError\n\n`*primitive.APIError` is returned by `Client.Send`, `Client.Reply`, `Client.Forward`, and `Client.SemanticSearch` on any non-2xx response from the Primitive API.\n\n```go\ntype APIError struct {\n\tStatusCode int\n\tCode       string\n\tMessage    string\n\tRetryAfter *int\n\tGates      []primitiveapi.GateDenial\n\tRequestID  string\n\tDetails    *primitiveapi.ErrorResponseErrorDetails\n\tPayload    any\n}\n\nfunc (e *APIError) Error() string\n```\n\n| Field | Type | Description |\n|---|---|---|\n| `StatusCode` | `int` | The HTTP status code (`400`, `401`, `403`, `404`, `422`, `429`, `500`, `502`, `503`). |\n| `Code` | `string` | The server's machine-readable error code (e.g. `validation_error`, `recipient_not_allowed`, `inbound_not_repliable`). |\n| `Message` | `string` | Human-readable error message. Returned verbatim by `Error()`. |\n| `RetryAfter` | `*int` | Seconds to wait before retrying, populated only on `429` responses that carry a `Retry-After` header. `nil` otherwise. |\n| `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. |\n| `RequestID` | `string` | The server's request id, when present in the error envelope. Useful when filing a support request. |\n| `Details` | `*primitiveapi.ErrorResponseErrorDetails` | Additional structured error detail, when the server includes it. |\n| `Payload` | `any` | The raw decoded error response, for cases not covered by the typed fields above. |\n\n```go\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"time\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nresult, err := client.Send(ctx, primitive.SendParams{\n\tFrom:    \"Support <support@example.com>\",\n\tTo:      \"alice@example.com\",\n\tSubject: \"Hello\",\n\tBodyText: \"Hi there\",\n})\nif err != nil {\n\tvar apiErr *primitive.APIError\n\tif errors.As(err, &apiErr) {\n\t\tswitch apiErr.StatusCode {\n\t\tcase 429:\n\t\t\tif apiErr.RetryAfter != nil {\n\t\t\t\ttime.Sleep(time.Duration(*apiErr.RetryAfter) * time.Second)\n\t\t\t}\n\t\tcase 422:\n\t\t\tlog.Printf(\"send rejected: %s (%s)\", apiErr.Message, apiErr.Code)\n\t\tdefault:\n\t\t\tlog.Printf(\"send failed: %s\", apiErr.Error())\n\t\t}\n\t}\n}\n```\n\n<Note>\n\n`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.\n\n</Note>\n\nA 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](go-context-timeouts).\n\n## Webhook errors\n\nFive error types cover webhook verification, parsing, and validation failures, each carrying a stable `Code` you can branch on.\n\n### PrimitiveWebhookError\n\nThe shared webhook error type the SDK exposes alongside the specific verification, payload, validation, and raw-email variants.\n\n### WebhookVerificationError\n\nRaised when signature verification fails: bad HMAC, malformed header, expired timestamp, or a missing secret.\n\n| Code | Cause |\n|---|---|\n| `MISSING_SECRET` | The webhook secret argument was empty or not provided. |\n| `INVALID_SIGNATURE_HEADER` | The `Primitive-Signature` (or Standard Webhooks) header is missing or malformed. |\n| `TIMESTAMP_OUT_OF_RANGE` | The signed timestamp is older than the tolerance window (default 300s) or too far in the future. |\n| `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. |\n\n```go\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nevent, err := primitive.HandleWebhookEvent(primitive.HandleWebhookOptions{\n\tBody:    rawBody,\n\tHeaders: req.Header,\n\tSecret:  os.Getenv(\"PRIMITIVE_WEBHOOK_SECRET\"),\n})\nif err != nil {\n\tvar verifyErr *primitive.WebhookVerificationError\n\tif errors.As(err, &verifyErr) {\n\t\tlog.Printf(\"signature verification failed: %s\", verifyErr.Error())\n\t\t// respond 400, do not process the payload\n\t}\n}\n```\n\n### WebhookPayloadError\n\nRaised when the raw request body cannot be turned into a JSON object at all, before any schema validation runs.\n\n| Code | Cause |\n|---|---|\n| `PAYLOAD_NULL` | The body decoded to `null`. |\n| `PAYLOAD_IS_ARRAY` | The body is a JSON array instead of an object. |\n| `PAYLOAD_EMPTY_BODY` | The request body was empty. |\n| `JSON_PARSE_FAILED` | The body is not valid JSON. |\n| `PAYLOAD_MISSING_EVENT` | No `X-Webhook-Event` header and no `event` field in the body, so the event family cannot be classified. |\n| `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`). |\n| `PAYLOAD_UNKNOWN_EVENT` | `HandleWebhook` received a known, verified event that is not `email.received` (it is hard-typed to that event only). |\n\n### WebhookValidationError\n\nRaised 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.\n\n### RawEmailDecodeError\n\nRaised by `DecodeRawEmail` and `VerifyRawEmailDownload` when the raw MIME bytes cannot be decoded or verified.\n\n| Code | Cause |\n|---|---|\n| `NOT_INCLUDED` | The raw email was not included inline; the caller must download it from `email.content.download.url` instead. |\n| `INVALID_BASE64` | The inline `email.content.raw.data` field is not valid base64. |\n| `HASH_MISMATCH` | The decoded (or downloaded) bytes' SHA-256 does not match `email.content.raw.sha256`. Indicates corrupted or tampered content. |\n\nSee [Raw Email and Attachment Downloads](go-raw-email-downloads) for the functions that raise this error.\n\n## Distinguishing error types\n\nUse `errors.As` to branch on the concrete type:\n\n```go\nimport (\n\t\"context\"\n\t\"errors\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nvar (\n\tapiErr        *primitive.APIError\n\tverifyErr     *primitive.WebhookVerificationError\n\tpayloadErr    *primitive.WebhookPayloadError\n\tvalidationErr *primitive.WebhookValidationError\n\trawErr        *primitive.RawEmailDecodeError\n)\n\nswitch {\ncase errors.As(err, &apiErr):\n\t// *primitive.APIError — non-2xx API response\ncase errors.As(err, &verifyErr):\n\t// *primitive.WebhookVerificationError — bad signature/timestamp\ncase errors.As(err, &payloadErr):\n\t// *primitive.WebhookPayloadError — malformed body\ncase errors.As(err, &validationErr):\n\t// *primitive.WebhookValidationError — schema mismatch\ncase errors.As(err, &rawErr):\n\t// *primitive.RawEmailDecodeError — bad raw-email bytes\ncase errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):\n\t// client-side abort, not a server response\n}\n```\n\nx402 payment errors (`*primitive.X402Error`) are a separate type with its own status/retry-after semantics; see [x402 Errors](go-x402-errors).\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Context, Timeouts, and Cancellation\" href=\"go-context-timeouts\">\n\nDistinguish a canceled or timed-out context from an API error on any network call.\n\n</Card>\n\n<Card title=\"Receiving and Verifying Webhooks\" href=\"go-receiving-webhooks\">\n\nSee where WebhookVerificationError and WebhookPayloadError originate in the receive flow.\n\n</Card>\n\n<Card title=\"Raw Email and Attachment Downloads\" href=\"go-raw-email-downloads\">\n\nUnderstand the raw-email decode path that raises RawEmailDecodeError.\n\n</Card>\n\n<Card title=\"x402 Errors\" href=\"go-x402-errors\">\n\nLook up X402Error status codes and retry-after handling for payment calls.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-go/client.go","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Error+Handling&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fgo-error-handling","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}