{"record":{"id":"de0fc86c90335f23","repo":"knadh/listmonk","slug":"error-unmarshalling-lettermint-notification-v","errorCode":null,"errorMessage":"error unmarshalling Lettermint notification: %v","messagePattern":"error unmarshalling Lettermint notification: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/bounce/webhooks/lettermint.go","lineNumber":77,"sourceCode":"\t}\n\n\t// Decode the hex signature from the header.\n\tsigB, err := hex.DecodeString(strings.TrimSpace(sigHex))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid signature encoding: %v\", err)\n\t}\n\n\t// Compute HMAC-SHA256 of \"{timestamp}.{body}\" and compare.\n\tmac := hmac.New(sha256.New, l.hmacKey)\n\tmac.Write([]byte(fmt.Sprintf(\"%d.%s\", ts, body)))\n\n\tif !hmac.Equal(mac.Sum(nil), sigB) {\n\t\treturn nil, fmt.Errorf(\"invalid signature\")\n\t}\n\n\tvar n lettermintNotif\n\tif err := json.Unmarshal(body, &n); err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling Lettermint notification: %v\", err)\n\t}\n\n\t// Map event to bounce type.\n\tvar typ string\n\tswitch n.Event {\n\tcase \"message.hard_bounced\":\n\t\ttyp = models.BounceTypeHard\n\tcase \"message.soft_bounced\":\n\t\ttyp = models.BounceTypeSoft\n\tcase \"message.spam_complaint\":\n\t\ttyp = models.BounceTypeComplaint\n\tdefault:\n\t\t// Ignore irrelevant events (e.g. webhook.test).\n\t\treturn nil, nil\n\t}\n\n\tcampUUID := \"\"\n\tif len(n.Data.Metadata) > 0 {","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/bounce/webhooks/lettermint.go#L59-L95","documentation":"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).","triggerScenarios":"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).","commonSituations":"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).","solutions":["Confirm the body is valid JSON matching Lettermint's bounce event schema: {id, event, timestamp, data:{message_id, recipient, ...}}.","Log the wrapped json.Unmarshal error — Go's error names the exact field/type conflict.","Use a real captured Lettermint webhook payload (signed) as a test fixture instead of a hand-built one.","Check you wired the correct Lettermint webhook (bounce events) to this endpoint."],"exampleFix":"// before: data as a string\nbody := []byte(`{\"event\":\"message.hard_bounced\",\"data\":\"user@example.com\"}`)\n\n// after: data as an object\nbody := []byte(`{\"event\":\"message.hard_bounced\",\"data\":{\"message_id\":\"m1\",\"recipient\":\"user@example.com\"}}`)","handlingStrategy":"validation","validationCode":"func isLettermintBouncePayload(body []byte) bool {\n    if !json.Valid(body) {\n        return false\n    }\n    var probe struct {\n        Event string `json:\"event\"`\n        Data  struct {\n            Recipient string `json:\"recipient\"`\n        } `json:\"data\"`\n    }\n    return json.Unmarshal(body, &probe) == nil && probe.Event != \"\" && probe.Data.Recipient != \"\"\n}","typeGuard":null,"tryCatchPattern":"bounces, err := lm.ProcessBounce(sig, body)\nif err != nil {\n    if strings.Contains(err.Error(), \"unmarshalling Lettermint notification\") {\n        log.Printf(\"lettermint payload schema mismatch: %v; body=%s\", err, body)\n        http.Error(w, \"invalid payload\", http.StatusBadRequest)\n        return\n    }\n    http.Error(w, \"processing failed\", http.StatusInternalServerError)\n}","preventionTips":["Use captured real Lettermint bounce payloads as test fixtures, not hand-built JSON.","Validate json.Valid(body) and the presence of event/data.recipient before processing.","Track Lettermint webhook schema updates; pin the handler tests to the documented schema.","Note that unknown events (e.g. webhook.test) return nil,nil by design — only schema violations throw this error."],"tags":["webhook","json","lettermint","schema","bounce"],"backgroundTag":"webhook-payload-schema-mismatch","analyzedSha":"670c01717d48647093335cc23a6be6f4b79c3b6b","analyzedAt":"2026-09-01T03:39:35.452Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}