---
title: "Receiving and Verifying Webhooks"
canonical: "https://test.abhinandan.one/go-receiving-webhooks"
markdown_url: "https://test.abhinandan.one/go-receiving-webhooks.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Go SDK"
description: "primitive.Receive verifies the Primitive-Signature HMAC header and returns a normalized ReceivedEmail from a raw Go webhook request body."
keywords: ["primitive.Receive", "ReceiveFromHTTPRequest", "HandleWebhook", "VerifyWebhookSignature", "Primitive-Signature", "primitive.HandleWebhookOptions"]
last_modified: "2026-08-11T18:54:54.556265+00:00"
published_at: "2026-08-11T18:54:54.385774+00:00"
source_files:
  - "sdk-go/webhook.go"
sections:
  - {anchor: "what-receive-does", title: "What `Receive` does"}
  - {anchor: "verify-and-normalize-a-webhook", title: "Verify and normalize a webhook"}
  - {anchor: "step-read-the-raw-request-body", title: "Read the raw request body"}
  - {anchor: "step-collect-the-request-headers", title: "Collect the request headers"}
  - {anchor: "step-call-primitivereceive-with-your-webhook-secret", title: "Call primitive.Receive with your webhook secret"}
  - {anchor: "step-handle-the-verification-error", title: "Handle the verification error"}
  - {anchor: "step-use-the-normalized-receivedemail", title: "Use the normalized ReceivedEmail"}
  - {anchor: "what-counts-as-a-valid-signature", title: "What counts as a valid signature"}
  - {anchor: "lower-level-building-blocks", title: "Lower-level building blocks"}
  - {anchor: "common-failure-signature-mismatch-after-body-re-encoding", title: "Common failure: signature mismatch after body re-encoding"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Receiving and Verifying Webhooks

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.

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.

## What `Receive` does

`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](https://test.abhinandan.one/email-model.md) object, all in one call.

```go
package main

import (
	"io"
	"log"
	"net/http"
	"os"

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

func handleInbound(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "failed to read body", http.StatusBadRequest)
		return
	}

	headers := map[string]string{}
	for name := range r.Header {
		headers[name] = r.Header.Get(name)
	}

	email, err := primitive.Receive(primitive.HandleWebhookOptions{
		Body:    body,
		Headers: headers,
		Secret:  os.Getenv("PRIMITIVE_WEBHOOK_SECRET"),
	})
	if err != nil {
		log.Printf("invalid webhook: %v", err)
		http.Error(w, "invalid webhook", http.StatusBadRequest)
		return
	}

	log.Printf("received email from %s: %s", email.Sender.Address, email.Subject)
	w.WriteHeader(http.StatusOK)
}
```

On 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](https://test.abhinandan.one/email-model.md); this page covers verification and normalization mechanics only.

> **Tip:** `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.

## Verify and normalize a webhook

### 1. Read the raw request body

`Receive` verifies the HMAC signature over the *exact* bytes Primitive sent, so read the body without re-encoding or re-serializing it:

```go
body, err := io.ReadAll(r.Body)
if err != nil {
	http.Error(w, "failed to read body", http.StatusBadRequest)
	return
}
```

Do 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.

### 2. Collect the request headers

Build 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:

```go
headers := map[string]string{}
for name := range r.Header {
	headers[name] = r.Header.Get(name)
}
```

### 3. Call primitive.Receive with your webhook secret

Pass 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`:

```go
email, err := primitive.Receive(primitive.HandleWebhookOptions{
	Body:    body,
	Headers: headers,
	Secret:  os.Getenv("PRIMITIVE_WEBHOOK_SECRET"),
})
```

### 4. Handle the verification error

`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":

```go
if err != nil {
	log.Printf("invalid webhook: %v", err)
	http.Error(w, "invalid webhook", http.StatusBadRequest)
	return
}
```

Respond with a non-2xx status on failure so Primitive's webhook retry logic doesn't mistake a rejected delivery for a successfully processed one.

### 5. Use the normalized ReceivedEmail

On success, `email` is a `*primitive.ReceivedEmail` ready to pass straight into `client.Reply(ctx, email, ...)` or `client.Forward(ctx, email, ...)`:

```go
ctx := r.Context()

client, err := primitive.NewClient(os.Getenv("PRIMITIVE_API_KEY"))
if err != nil {
	log.Fatal(err)
}

_, err = client.Reply(ctx, email, primitive.ReplyParams{
	BodyText: "Thank you for your email.",
})
```

Sending, replying, and forwarding are covered in full on [Sending Emails](https://test.abhinandan.one/go-sending-emails.md) and [Replying to Emails](https://test.abhinandan.one/go-replying-to-emails.md).

## What counts as a valid signature

Primitive signs every webhook delivery with HMAC-SHA256 over `${timestamp}.${rawBody}`, sent as:

```text
Primitive-Signature: t=<unix-seconds>,v1=<hex>
```

`Receive` and the lower-level `VerifyWebhookSignature` reject a delivery when:

- the header is missing or doesn't match the `t=...,v1=...` format
- the timestamp is more than 5 minutes old (replay protection) or more than 60 seconds in the future (clock-skew guard)
- the computed HMAC doesn't match any signature in the header

A 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](https://test.abhinandan.one/webhook-events.md).

> **Warning:** Never 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.

## Lower-level building blocks

`Receive` and `ReceiveFromHTTPRequest` compose three primitives that remain available individually for advanced use:

| Function | Purpose |
| --- | --- |
| `primitive.VerifyWebhookSignature(options)` | Verify the `Primitive-Signature` header alone, without parsing the body. |
| `primitive.ParseJSONBody(rawBody)` | Parse the raw body into a generic JSON value. |
| `primitive.HandleWebhook(options)` | Verify + parse + validate, returning an `*EmailReceivedEvent` (not yet normalized to `ReceivedEmail`). |

Reach 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](https://test.abhinandan.one/go-webhook-event-types.md).

## Common failure: signature mismatch after body re-encoding

If 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.

The 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.
