Webhook Event Types
Explains the Go SDK's webhook event catalog, email, payment, and interaction families, and how HandleWebhookEvent uses the X-Webhook-Event header to return typed events with forward compatibility for unknown types.
Every Primitive webhook delivery carries its event type in the X-Webhook-Event HTTP header, not in the response body. The Go SDK's primitive.HandleWebhookEvent reads that header to return a typed Go value for known event types, and a forward-compatible UnknownEvent for anything it doesn't recognize yet.
Why the header, not the body#
The stored body is sent verbatim with no shared envelope, so its shape depends on which family it belongs to:
| Family | Example type | Body shape |
|---|---|---|
email.* | email.received | Carries the event name in a top-level event field |
payment.* | payment.settled | Flat fields; the event name is in type, not event |
interaction.* | interaction.x402.challenge | Just {"interaction": {...}}, no event or type field at all |
Because only email.* bodies self-describe consistently, the header is the one discriminator that works across every family. ParseWebhookEvent and HandleWebhookEvent both key off it first, falling back to a body-level event string only for backward compatibility with older senders.
The event catalog#
The full set of current header values is exported as the WebhookEventTypes slice:
Email family (subject: an email)
email.receivedemail.bouncedemail.tls_reportemail.dmarc_reportemail.dmarc_failure
Payment family (subject: an x402 settlement)
payment.settledpayment.failed
Interaction family (subject: an x402-over-email or ack interaction step)
interaction.x402.challengeinteraction.x402.paymentinteraction.x402.settledinteraction.x402.rejectedinteraction.x402.declinedinteraction.x402.expiredinteraction.x402.verify_timeoutinteraction.ack.receivedinteraction.ack.requestedinteraction.ack.ackedinteraction.ack.canceledinteraction.ack.expired
Only email.received carries a dedicated, schema-validated struct (EmailReceivedEvent). The other email.* types (bounces, TLS/DMARC reports) fall through to UnknownEvent today, alongside any type Primitive adds in the future.
Verifying and dispatching a delivery#
Signature verification runs on the raw request body and is independent of which family the delivery belongs to. It works identically for email.*, payment.*, and interaction.* bodies. Each delivery is signed once, and that signature is sent on three headers: Primitive-Signature (primary), X-Primitive-Signature, and the legacy X-Webhook-Signature.
HandleWebhookEvent does the verify-then-classify sequence in one call:
package main
import (
"io"
"log"
"net/http"
primitive "github.com/primitivedotdev/sdks/sdk-go"
)
func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
event, err := primitive.HandleWebhookEvent(primitive.HandleWebhookOptions{
Body: body,
Headers: r.Header,
Secret: "whsec_...",
})
if err != nil {
// signature or verification failure
http.Error(w, "invalid webhook", http.StatusBadRequest)
return
}
switch {
case primitive.IsPaymentSettledEvent(event):
settled := event.(primitive.PaymentEvent) // flat fields; amount in base units
log.Println("settled:", settled.ChallengeID, settled.Amount, settled.SettleTx)
case primitive.IsInteractionX402Event(event):
x402 := event.(primitive.InteractionEvent) // interaction.x402.* lifecycle
log.Println("interaction:", x402.Event)
default:
if received, ok := event.(primitive.EmailReceivedEvent); ok {
log.Println("inbound email:", received.Email.Headers.Subject)
}
}
w.WriteHeader(http.StatusOK)
}
HandleWebhookEvent never errors on an unrecognized event type. It returns UnknownEvent instead, so a code path that hasn't been updated for a new event type keeps compiling and running rather than breaking on delivery.
HandleWebhookEvent vs. the legacy HandleWebhook#
HandleWebhook is hard-typed to email.received and returns an error for any other event type, so use HandleWebhookEvent unless your integration only ever handles inbound email. HandleWebhook remains for backward compatibility with integrations written before the payment and interaction families existed. See Webhook Events Overview for the shared contract this catalog implements.
Lower-level helpers#
For cases where you already have the parsed body and just need to classify it, ParseWebhookEvent accepts the raw parsed value plus the X-Webhook-Event header value as an optional second argument; this is what HandleWebhookEvent calls internally after verification. VerifyWebhookSignature is available on its own when you need to verify without parsing at all.
Next steps#
Normalize a verified email.received delivery into a ReceivedEmail with primitive.Receive.
Webhook Payload Schema ValidationValidate a raw email.received payload against the embedded JSON Schema directly.
x402 ErrorsInterpret X402Error status codes and retry-after headers on payment calls.
Webhook Events OverviewSee the cross-SDK signature verification and event-catalog contract this page implements.
Was this page helpful?