/ TL;DR
"Body hash did not verify" means your message body changed after your server signed it. DKIM stored a fingerprint of the body in the signature; something between signing and delivery altered the content, so the receiver's recomputed fingerprint no longer matches. Your configuration is usually correct — this is a mail-flow problem.
The usual culprit is a system that touches the message after signing: a compliance footer, a security gateway rewriting links, a mailing list appending text, or a MIME re-encoding. Two fixes: set canonicalization to relaxed/relaxed to tolerate trivial whitespace changes, and — the durable one — make DKIM signing the last step before the message leaves, after every content-modifying tool.
Forwarding and mailing lists break DKIM by design; ARC lets intermediaries you control vouch for the original result. And don't rotate keys to fix a body hash error — that fixes selector and key problems, not a changed body.
/ 01
What the body hash checks
DKIM proves two things about a message: that it came from a holder of your private key, and that it hasn't been altered in transit. It does the second with a hash. When your server signs a message, it computes a cryptographic fingerprint of the message body and stores it in the bh= tag of the DKIM-Signature header, then signs that hash plus a set of chosen headers with the private key, producing the b= value.
On the receiving side, the server fetches your public key from DNS, recomputes the body's fingerprint on the message it actually received, and checks that recomputed hash against the bh= in the signature. If they match, the body is intact and verification proceeds; if they differ by even a single character, the check fails with the message you came here about. This is the integrity half of DKIM working exactly as designed — the whole point is to catch a body that changed after it was signed.
That design has a sharp implication worth sitting with. A body hash failure means the signature was real and the math was done correctly, and then the content was modified. So the reflex to edit your DKIM record or regenerate keys is usually wrong: the record and key are fine, and the problem is a system in your outbound path that altered the message after signing. Fixing it is a matter of finding and reordering that system, not touching your authentication setup.
/ 02
Which failure do you have?
"DKIM fail" is a bucket, not a diagnosis, and identifying which bucket you're in gets you most of the way to the fix. Reading the exact reason in the receiver's Authentication-Results header sorts you into one of a few cases.
A body hash mismatch — the reason mentions the body hash — means the content changed after signing, and it's the focus of this guide. A missing or wrong selector, often shown as "no key for signature," means the receiver couldn't find your public key, which is a DNS problem: the selector in the signature doesn't match a published record, or a new key was used before DNS propagated. A key too short is rejected outright by modern receivers. An expired signature means the optional x= deadline passed. And a syntax error in the signature shows as permerror.
One case belongs in a different bucket entirely and causes endless confusion. If DKIM passes but DMARC still fails, that's not a signature problem at all — it's alignment, where the signing domain doesn't match your visible From address. That's a distinct fix covered in our authentication guide. Everything below assumes DKIM itself is failing, and most often that it's failing on the body hash.
/ 03
Body hash did not verify
This is the most specific and most commonly searched DKIM error, and its meaning is precise: the body was changed after your server signed it. The frustrating part is that it usually means your DKIM setup is correct — the signer worked, the key is valid, and then something downstream edited the message. The task is to find what.
The culprits are a predictable set of systems that sit between your signer and the recipient. Compliance and antivirus tools that append a footer or legal disclaimer after signing are the classic cause. Security gateways like the major enterprise filters rewrite URLs for safe-link protection and insert warning banners, both of which change the body. Mailing lists almost universally break DKIM by adding unsubscribe footers and rewrapping lines. And MIME re-encoding — a relay converting 8-bit content to quoted-printable, or reorganizing multipart messages with attachments — alters the bytes the hash was computed over even when the visible text looks identical.
A useful tell: multipart messages with attachments or HTML-plus-text alternatives are especially prone to this, because there are more structural bytes for an intermediary to normalize. And a diagnostic shortcut — an empty body has a known, fixed hash value, so if you ever see that value in bh= while the delivered message clearly has content, you know content was added after signing.
/ 04
Canonicalization: simple vs relaxed
Before the body is hashed, it's normalized according to a canonicalization mode set in the c= tag, and that mode is effectively your tolerance setting for change. There are two options. Simple is strict: every line, every trailing space, and the exact header formatting must survive untouched, so an extra blank line or a re-folded header breaks the hash. Relaxed is forgiving: it collapses runs of whitespace and lowercases header names before hashing, so the trivial normalizations that ordinary relays perform don't count as changes.
The practical upshot is to run c=relaxed/relaxed as your baseline, header and body both. If your failures come from a relay that re-wraps lines or normalizes whitespace, switching from simple to relaxed often clears them on its own, and reports from senders who made that switch behind disclaimer systems describe body-hash failures dropping sharply afterward. It costs nothing and removes a whole class of false failures.
What relaxed can't do is the important caveat. It tolerates formatting noise, not content change. An appended disclaimer, a rewritten link, a repackaged attachment, or a MIME re-encoding all still break the hash under relaxed canonicalization, because those genuinely alter the body rather than merely reformat it. So relaxed is a necessary baseline and rarely a complete fix. When it doesn't resolve the failure, you have a system actually modifying content, and the next section is the real answer.
/ 05
The real fix: sign last
The durable fix for a body hash failure is a single principle: DKIM signing must be the last thing that happens to a message before it leaves your infrastructure. Signing before modification is almost always why the hash fails, so the answer is to reorder your outbound flow. Composition, security scanning, disclaimer injection, and link rewriting all belong upstream of the signer; the signer runs last, over the final bytes, and nothing touches the body afterward.
In practice this means auditing every system in your sending pipeline and identifying the one that modifies the message after the current signing point. If a compliance tool appends footers, move that injection before the signer, or move signing to the egress gateway that runs after it. If a security gateway rewrites URLs, sign after the rewrite rather than before. When you can't move the modifier, the alternative is to re-sign at the very edge: let the outbound gateway apply DKIM to the finished message on its way out, so the signature reflects what's actually delivered.
A note on a tempting shortcut to avoid. DKIM has an optional l= tag that limits how much of the body is signed, which can make appended footers survive — but it's a security risk, because it lets an attacker add arbitrary content below the signed portion, and some evaluators trust signatures with l= less. Don't reach for it. Reordering the flow so signing comes last is both the correct fix and the secure one, and once it's in place the failure stops recurring.
/ 06
Forwarding, lists, and ARC
Some body hash failures aren't your infrastructure's fault at all — they happen after the message has left you, and that changes the fix. DKIM survives forwarding only if the body and signed headers arrive unchanged, which actually makes it more resilient than SPF, since SPF breaks on the forwarder's IP while DKIM sails through an honest relay untouched. But the moment a forwarder or list modifies the message, the body hash breaks on that hop even though it passed on the original leg.
Mailing lists are the near-universal offenders: they append footers, rewrite the subject, add List headers, and rewrap lines, any one of which invalidates the signature. Forwarding services vary — some preserve the message byte-for-byte and DKIM survives, others rewrite it and DKIM breaks. The key distinction is control. If you operate the intermediary, you can fix it: configure the list to leave the body alone or to strip and re-sign with its own key, or deploy ARC.
ARC, the Authenticated Received Chain, is the protocol built for exactly this. An ARC-aware intermediary records the authentication results at the moment it received the message and cryptographically attests to them, so the final receiver can trust that the mail was properly authenticated when it arrived at the intermediary, even though the body was modified afterward. Major receivers including Gmail and Microsoft honor ARC, which makes it the standard answer for gateways and lists you run. When you don't control the intermediary, forwarding breakage may be unavoidable; the defense is to have SPF, DKIM, and DMARC all in place so one broken hop doesn't collapse your whole authentication story.
/ 07
How to diagnose it
Diagnosis is fast if you work from evidence rather than guesses. Start with the raw message source, not the rendered inbox view — the rendered view hides MIME boundaries, transfer encoding, injected footers, and link rewrites, which are exactly the things you're hunting. Open the raw source and read the Authentication-Results header for the precise DKIM reason, and inspect the DKIM-Signature tags: d= domain, s= selector, c= canonicalization, and bh= body hash. Our email header analyzer pulls these apart for you.
Then run the decisive test: send a message directly to a Gmail or Outlook account, bypassing any mailing list, forwarder, or gateway. If DKIM passes on that direct delivery, your signing is correct and the problem is a system in the delivery chain — go find the modifier. If it fails even on direct delivery to Gmail, the change is happening on your side, before the message leaves, so your signer is either broken or running before a modifier.
For failures spread across many senders or appearing only at certain receivers, your DMARC aggregate reports are the map — they show which sources and which receiving domains are failing, so you can target the one broken path instead of making a global change that creates new problems. Confirm the DKIM record and selector resolve with our DKIM checker, and if the diagnosis turns out to be a short or mismatched key rather than a changed body, generate a fresh 2048-bit or Ed25519 pair with the DKIM key generator. And always re-verify with a controlled test before declaring it fixed — pair this with the SPF lookup guide to keep the whole authentication stack healthy.
/ 08 — FAQ