---
title: "Webhook Event Types"
canonical: "https://test.abhinandan.one/go-webhook-event-types"
markdown_url: "https://test.abhinandan.one/go-webhook-event-types.md"
publisher: "Primitive SDKs"
kind: "concept"
content_type: "reference"
category: "Go SDK"
description: "The X-Webhook-Event HTTP header names every Primitive webhook delivery's event type across the email.*, payment.*, and interaction.* families."
keywords: ["X-Webhook-Event header", "WebhookEventTypes", "HandleWebhookEvent", "PaymentEvent Go", "InteractionEvent Go", "IsPaymentSettledEvent"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:54:54.961778+00:00"
source_files:
  - "sdk-go/webhook.go"
sections:
  - {anchor: "why-the-header-not-the-body", title: "Why the header, not the body"}
  - {anchor: "the-event-catalog", title: "The event catalog"}
  - {anchor: "verifying-and-dispatching-a-delivery", title: "Verifying and dispatching a delivery"}
  - {anchor: "handlewebhookevent-vs-the-legacy-handlewebhook", title: "HandleWebhookEvent vs. the legacy HandleWebhook"}
  - {anchor: "lower-level-helpers", title: "Lower-level helpers"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# 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.received`
- `email.bounced`
- `email.tls_report`
- `email.dmarc_report`
- `email.dmarc_failure`

**Payment family** (subject: an x402 settlement)

- `payment.settled`
- `payment.failed`

**Interaction family** (subject: an x402-over-email or ack interaction step)

- `interaction.x402.challenge`
- `interaction.x402.payment`
- `interaction.x402.settled`
- `interaction.x402.rejected`
- `interaction.x402.declined`
- `interaction.x402.expired`
- `interaction.x402.verify_timeout`
- `interaction.ack.received`
- `interaction.ack.requested`
- `interaction.ack.acked`
- `interaction.ack.canceled`
- `interaction.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:

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