DKIM Setup for Transactional Email (2026): DNS TXT, Alignment, and API Send
Developer guide to DKIM for app transactional email: how signing works, the selector TXT record, common DNS mistakes, SPF/DMARC alignment at a high level, and sending via Rovela once the domain verifies.
Why DKIM matters before you scale OTP and receipts
Transactional email—signup codes, password resets, magic links, invoices, order receipts—fails differently from marketing mail. A newsletter that lands in spam is annoying. An OTP that never arrives is a blocked signup and a support ticket.
Inbox providers score every message on whether the sending domain can prove the message is authentic. DKIM (DomainKeys Identified Mail) is the cryptographic half of that proof: your provider signs each outbound message with a private key; DNS publishes the matching public key so the receiver can verify the body and selected headers were not altered in transit.
This guide is a developer-focused DKIM setup for app transactional send—not another high-level “SPF + DKIM + DMARC in one page” overview. For those overviews see SPF, DKIM, and DMARC explained and Email deliverability: DKIM, SPF, and DMARC. Dedicated SPF and DMARC how-tos live at SPF record setup and DMARC record setup.
If you are wiring a product to a transactional HTTP API, get DKIM (and its companions) right before you turn up OTP volume. Authentication is cheaper than warming a burned reputation later.
How DKIM works on transactional API mail
At a high level:
- You add a sending domain (or subdomain) with your email provider.
- The provider generates a key pair for that domain. You never hold the private key for normal API send—the provider keeps it and signs mail leaving their infrastructure.
- You publish a DNS TXT record at a selector-scoped name:
<selector>._domainkey.yourdomain.com, with a value shaped likev=DKIM1; k=rsa; p=<public key>. - On every send, the provider attaches a
DKIM-Signatureheader. Receivers fetch the public key from DNS and verify the signature.
For Rovela specifically, docs are clear on ownership of the keys:
- Adding a domain via
POST /domainsreturns the DNS records the domain needs. DKIM keys are created per domain server-side—read the values from that response (or the dashboard), do not invent or copy keys from a blog example (API: Domains). - Rovela holds the private key and signs outbound mail; you publish the public TXT (Managing domains, Domain verification).
- You cannot send from an unverified domain. An unverified
fromis refused with a400, not accepted and silently dropped (Quick Start).
DKIM does not replace SPF. SPF answers “may this IP send for the domain?” DKIM answers “was this message altered, and which domain claims it?” DMARC tells receivers what to do when those checks fail and whether the identifiers align. Treat them as one system; this article stays on the DKIM leg.
The DNS TXT record you actually publish
Shape (placeholders match Rovela docs—use the selector and p= value your project returns):
name: <selector>._domainkey.yourdomain.com
type: TXT
value: v=DKIM1; k=rsa; p=<public key>
Notes that trip people up in production:
- The selector is part of the hostname. That is intentional: it lets you rotate keys by publishing a new selector without ripping out the old one mid-flight (Domain verification).
- Many DNS UIs treat the name as relative to the zone. If the dashboard shows
<selector>._domainkey.yourdomain.comand your provider already appends the zone, you often enter only<selector>._domainkey. Typing the FQDN into a relative field creates<selector>._domainkey.yourdomain.com.yourdomain.com, which never resolves—and verification looks “stuck” forever (Domain verification). - Do not wrap the TXT value in extra quotes unless your provider strips them. A literal
"character becomes part of the published value and breaks the key (Domain verification). - Long
p=keys are often split across quoted strings in DNS wire format. That is normal. Paste exactly what the API/dashboard gives you; do not hand-edit the public key.
Key rotation (docs-backed)
When you rotate, Rovela’s domains API can expose a second DKIM record labeled along the lines of DKIM (add alongside the one above) with required_for_verification: false. Publish it alongside the existing record—not instead of it. Replacing the old TXT early breaks signing for messages still using the previous key (API: Domains).
Align DKIM with SPF and DMARC (high level)
You do not need a full DMARC policy essay to ship OTP, but you do need the neighbors correct:
| Record | Question it answers | Typical shape (see live docs for your values) |
|---|---|---|
| SPF | May this server send for the domain? | One TXT at the domain: v=spf1 include:<our-host> ~all — exactly one SPF record; merge include: if you already have one (Domain verification) |
| DKIM | Was this message altered? | TXT at <selector>._domainkey… with v=DKIM1; k=rsa; p=… |
| DMARC | What if SPF/DKIM fail? | TXT at _dmarc.yourdomain.com, start v=DMARC1; p=none; then tighten (Domain verification, DMARC record setup) |
| Return-path | Where do bounces go? | MX + SPF on a subdomain (not the root); SPF does not inherit from the parent (API: Domains) |
DMARC alignment (simplified): the domain in the visible From header should match the domain DKIM signed for (and/or the SPF-authenticated Mail From / return-path domain, depending on your policy). That is why sending from: no-reply@mail.yourapp.com on a domain you actually verified—and isolating transactional traffic on a sending subdomain—shows up in deliverability guides (Sending subdomain setup, Why OTP emails go to spam).
Rovela’s domain verification expects the set of records returned for your domain (SPF, DKIM, DMARC, and return-path when configured). Click Verify after publish; use dig when the dashboard failure might be propagation rather than a wrong value (Domain verification):
dig +short TXT <selector>._domainkey.yourdomain.com
dig @1.1.1.1 TXT <selector>._domainkey.yourdomain.com
If dig matches what the dashboard asked for, wait on propagation. If it returns nothing or a different value, fix DNS—do not keep regenerating keys hoping the UI will “refresh.”
Common DNS mistakes that break DKIM for apps
- Copying a blog’s sample
p=key instead of the per-domain key fromPOST /domains/ the dashboard. Keys are generated server-side per domain (API: Domains). - Double-appending the zone in relative DNS UIs (
selector._domainkey.example.com.example.com). - Removing the old DKIM TXT during rotation before the new selector is confirmed.
- Assuming root SPF covers a return-path subdomain—it does not; the subdomain needs its own SPF TXT plus MX (API: Domains).
- Publishing two SPF records while debugging “auth.” Two
v=spf1TXT records are a permanent fail; merge includes (Domain verification). - Treating verification as one-and-done. Records get deleted in DNS migrations; Rovela warns when a verified domain drifts, and
domain.updatedcan fire when settings change (Domain verification). - Sending OTP volume from an unverified or misaligned domain “just to test.” Rovela refuses unverified
fromdomains with400(Quick Start); other stacks may accept and burn reputation silently.
Domain authentication before OTP / receipt volume
Warm-up and list hygiene matter, but for transactional traffic the first filter is authentication:
- Prove the domain before you automate signup codes and password resets.
- Prefer a sending subdomain for app mail so transactional reputation stays separate from marketing (Sending subdomain setup).
- Keep auth links boring: for magic links and resets, turn off click tracking so the URL in the email matches what the user opens (Why OTP emails go to spam).
- Use idempotent sends (
Idempotency-Key) so retries do not double-deliver codes once DNS is fixed and the API path is live (API: emails).
Authentication does not guarantee inbox placement by itself—but without DKIM (and aligned SPF/DMARC), scaling transactional volume mostly trains filters that you look like spoofed mail.
Rovela as the send layer once DNS is correct
After the domain verifies, Rovela is the HTTP send path—not a second DNS host you reconfigure per request:
- Add the domain; publish the records returned (including DKIM); verify (Quick Start, Domain verification, How to set up domain email).
- Create a
sending_accessAPI key for production workers (Authentication). - Send with
POST https://api.rovela.dev/emailsfrom a verifiedfromaddress:
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp-user_42-20261105" \
-d '{
"from": "Acme <no-reply@mail.yourdomain.com>",
"to": "user@example.com",
"subject": "Your Acme code",
"html": "<p>Your code is <b>482193</b>. It expires in 10 minutes.</p>",
"text": "Your code is 482193. It expires in 10 minutes."
}'
A 200 means accepted for delivery, not “already in the inbox.” Watch GET /emails/:id, events, or webhooks for the rest (Quick Start).
This article does not invent dashboard click-paths beyond what docs describe (add domain → publish shown records → Verify; dig when stuck). Exact hostnames for include: and MX come from your domain’s API/dashboard response—never from a hardcoded blog string.
Pricing ceilings for Free/Pro live on Pricing. If you still relay over SMTP somewhere in the stack, port choice is covered in SMTP ports 25 / 465 / 587—most new app code should prefer the HTTP API once the domain is verified.
Checklist
-
Domain added; DKIM TXT published from your returned records (selector +
p=) - Relative DNS names not double-suffixed; TXT value not quote-corrupted
-
Single merged SPF; DMARC present (start
p=noneunless you already enforce stricter) - Return-path subdomain MX + SPF published when the API requires them
-
digconfirms DKIM before you blame the send API -
Verify succeeds; then send OTP/receipts with
Idempotency-Keyfrom a verifiedfrom
DKIM is the signature your transactional API cannot fake in application code. Publish the public key correctly, keep rotation additive, align SPF/DMARC at the domain layer, then let the send API do what it is for: deliver signed mail.