Verify That an Email Came From an AI Agent

MultiMail signs every outbound agent email with an ECDSA identity header. Decode the X-MultiMail-Identity header to confirm agent origin, model metadata, and disclosure state — without relying on sender logs.


Why this matters

When an AI agent sends email on behalf of a user or organization, downstream systems, security teams, and recipients have no reliable way to confirm the message is authentic. Application logs can be tampered with. Claimed sender identities can be spoofed. There is no standard mechanism to cryptographically bind an email to the specific AI agent that generated it, the model version it ran, or the oversight mode it operated under at send time. This gap creates audit failures, compliance exposure under the EU AI Act's transparency requirements, and opens the door to spoofed automation masquerading as legitimate agent output.


How MultiMail solves this

MultiMail attaches a signed X-MultiMail-Identity header to every email dispatched through the API. The header contains a base64url-encoded JSON payload — agent ID, model metadata, tenant ID, oversight mode, disclosure state, and a timestamp — along with an ECDSA signature using the P-256 curve. The signing key is tenant-scoped and rotatable. MultiMail publishes the corresponding verification public key at a well-known endpoint so any recipient system can independently verify the signature without calling back into your application. Verification is purely cryptographic: no trust is placed in the sending application's claims alone.

1

Retrieve the email and its headers

Call GET /v1/emails/{email_id} to fetch the full email record. The response includes a `headers` object containing X-MultiMail-Identity alongside standard fields like Message-ID and Date. For inbound emails that need verification, the same header is preserved when MultiMail receives and stores the message.

2

Decode the identity payload

The X-MultiMail-Identity header value is a dot-separated structure: base64url(payload).base64url(signature). Decode the payload to a JSON object containing fields: agent_id, model_id, model_provider, tenant_id, oversight_mode, ai_disclosure, mailbox, sent_at, and nonce. These fields describe the exact agent state at send time.

3

Fetch the tenant public key

Retrieve the signing public key from GET /v1/identity/public-key?tenant_id={tenant_id}. The endpoint returns a JWK object with the P-256 public key. Keys are cached with a 24-hour TTL on the verifier side to avoid per-message roundtrips. Key rotation events are published via webhook so verifiers can invalidate their cache immediately.

4

Verify the ECDSA signature

Reconstruct the signed message as base64url(payload) and verify the signature bytes against the public key using ES256 (ECDSA with P-256 and SHA-256). A valid signature confirms the payload has not been tampered with and was produced by MultiMail's signing infrastructure using the tenant's key at the stated timestamp.

5

Check agent metadata against expected values

After signature verification, assert that the decoded fields match your policy: agent_id matches the expected agent, oversight_mode is not `autonomous` if your policy requires human review, ai_disclosure is true if EU AI Act disclosure is required, and sent_at falls within an acceptable time window to prevent replay attacks.

6

Log the verification result

Write the verification outcome — pass/fail, agent_id, model_id, oversight_mode, and timestamp — to your audit log. This record satisfies EU AI Act Article 13 transparency requirements and provides evidence for security incident investigations. MultiMail also stores a verification-ready receipt accessible via GET /v1/emails/{email_id}/identity-receipt.


Implementation

Fetch email headers via REST API
python
import httpx
import base64
import json

MM_API_KEY = "$MULTIMAIL_API_KEY"
EMAIL_ID = "em_01HZ8QXYZ1234567890"

response = httpx.get(
    f"https://api.multimail.dev/v1/emails/{EMAIL_ID}",
    headers={"Authorization": f"Bearer {MM_API_KEY}"},
)
response.raise_for_status()
email = response.json()

identity_header = email["headers"].get("X-MultiMail-Identity")
if not identity_header:
    raise ValueError("No X-MultiMail-Identity header found — message was not sent via MultiMail agent")

payload_b64, signature_b64 = identity_header.split(".")
payload = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))

print("Agent ID:", payload["agent_id"])
print("Model:", payload["model_id"], "(", payload["model_provider"], ")")
print("Oversight mode:", payload["oversight_mode"])
print("AI disclosure:", payload["ai_disclosure"])
print("Sent at:", payload["sent_at"])

Retrieve a sent email and extract its X-MultiMail-Identity header using the MultiMail REST API.

Verify the ECDSA signature
python
import httpx
import base64
import json
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidSignature
from jwt.algorithms import ECAlgorithm

MM_API_KEY = "$MULTIMAIL_API_KEY"

def verify_identity_header(identity_header: str, tenant_id: str) -> dict:
    payload_b64, signature_b64 = identity_header.split(".")

    "cm"># Fetch tenant public key (JWK format)
    jwk_response = httpx.get(
        f"https://api.multimail.dev/v1/identity/public-key",
        params={"tenant_id": tenant_id},
        headers={"Authorization": f"Bearer {MM_API_KEY}"},
    )
    jwk_response.raise_for_status()
    jwk = jwk_response.json()

    "cm"># Load public key from JWK
    public_key = ECAlgorithm.from_jwk(json.dumps(jwk))

    "cm"># Verify signature over the raw payload bytes
    signed_message = payload_b64.encode("ascii")
    signature = base64.urlsafe_b64decode(signature_b64 + "==")

    try:
        public_key.verify(signature, signed_message, ECDSA(SHA256()))
    except InvalidSignature:
        raise ValueError("X-MultiMail-Identity signature is INVALID — message may be tampered or spoofed")

    payload = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))
    return payload


"cm"># Usage
payload = verify_identity_header(identity_header, tenant_id=payload["tenant_id"])
print("Signature VALID")
print(json.dumps(payload, indent=2))

Fetch the tenant public key from MultiMail and verify the P-256 signature in the identity header.

Full verification pipeline in TypeScript
typescript
import { createPublicKey, verify } from 'crypto';

const MM_API_KEY = process.env.MM_API_KEY!;
const BASE_URL = 'https://api.multimail.dev';

interface IdentityPayload {
  agent_id: string;
  model_id: string;
  model_provider: string;
  tenant_id: string;
  oversight_mode: 'read_only' | 'gated_all' | 'gated_send' | 'monitored' | 'autonomous';
  ai_disclosure: boolean;
  mailbox: string;
  sent_at: string;
  nonce: string;
}

async function verifyAgentEmail(emailId: string): Promise<{ valid: boolean; payload: IdentityPayload }> {
  const emailRes = await fetch(`${BASE_URL}/v1/emails/${emailId}`, {
    headers: { Authorization: `Bearer ${MM_API_KEY}` },
  });
  if (!emailRes.ok) throw new Error(`Failed to fetch email: ${emailRes.status}`);
  const email = await emailRes.json();

  const identityHeader: string | undefined = email.headers?.['X-MultiMail-Identity'];
  if (!identityHeader) throw new Error('Missing X-MultiMail-Identity header');

  const [payloadB64, signatureB64] = identityHeader.split('.');
  const payload: IdentityPayload = JSON.parse(
    Buffer.from(payloadB64, 'base64url').toString('utf8')
  );

  "cm">// Fetch public key for this tenant
  const keyRes = await fetch(
    `${BASE_URL}/v1/identity/public-key?tenant_id=${payload.tenant_id}`,
    { headers: { Authorization: `Bearer ${MM_API_KEY}` } }
  );
  if (!keyRes.ok) throw new Error(`Failed to fetch public key: ${keyRes.status}`);
  const jwk = await keyRes.json();

  const publicKey = createPublicKey({ key: jwk, format: 'jwk' });
  const signatureBytes = Buffer.from(signatureB64, 'base64url');
  const signedMessage = Buffer.from(payloadB64, 'ascii');

  const valid = verify('sha256', signedMessage, publicKey, signatureBytes);
  if (!valid) throw new Error('Signature verification failed');

  "cm">// Policy assertions
  if (!payload.ai_disclosure) {
    throw new Error(`EU AI Act violation: ai_disclosure is false for agent ${payload.agent_id}`);
  }

  const sentAt = new Date(payload.sent_at).getTime();
  const ageMs = Date.now() - sentAt;
  if (ageMs > 5 * 60 * 1000) {
    console.warn(`Identity header is ${Math.round(ageMs / 1000)}s old — check for replay`);
  }

  return { valid: true, payload };
}

"cm">// Audit log
const { valid, payload } = await verifyAgentEmail('em_01HZ8QXYZ1234567890');
console.log(JSON.stringify({
  event: 'identity_verification',
  result: valid ? 'PASS' : 'FAIL',
  agent_id: payload.agent_id,
  model_id: payload.model_id,
  oversight_mode: payload.oversight_mode,
  ai_disclosure: payload.ai_disclosure,
  verified_at: new Date().toISOString(),
}));

End-to-end verification: fetch email, decode header, verify signature, assert policy, and emit audit log entry.

Verify via MCP tool in Claude Desktop
text
// In Claude Desktop or any MCP client connected to MultiMail:

// Step 1: Read the email and inspect headers
read_email({
  email_id: "em_01HZ8QXYZ1234567890"
})
// Returns: { subject, from, body, headers: { "X-MultiMail-Identity": "...", ... }, agent_metadata: { agent_id, model_id, oversight_mode, ai_disclosure } }

// Step 2: If the email is in the pending/approval queue, decide on it after verifying identity
decide_email({
  email_id: "em_01HZ8QXYZ1234567890",
  decision: "approve",
  reason: "Identity header verified — agent_id matches expected automation, ai_disclosure confirmed"
})

// The MCP server surfaces agent_metadata as a first-class field so you do not need
// to manually decode the X-MultiMail-Identity header in conversational workflows.
// For programmatic verification pipelines, use the REST API approach above.

Use the MultiMail MCP server's read_email tool to fetch the email and inspect identity metadata directly in an MCP-compatible client.

Retrieve the identity receipt
bash
"cm"># GET /v1/emails/{email_id}/identity-receipt returns a signed JSON receipt
"cm"># containing the identity payload, the verification public key fingerprint,
"cm"># and a MultiMail-issued timestamp. Safe to store in audit logs as-is.

curl -s \
  -H "Authorization: Bearer $MM_API_KEY" \
  "https://api.multimail.dev/v1/emails/em_01HZ8QXYZ1234567890/identity-receipt" \
  | jq &"cm">#039;{
      agent_id: .payload.agent_id,
      model_id: .payload.model_id,
      oversight_mode: .payload.oversight_mode,
      ai_disclosure: .payload.ai_disclosure,
      key_fingerprint: .key_fingerprint,
      receipt_issued_at: .issued_at
    }&"cm">#039;

"cm"># Example output:
"cm"># {
"cm">#   "agent_id": "agt_support_responder_v2",
"cm">#   "model_id": "claude-sonnet-4-6",
"cm">#   "oversight_mode": "monitored",
"cm">#   "ai_disclosure": true,
"cm">#   "key_fingerprint": "SHA256:k3Fg9...",
"cm">#   "receipt_issued_at": "2026-04-19T14:22:31Z"
"cm"># }

Fetch MultiMail's stored verification receipt for an email — useful when you need a third-party-readable audit artifact.


What you get

Cryptographic proof, not log trust

The ECDSA signature in X-MultiMail-Identity is verifiable by any party with the public key. You do not need to query the sending application's logs or database to confirm authenticity — the header is self-contained evidence.

EU AI Act Article 13 transparency support

The identity payload includes ai_disclosure state, model_id, and model_provider, giving recipients the information required under EU AI Act transparency obligations for automated decision-making systems. The identity receipt provides a storable audit artifact.

Replay attack detection

Each identity header includes a nonce and sent_at timestamp. Verifiers can reject headers outside an acceptable time window and track nonces to prevent replayed headers from being accepted as fresh verification evidence.

Key rotation without downtime

Tenant signing keys can be rotated via the API without interrupting email sending. MultiMail issues key rotation webhooks so verifier caches can invalidate immediately rather than serving stale public keys for the full TTL period.

Works across all oversight modes

Identity headers are attached regardless of whether the email was sent under gated_all, monitored, or autonomous oversight. Security teams can verify not just that an agent sent a message but what constraints it was operating under at send time.


Recommended oversight mode

Recommended
monitored
Verification workflows are typically triggered by inbound events — a recipient's security system checking an email it received, or an audit pipeline reviewing sent messages. The verifying agent needs read access to emails and identity receipts but does not need to gate or block sending. Monitored mode gives the agent autonomous read access while keeping a human-visible trail of every verification query it performs, which is the appropriate posture for a security-adjacent automation reading sensitive message metadata.

Common questions

What algorithm does MultiMail use for the identity signature?
MultiMail uses ECDSA with the P-256 curve and SHA-256 (ES256 in JWA terminology). The signature is computed over the base64url-encoded payload string, not the decoded JSON bytes. The public key is served as a JWK object from GET /v1/identity/public-key.
Can an email lack the X-MultiMail-Identity header?
Yes — emails sent directly via SMTP relay or inbound emails received from external senders will not carry this header. Your verification logic should treat a missing header as unverified, not as invalid. Only emails dispatched through the MultiMail send_email or reply_email API endpoints carry a signed identity header.
How long are signing keys valid before rotation?
Keys do not expire automatically. Rotation is initiated explicitly via POST /v1/identity/rotate-key. After rotation, the old key remains valid for a 48-hour overlap window so in-flight verification requests against recently-sent emails continue to resolve. The rotation webhook fires immediately when a new key is activated.
Is the X-MultiMail-Identity header preserved if the email is forwarded?
Standard email forwarding strips or rewrites many headers. The header will be present in the original SMTP envelope as received by the first recipient, but may not survive subsequent forwards. For audit purposes, the identity receipt at GET /v1/emails/{email_id}/identity-receipt is more reliable than relying on header propagation through mail chains.
Does verification require calling the MultiMail API, or can it be done offline?
Signature verification itself is fully offline once you have the public key. The only network call required is fetching the JWK from GET /v1/identity/public-key, which you can cache. The identity receipt endpoint is optional — it is a convenience artifact, not required for cryptographic verification.
What does the ai_disclosure field mean and when is it false?
ai_disclosure is true when the email's body or subject includes a machine-readable or human-readable disclosure that the message was AI-generated, as required by the EU AI Act for automated communications. It is false if the agent was configured to suppress disclosure — which your verification policy may want to flag as a compliance issue depending on your jurisdiction and use case.
Can I verify the identity of an email using the Python SDK?
The multimail-sdk does not currently include a built-in verify_identity helper. Use the REST API directly as shown in the code samples above. The SDK's client.emails.get(email_id) method returns the full headers dict including X-MultiMail-Identity, so you can extract and verify the header using the cryptography library as shown.

Explore more use cases

The only agent email with a verifiable sender

Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 50-tool MCP server. Formally verified in Lean 4.