Custom Email Headers — When and Why
Truncus lets you inject a small set of standard RFC headers into the outbound MIME of every send. The feature is off by default and must be enabled explicitly in Dashboard → Settings → Email Features. It is available on the Pro and Scale plans.
When to enable
Turn it on if you are sending:
- Newsletters or digests — Gmail and Yahoo require a working
List-Unsubscribeheader on bulk mail. Without it your sender reputation will degrade and inbox placement will follow. - Cold outreach sequences — same Gmail/Yahoo requirement, and
Feedback-IDlets you slice Postmaster Tools by cohort or campaign. - Auto-reply / digest mail — set
Auto-Submitted: auto-generatedso vacation responders and other auto-mailers skip your message. - Threaded transactional mail — use
References+In-Reply-Toto keep replies in the right thread (e.g. an order-status mail in the original order thread).
When NOT to enable
Leave it off if you are sending only transactional mail
(password resets, receipts, magic links). Several inbox providers treat the
presence of List-Unsubscribe as a "this is bulk" signal — exactly
what you don't want for a one-to-one receipt.
Allowed headers
| Header | Use case |
|---|---|
List-Unsubscribe | One-click unsubscribe URL and/or mailto. |
List-Unsubscribe-Post | Companion to List-Unsubscribe enabling RFC 8058 one-click. |
Auto-Submitted | Suppresses vacation responders for automated mail. |
Feedback-ID | Gmail Postmaster Tools cohort/campaign identifier. |
X-Entity-Ref-ID | Gmail thread-collapse hint. |
X-Mailer | Free-form mailer identifier. |
References | RFC 5322 threading — list of parent Message-IDs. |
In-Reply-To | RFC 5322 threading — direct parent Message-ID. |
Everything else is rejected. In particular, you cannot set
From, To, Bcc, Subject,
Reply-To, Message-ID, DKIM-Signature,
Content-Type, or any ARC-* header. These are managed
by Truncus and SES so the message stays authentic, signed, and routable.
Example
POST https://truncus.co/api/v1/emails/send
Authorization: Bearer tr_live_...
Idempotency-Key: 9b8b...
{
"to": "subscriber@example.com",
"from": "newsletter@mail.yourdomain.com",
"subject": "Weekly digest",
"html": "<h1>Hi!</h1>",
"headers": {
"List-Unsubscribe": "<https://app.example.com/u/abc123>, <mailto:unsubscribe@example.com>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
"Feedback-ID": "weekly:saas:cohort-42"
}
}
Errors
| HTTP | Code | Meaning |
|---|---|---|
| 403 | custom_headers_not_enabled | The feature is off for this account. Enable in Dashboard → Settings → Email Features. |
| 400 | header_not_allowed | A denylisted header (e.g. From) or non-allowlisted header was sent. The response includes a denied array. |
| 400 | invalid_header | Bad shape, CR/LF in a value, too many keys, or payload over 2 KB. |
Limits
- Maximum 10 headers per send
- Header name max 80 chars, ASCII letters/digits/
-only - Header value max 998 chars (RFC 5322 line limit)
- Total serialized payload ≤2 KB
- CR and LF in values are rejected (CRLF injection defense)
Edge cases
Caller vs. tracking-layer precedence
Truncus' click-tracking layer automatically adds a
List-Unsubscribe + List-Unsubscribe-Post pair when
tracking is enabled on the send. If you also pass either header via
headers, your value wins. The tracking-layer
default is suppressed on a per-header basis (so passing only
List-Unsubscribe still gets the auto-generated
List-Unsubscribe-Post, and vice versa). Dedupe is keyed on the
lowercased header name and runs before MIME emit — Gmail and Yahoo reject
mail with duplicate List-Unsubscribe lines under the 2024 Bulk
Sender requirements, so this is intentional and not configurable.
Why these headers are on the denylist
| Header(s) | Why denied |
|---|---|
From, Sender, Return-Path, Resent-From | Identity. Allowing override would let any caller spoof the sending address inside an otherwise-verified domain, defeating SPF/DKIM/DMARC alignment. |
To, Cc, Bcc (and Resent-* variants) | Envelope. These are derived from the JSON body so suppression-list checks, dedupe, recipient cooldowns, and warmup caps all see the real recipient set. Allowing header overrides would let Bcc recipients silently bypass suppression. |
Subject, Reply-To | Overlap with body params. The existing subject and reply_to fields are how you set these — exposing them twice would create ambiguity for which value wins. |
Date, Message-ID, Resent-Date, Resent-Message-ID | Truncus-owned. Message-ID is the cross-provider correlation anchor (paired with X-Truncus-Global-Message-Id) — overriding it would break webhooks and replay. |
DKIM-Signature, ARC-*, Authentication-Results, Received, Received-SPF | Authentication. SES signs DKIM at send time and receiving MTAs add the rest. A caller-provided value would either be stripped by SES or, worse, accepted and immediately invalidated. |
Content-Type, Content-Transfer-Encoding, MIME-Version | Content negotiation. Truncus builds the multipart structure (text + html + optional attachments). Overriding these breaks MIME parsing in major clients. |
Non-ASCII header values
Header values are passed through to the MIME unchanged. Non-ASCII bytes
are valid UTF-8 in the value but will not be auto-encoded —
several receivers will mangle non-ASCII characters in headers they don't
recognize as encoded-word (RFC 2047). If you need an accented character in
a header value (rare — typically only useful in custom X-Mailer
strings), pre-encode it yourself as =?UTF-8?B?…?=. The
validator rejects only CR, LF, NUL, and most C0/C1 control bytes — UTF-8
letter bytes pass.
Rate limiting scope
Custom headers do not add a separate rate-limit scope. The same per-API-key burst limiter (10/sec, 60/min) and the same monthly plan cap (3K / 50K / 300K) apply. There is no per-header throttle. The 2 KB serialized-size cap is the only quantitative limit attached to the feature itself.
Syntax error in header value
Validation runs before persistence and before
suppression / cap checks. A request with a bad header value returns
400 invalid_header with a message naming the offending header
and the rule it broke. The email is never written to the database, never
counted against your monthly cap, and never sent — there is no partial
state to clean up.
Documentation translations
This article is currently English-only. The Dutch and German manual show
the same English content as a fallback until translation passes happen —
the article metadata (titles in the sidebar) is localized, but the body
text is shared. Track the translation pass in the
content/manual/{nl,de}/ directories.