Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Set Up DKIM (Sign, Verify and Rotate Keys for Outbound Email)

Generate a key, publish the selector, prove a signature passes and fails, rotate without dropping mail, then verify the list you send to.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

DKIM proves that a message was signed by a domain you control and was not changed in transit. You need three things: a private key on the sender, a public key in DNS, and a signature that names both. This guide builds all three, tests them, and ends with the part most DKIM guides skip: checking the list you send to.

TL;DR

  • DKIM takes about 45 minutes to set up and test. DNS changes can take up to 48 hours to show on a hosted provider.
  • You generate a 2048-bit RSA key, publish it as a TXT record under a selector, sign a test message, and prove that an edited message fails.
  • It spends 0 LeadOcean records. The audience check at the end uses a free count.

Prerequisites

  • Edit access to the DNS zone of your sending domain.
  • A shell with openssl and dig, and Python 3.5 or later for the test.
  • A LeadOcean API key in $LEADOCEAN_API_KEY for Step 7. The free plan covers it.
  • A list of every system that sends mail as your domain: your mailbox provider, your sequencer, your help desk, your product.

Use placeholders as you go. acme.com stands for your domain and lo2026 for your selector.

Step 1: Pick the signing domain and a selector per sender

Each sending system gets its own selector. A selector is a label that tells the receiver which public key to fetch.

The receiver builds the DNS name from two tags in the signature: s= (selector) and d= (signing domain). The name is always <selector>._domainkey.<domain>.

text
Sender             d=            s=
Mailboxes          acme.com      google
Sequencer          acme.com      seq2026
Product mail       mail.acme.com app2026

Keep d= on your own domain. A signature from your vendor's domain can pass DKIM and still fail DMARC alignment, because DMARC compares d= with the visible From address.

One key per sender limits the damage when a key leaks or a vendor leaves. Selectors are public, so skip anything secret in the name.

Step 2: Generate a 2048-bit key pair

Use 2048 bits. RFC 8301 says signers MUST use RSA keys of at least 1024 bits and SHOULD use 2048 or more. Google asks for 1024 or longer when you send to personal Gmail accounts and recommends 2048 (Google, September 2026).

If your provider generates the key for you, skip to Step 3 and paste what it gives you. For a self-managed sender, run this on a secured machine:

bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out lo2026.private
openssl pkey -in lo2026.private -pubout -outform DER | openssl base64 -A

The second command prints the public key as one base64 line. That line is the p= value in DNS. The private file never leaves the signing system, and it stays out of git and out of unprotected backups.

text
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcqda/FH20YLza+B...

This is the start of a throwaway key made for this guide on 1 October 2026. Yours will differ.

Step 3: Publish the selector as a TXT record

Create one TXT record at lo2026._domainkey.acme.com with this value:

text
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcqda/FH20YLza+B...

A 2048-bit key makes the value about 400 characters. DNS limits each quoted string in a TXT record to 255 octets (RFC 1035, September 2026), so the value goes in as two strings. Most DNS panels split it for you. If yours does not, close the first quote after 255 characters and open a second.

Some providers hand you CNAME records instead of a key. Publish them exactly as shown. The provider then holds the key behind the CNAME.

Wait for propagation, then query the record:

bash
dig +short TXT lo2026._domainkey.acme.com

You should get the v=DKIM1 string back. Two cautions apply on hosted mail:

  • Google Workspace makes you wait 24 to 72 hours after turning on Gmail before you can get a key. A new key can then take up to 48 hours to start working (Google, September 2026).
  • An empty answer means the name is wrong. Check for a doubled domain, such as lo2026._domainkey.acme.com.acme.com, which DNS panels that append the zone name produce.

Step 4: Sign a test message and verify it

Test the whole chain before you trust a real send. The dkimpy package ships two helper programs, dkimsign and dkimverify, and a Python API (PyPI, September 2026).

bash
python3 -m venv v && ./v/bin/pip install dkimpy

cat > msg.eml <<'MAIL'
From: Jane Doe <jane@acme.com>
To: you@example.net
Subject: Quick question
Date: Thu, 01 Oct 2026 09:00:00 +0000
Message-ID: <demo-001@acme.com>

Hello from a DKIM test.
MAIL

./v/bin/dkimsign --hcanon relaxed --bcanon relaxed \
  lo2026 acme.com lo2026.private < msg.eml > signed.eml

dkimsign takes the selector, the domain and the private key file, and prints the signed message. To verify against the live DNS record instead of a local one, run ./v/bin/dkimverify < signed.eml after Step 3 has propagated.

The check below uses the Python API with a stand-in DNS answer, so you can prove the logic before DNS is ready. Run it three times: on the signed file, on a copy with an edited subject, and on a copy with an edited body.

python
import dkim, sys
PUB = "v=DKIM1; k=rsa; p=<paste the base64 line from Step 2>"
def dns(name, timeout=5):
    return PUB.encode() if name.decode().startswith("lo2026._domainkey.acme.com") else None
print(sys.argv[1], dkim.verify(open(sys.argv[1], "rb").read(), dnsfunc=dns))

Step 5: Read the result and check alignment

Any change to a signed header or to the body breaks the signature. That includes a gateway that adds a footer or rewrites a link.

This is the signature the test produced. The tag h= lists the signed headers, and bh= is the body hash.

text
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=acme.com;
 i=@acme.com; q=dns/txt; s=lo2026; t=1790906690;
 h=from : to : subject : date : message-id : from;
 bh=ZRNFjhzW4tv+uJesNrRw4E4LIahVIT494hLO4zUkUn4=;
 b=D5Eh7/ihmu16VZL+ynUUaiFWwLKrVua5XD5I1jGAejHb/pC2yXoJJGx3L0K...

Then send a real message to a mailbox you own and open the raw source. Look for dkim=pass in the Authentication-Results header, and confirm that the domain next to it is yours. Use a=rsa-sha256. RFC 8301 forbids rsa-sha1 for signing.

Step 6: Rotate keys without dropping mail

Rotate by overlapping two selectors, not by editing one in place. RFC 6376 describes the pattern (RFC 6376, September 2026). Publish the new key first and switch the signers to the new private key. Remove the old public key only after a transition period, because mail may still be in transit.

  1. Generate a new pair under a new selector, such as lo2026b, and publish its TXT record.
  2. Confirm it resolves with dig +short TXT lo2026b._domainkey.acme.com.
  3. Point the signer at the new private key and selector. Send a test message and read s=.
  4. Leave the old record live for several days. We suggest seven, which is our choice and not a rule.
  5. Revoke the old key by publishing an empty p=, then delete the record when you are sure.

Rotate when someone with key access leaves, when a key is exposed, or on a schedule you set. Twice a year is a common pick.

This is what an empty p= looks like in a live query. It was run on 1 October 2026 against the example.com test domain, which answers every selector with a revoked key.

bash
$ dig +noall +answer TXT lo2026._domainkey.example.com
lo2026._domainkey.example.com. 300 IN TXT "v=DKIM1; p="

Step 7: Check the list before you send

DKIM proves who sent the mail. It does not make a bad list safe. Bounces and spam traps hurt a domain that is perfectly signed.

LeadOcean stores an email_status for each address. A sizing call is free. Send count=true as a query parameter with limit 1, and read meta.total.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jobLevel": ["VP"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["CA"],
    "emailStatus": ["verified"],
    "limit": 1
  }'

Run the same filters with different emailStatus arrays to see how much of an audience is safe to mail. The MCP tool leadocean_count_leads does the same job from Claude or any MCP client, and it is free. Read how to verify email addresses for the full method, and verify emails for Instantly, Smartlead and Lemlist for sender-side checks.

What you get

Two outputs. The first is the signature test from Step 4. It ran on 1 October 2026 with a throwaway key and the placeholder domain acme.com.

text
signed.eml    True
tampered.eml  False   (subject edited after signing)
body.eml      False   (one word in the body changed)

The second is a free count from the LeadOcean MCP tool leadocean_count_leads. It ran on 1 October 2026 for VP, Sales & Business Development, country CA, and spent 0 records.

FilterTotal
Default mailable (verified, catch_all_valid or catch_all)5,066
emailStatus verified only1,658
emailStatus risky, unknown or untested8,208

The default count is the audience worth sending to. The last row is the audience you would be guessing on. Send to verified addresses first and read email_status on every record before it goes into a sequence. LeadOcean does not refund or credit back bounced emails.

The sizing call through the REST API returns the response shape from the API reference: success, an empty data array, and a meta object with countOnly: true and total. meta.total is capped at 100,000.

Troubleshooting

ErrorCauseFix
dkim=fail (no key for signature)The selector or domain in s= and d= does not match the DNS nameRebuild the name as <s>._domainkey.<d> and run dig on it
dkim=fail (body hash did not verify)A gateway or footer changed the body after signingSign after the last system that edits the message
dkim=pass but DMARC failsd= is the vendor's domain, not yoursSet up custom signing so d= is your domain
401 from the LeadOcean APIMissing or wrong x-api-key headerCheck $LEADOCEAN_API_KEY and send it as x-api-key
429 from the LeadOcean APIAbove your plan's rate on one key (5 a second on Free, 200 on Agency)Slow the client and wait the Retry-After seconds
402 from the LeadOcean APIFree plan: the 1,000 one-off records are spent (counts never cost any)Check GET /v1/account, or move to Solo at $97 or Agency at $497 a month
Empty results for a countA bad enum value, such as vp for jobLevelUse exact strings: VP, not vp. Check leadocean_list_enum_values

FAQ

How many DKIM selectors do I need?

One per sending system, plus a spare while you rotate. A shared key across vendors makes revocation harder, since you cannot cut off one sender without breaking the others.

Should I use a 1024-bit or 2048-bit key?

Use 2048 bits when your DNS host allows it. RFC 8301 sets 1024 bits as the floor and recommends 2048 or more. Google accepts 1024 for personal Gmail accounts and recommends 2048.

Is DKIM enough for cold email?

No. Google requires DKIM, SPF and DMARC from senders above 5,000 messages a day to personal Gmail accounts, and DKIM or SPF from everyone else (Google, September 2026). Set up all three, then watch bounces.

Does LeadOcean sign or send email?

No. LeadOcean is a people and company data API with a hosted MCP server. It gives you the list and the email_status on each address. Your mailbox provider or sequencer does the signing.

Sign your mail, then fill the list with addresses you can trust

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →

See also how to verify an email address and LeadOcean pricing. More guides are in the blog.