{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/go-receiving-webhooks","markdown_url":"https://test.abhinandan.one/go-receiving-webhooks.md","article":{"id":"867e04f8-8c5e-4497-9a8b-d8e7afd4caac","article_slug":"go-receiving-webhooks","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:54:54.385774+00:00","keywords":["primitive.Receive","ReceiveFromHTTPRequest","HandleWebhook","VerifyWebhookSignature","Primitive-Signature","primitive.HandleWebhookOptions"],"meta_description":"primitive.Receive verifies the Primitive-Signature HMAC header and returns a normalized ReceivedEmail from a raw Go webhook request body.","og_image_url":null,"source_file_paths":["sdk-go/webhook.go"],"recording_id":null,"replayable":false,"task_name":"Receiving and Verifying Webhooks","category":"Go SDK","summary":null,"description":"Verify the Primitive-Signature HMAC header and normalize an inbound webhook body into a ReceivedEmail with primitive.Receive, so your Go handler can trust and act on inbound mail in one call.","content_kind":"repo_page","content_markdown":"Use `primitive.Receive` to turn a raw inbound webhook delivery into a verified, normalized `ReceivedEmail` in one call. Reach for it in the HTTP handler that receives Primitive's inbound-mail webhook, before you do anything else with the payload.\n\n## What `Receive` does\n\n`primitive.Receive` verifies the `Primitive-Signature` HMAC header against your webhook secret, parses the JSON body, validates it against the `email.received` schema, and returns a normalized [ReceivedEmail](email-model) object, all in one call.\n\n```go\npackage main\n\nimport (\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\n\tprimitive \"github.com/primitivedotdev/sdks/sdk-go\"\n)\n\nfunc handleInbound(w http.ResponseWriter, r *http.Request) {\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\thttp.Error(w, \"failed to read body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\theaders := map[string]string{}\n\tfor name := range r.Header {\n\t\theaders[name] = r.Header.Get(name)\n\t}\n\n\temail, err := primitive.Receive(primitive.HandleWebhookOptions{\n\t\tBody:    body,\n\t\tHeaders: headers,\n\t\tSecret:  os.Getenv(\"PRIMITIVE_WEBHOOK_SECRET\"),\n\t})\n\tif err != nil {\n\t\tlog.Printf(\"invalid webhook: %v\", err)\n\t\thttp.Error(w, \"invalid webhook\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tlog.Printf(\"received email from %s: %s\", email.Sender.Address, email.Subject)\n\tw.WriteHeader(http.StatusOK)\n}\n```\n\nOn success you get a `*ReceivedEmail` with fields like `email.Sender.Address`, `email.ReplyTarget.Address`, `email.Subject`, `email.Text`, `email.Thread.MessageID`, and `email.Raw` (the full validated `email.received` event). The full field list and the receive/send/reply/forward model are documented once on [Inbound and Outbound Email Model](email-model); this page covers verification and normalization mechanics only.\n\n<Tip>\n\n`Receive` takes a raw body plus a header map, so it works the same in `net/http`, Gin, Echo, or any other framework. Extract the exact request bytes and the headers, and the rest of the flow is identical.\n\n</Tip>\n\n## Verify and normalize a webhook\n\n<Steps>\n\n<Step title=\"Read the raw request body\">\n\n`Receive` verifies the HMAC signature over the *exact* bytes Primitive sent, so read the body without re-encoding or re-serializing it:\n\n```go\nbody, err := io.ReadAll(r.Body)\nif err != nil {\n\thttp.Error(w, \"failed to read body\", http.StatusBadRequest)\n\treturn\n}\n```\n\nDo not `json.Unmarshal` and re-`json.Marshal` the body before passing it to `Receive`. Re-serialized JSON can differ in whitespace or key order from the bytes that were signed, which makes verification fail even for a genuine delivery.\n\n</Step>\n\n<Step title=\"Collect the request headers\">\n\nBuild a `map[string]string` from the incoming headers. `Receive` looks for the `Primitive-Signature` header (falling back to the legacy `MyMX-Signature` header) case-insensitively:\n\n```go\nheaders := map[string]string{}\nfor name := range r.Header {\n\theaders[name] = r.Header.Get(name)\n}\n```\n\n</Step>\n\n<Step title=\"Call primitive.Receive with your webhook secret\">\n\nPass the body, headers, and your account's webhook secret. Get the secret from your Primitive dashboard and keep it out of source control, read it from an environment variable such as `PRIMITIVE_WEBHOOK_SECRET`:\n\n```go\nemail, err := primitive.Receive(primitive.HandleWebhookOptions{\n\tBody:    body,\n\tHeaders: headers,\n\tSecret:  os.Getenv(\"PRIMITIVE_WEBHOOK_SECRET\"),\n})\n```\n\n</Step>\n\n<Step title=\"Handle the verification error\">\n\n`Receive` returns a non-nil `error` when the signature is missing, malformed, expired, or does not match, or when the body fails JSON parsing or schema validation. Treat any error as \"reject this delivery\":\n\n```go\nif err != nil {\n\tlog.Printf(\"invalid webhook: %v\", err)\n\thttp.Error(w, \"invalid webhook\", http.StatusBadRequest)\n\treturn\n}\n```\n\nRespond with a non-2xx status on failure so Primitive's webhook retry logic doesn't mistake a rejected delivery for a successfully processed one.\n\n</Step>\n\n<Step title=\"Use the normalized ReceivedEmail\">\n\nOn success, `email` is a `*primitive.ReceivedEmail` ready to pass straight into `client.Reply(ctx, email, ...)` or `client.Forward(ctx, email, ...)`:\n\n```go\nctx := r.Context()\n\nclient, err := primitive.NewClient(os.Getenv(\"PRIMITIVE_API_KEY\"))\nif err != nil {\n\tlog.Fatal(err)\n}\n\n_, err = client.Reply(ctx, email, primitive.ReplyParams{\n\tBodyText: \"Thank you for your email.\",\n})\n```\n\nSending, replying, and forwarding are covered in full on [Sending Emails](go-sending-emails) and [Replying to Emails](go-replying-to-emails).\n\n</Step>\n\n</Steps>\n\n## What counts as a valid signature\n\nPrimitive signs every webhook delivery with HMAC-SHA256 over `${timestamp}.${rawBody}`, sent as:\n\n```text\nPrimitive-Signature: t=<unix-seconds>,v1=<hex>\n```\n\n`Receive` and the lower-level `VerifyWebhookSignature` reject a delivery when:\n\n- the header is missing or doesn't match the `t=...,v1=...` format\n- the timestamp is more than 5 minutes old (replay protection) or more than 60 seconds in the future (clock-skew guard)\n- the computed HMAC doesn't match any signature in the header\n\nA legacy `MyMX-Signature` header carries the same value for backward compatibility, so both header names verify identically. The full webhook signature contract, including the wire format, the `X-Webhook-Event` header, and forward-compatibility guarantees across all three SDKs, is documented once on [Webhook Events Overview](webhook-events).\n\n<Warning>\n\nNever base64-decode the webhook secret before using it as the HMAC key. The secret returned by the API looks base64-shaped but must be used as a raw UTF-8 string. Decoding it first produces a signature that never matches.\n\n</Warning>\n\n## Lower-level building blocks\n\n`Receive` and `ReceiveFromHTTPRequest` compose three primitives that remain available individually for advanced use:\n\n| Function | Purpose |\n| --- | --- |\n| `primitive.VerifyWebhookSignature(options)` | Verify the `Primitive-Signature` header alone, without parsing the body. |\n| `primitive.ParseJSONBody(rawBody)` | Parse the raw body into a generic JSON value. |\n| `primitive.HandleWebhook(options)` | Verify + parse + validate, returning an `*EmailReceivedEvent` (not yet normalized to `ReceivedEmail`). |\n\nReach for these only when you need to verify and normalize as separate steps, for example when working with the raw `EmailReceivedEvent` shape instead of the normalized `ReceivedEmail`. For handling `payment.*` and `interaction.x402.*` events on the same endpoint, use `primitive.HandleWebhookEvent` instead, covered on [Webhook Event Types](go-webhook-event-types).\n\n## Common failure: signature mismatch after body re-encoding\n\nIf verification fails for deliveries you're confident are genuine, check whether your framework has already parsed and re-serialized the JSON body before your handler sees it (common with middleware that logs or transforms request bodies). `Receive` needs the exact bytes Primitive sent; any re-encoding, even one that looks identical, changes the signed string and breaks verification.\n\nThe SDK detects one common shape of this problem: when a signature mismatch is accompanied by a pretty-printed body, the returned error adds a hint that the body looks re-serialized. Take that hint literally and pass the untouched bytes.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Webhook Event Types\" href=\"go-webhook-event-types\">\n\nBranch on email.*, payment.*, and interaction.x402.* events from the same webhook endpoint.\n\n</Card>\n\n<Card title=\"Validating Email Authenticity\" href=\"go-validating-email-authenticity\">\n\nDecide whether an inbound email's SPF/DKIM/DMARC results can be trusted before acting on it.\n\n</Card>\n\n<Card title=\"Replying to Emails\" href=\"go-replying-to-emails\">\n\nPass the ReceivedEmail from Receive straight into Client.Reply.\n\n</Card>\n\n<Card title=\"Standard Webhooks Signature Support (Go)\" href=\"go-standard-webhooks\">\n\nVerify deliveries using the Standard Webhooks header format instead of Primitive-Signature.\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/webhook.go","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Receiving+and+Verifying+Webhooks&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fgo-receiving-webhooks","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}