knadh/listmonk · error

error unmarshalling Sendgrid notification: %v

Error message

error unmarshalling Sendgrid notification: %v

What it means

Sendgrid's ProcessBounce expects the verified request body to be a JSON array of sendgridNotif objects. If json.Unmarshal fails — body isn't a JSON array, is malformed, or has fields with incompatible types — it returns 'error unmarshalling Sendgrid notification'.

Source

Thrown at internal/bounce/webhooks/sendgrid.go:60

	}

	pubKey, err := x509.ParsePKIXPublicKey(sigB)
	if err != nil {
		return nil, err
	}

	return &Sendgrid{pubKey: pubKey.(*ecdsa.PublicKey)}, nil
}

// ProcessBounce processes Sendgrid bounce notifications and returns one or more Bounce objects.
func (s *Sendgrid) ProcessBounce(sig, timestamp string, b []byte) ([]models.Bounce, error) {
	if err := s.verifyNotif(sig, timestamp, b); err != nil {
		return nil, err
	}

	var notifs []sendgridNotif
	if err := json.Unmarshal(b, &notifs); err != nil {
		return nil, fmt.Errorf("error unmarshalling Sendgrid notification: %v", err)
	}

	out := make([]models.Bounce, 0, len(notifs))
	for _, n := range notifs {
		if n.Event != "bounce" {
			continue
		}

		typ := models.BounceTypeHard
		if n.BounceClassification == "technical" || n.BounceClassification == "content" {
			typ = models.BounceTypeSoft
		}

		tstamp := time.Unix(n.Timestamp, 0)
		bn := models.Bounce{
			CampaignUUID: n.CampaignUUID,
			Email:        strings.ToLower(n.Email),
			Type:         typ,

View on GitHub (pinned to 670c01717d)

Solutions

  1. In SendGrid settings, set Event Webhook payload format to JSON, not the legacy form-encoded format
  2. Validate the body is a JSON array of objects, e.g. [{"email":"a@b.c","event":"bounce","timestamp":1700000000}]
  3. Log the raw body on failure and lint it as JSON
  4. Check that signature verification passed and the body wasn't altered in transit (any mutation breaks both verify and unmarshal)

Example fix

// before
{"email":"a@b.c","event":"bounce","timestamp":1700000000}
// after
[{"email":"a@b.c","event":"bounce","timestamp":1700000000}]
Defensive patterns

Strategy: validation

Validate before calling

func validSendgridBody(b []byte) bool {
    var probe []struct {
        Email     string `json:"email"`
        Event     string `json:"event"`
        Timestamp int64  `json:"timestamp"`
    }
    return json.Unmarshal(b, &probe) == nil
}
// verify body parses as a JSON array before calling ProcessBounce

Try / catch

bounces, err := handler.ProcessBounce(sig, ts, body)
if err != nil {
    if strings.HasPrefix(err.Error(), "error unmarshalling Sendgrid notification") {
        log.Printf("sendgrid raw body: %s", string(body))
        http.Error(w, "invalid payload", http.StatusBadRequest)
        return
    }
    http.Error(w, "webhook error", http.StatusInternalServerError)
}

Prevention

When it happens

Trigger: The SendGrid webhook endpoint receives a body that doesn't decode into []sendgridNotif: empty body, a single JSON object instead of an array, malformed JSON, form-encoded POST (SendGrid's default 'Event Webhook' POST vs JSON), or wrong types for timestamp (int64) or event fields.

Common situations: SendGrid Event Webhook is configured with default (form-encoded) payload format instead of JSON; testing with a single-object JSON body; a proxy injecting an HTML error page; SendGrid account sending non-bounce event batches with unexpected shapes.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/11255d88faad1427. Report an issue: GitHub.