knadh/listmonk · error

error unmarshalling Lettermint notification: %v

Error message

error unmarshalling Lettermint notification: %v

What it means

After signature verification succeeds, Lettermint's ProcessBounce unmarshals the body into the lettermintNotif struct. This error is thrown when the signed body is not valid JSON or its field types conflict with the struct (e.g. `data` not an object, `event` not a string, nested `response` of wrong shape).

Source

Thrown at internal/bounce/webhooks/lettermint.go:77

	}

	// Decode the hex signature from the header.
	sigB, err := hex.DecodeString(strings.TrimSpace(sigHex))
	if err != nil {
		return nil, fmt.Errorf("invalid signature encoding: %v", err)
	}

	// Compute HMAC-SHA256 of "{timestamp}.{body}" and compare.
	mac := hmac.New(sha256.New, l.hmacKey)
	mac.Write([]byte(fmt.Sprintf("%d.%s", ts, body)))

	if !hmac.Equal(mac.Sum(nil), sigB) {
		return nil, fmt.Errorf("invalid signature")
	}

	var n lettermintNotif
	if err := json.Unmarshal(body, &n); err != nil {
		return nil, fmt.Errorf("error unmarshalling Lettermint notification: %v", err)
	}

	// Map event to bounce type.
	var typ string
	switch n.Event {
	case "message.hard_bounced":
		typ = models.BounceTypeHard
	case "message.soft_bounced":
		typ = models.BounceTypeSoft
	case "message.spam_complaint":
		typ = models.BounceTypeComplaint
	default:
		// Ignore irrelevant events (e.g. webhook.test).
		return nil, nil
	}

	campUUID := ""
	if len(n.Data.Metadata) > 0 {

View on GitHub (pinned to 670c01717d)

Solutions

  1. Confirm the body is valid JSON matching Lettermint's bounce event schema: {id, event, timestamp, data:{message_id, recipient, ...}}.
  2. Log the wrapped json.Unmarshal error — Go's error names the exact field/type conflict.
  3. Use a real captured Lettermint webhook payload (signed) as a test fixture instead of a hand-built one.
  4. Check you wired the correct Lettermint webhook (bounce events) to this endpoint.

Example fix

// before: data as a string
body := []byte(`{"event":"message.hard_bounced","data":"user@example.com"}`)

// after: data as an object
body := []byte(`{"event":"message.hard_bounced","data":{"message_id":"m1","recipient":"user@example.com"}}`)
Defensive patterns

Strategy: validation

Validate before calling

func isLettermintBouncePayload(body []byte) bool {
    if !json.Valid(body) {
        return false
    }
    var probe struct {
        Event string `json:"event"`
        Data  struct {
            Recipient string `json:"recipient"`
        } `json:"data"`
    }
    return json.Unmarshal(body, &probe) == nil && probe.Event != "" && probe.Data.Recipient != ""
}

Try / catch

bounces, err := lm.ProcessBounce(sig, body)
if err != nil {
    if strings.Contains(err.Error(), "unmarshalling Lettermint notification") {
        log.Printf("lettermint payload schema mismatch: %v; body=%s", err, body)
        http.Error(w, "invalid payload", http.StatusBadRequest)
        return
    }
    http.Error(w, "processing failed", http.StatusInternalServerError)
}

Prevention

When it happens

Trigger: Calling ProcessBounce with a validly-signed but non-JSON or wrong-shaped body: empty body, plain text, `data` as a string instead of an object, `metadata` conflicting with RawMessage expectations in unexpected ways, or a payload from a different webhook (e.g. Lettermint's other event endpoints).

Common situations: Testing with a signature computed over a dummy body that isn't the actual JSON; Lettermint updating their event schema; posting the wrong Lettermint webhook type to the bounce endpoint; hand-written test fixtures with typo'd field types (numbers where strings are expected).

Related errors


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