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 object, all in one call.
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; this page covers verification and normalization mechanics only.
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#
Receiveverifies the HMAC signature over the exact bytes Primitive sent, so read the body without re-encoding or re-serializing it:body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "failed to read body", http.StatusBadRequest) return }Do not
json.Unmarshaland re-json.Marshalthe body before passing it toReceive. 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]stringfrom the incoming headers.Receivelooks for thePrimitive-Signatureheader (falling back to the legacyMyMX-Signatureheader) case-insensitively: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:email, err := primitive.Receive(primitive.HandleWebhookOptions{ Body: body, Headers: headers, Secret: os.Getenv("PRIMITIVE_WEBHOOK_SECRET"), }) - 4
Handle the verification error#
Receivereturns a non-nilerrorwhen 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":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,
emailis a*primitive.ReceivedEmailready to pass straight intoclient.Reply(ctx, email, ...)orclient.Forward(ctx, email, ...):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 and Replying to Emails.
What counts as a valid signature#
Primitive signs every webhook delivery with HMAC-SHA256 over ${timestamp}.${rawBody}, sent as:
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.
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.
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.
Next steps#
Branch on email., payment., and interaction.x402.* events from the same webhook endpoint.
Validating Email AuthenticityDecide whether an inbound email's SPF/DKIM/DMARC results can be trusted before acting on it.
Replying to EmailsPass the ReceivedEmail from Receive straight into Client.Reply.
Standard Webhooks Signature Support (Go)Verify deliveries using the Standard Webhooks header format instead of Primitive-Signature.
Was this page helpful?