daimon.email
Security

Idempotency

Safe retries, duplicate prevention, and agent restart handling

Overview

Idempotency ensures that retrying the same operation multiple times has the same effect as performing it once. This is critical for AI agents that may restart, retry failed requests, or recover from network issues.

Info

daimon.email supports idempotency on all write operations (POST, PUT, DELETE) via the client_id parameter. GET requests are naturally idempotent.

Why Idempotency Matters for Agents

AI agents face unique challenges that make idempotency essential:

Agent Restarts

Your agent crashes mid-operation and restarts. Without idempotency:

// Agent startup sequence (without idempotency):
const inbox = await client.inboxes.create({ username: 'support-bot' });
// Agent crashes here

// On restart, this creates a SECOND inbox:
const inbox = await client.inboxes.create({ username: 'support-bot' });
// Now you have support-bot@daimon.email and support-bot-1@daimon.email

With idempotency:

// Agent startup sequence (with idempotency):
const inbox = await client.inboxes.create({
  username: 'support-bot',
  client_id: 'agent-deployment-prod-1'  // Unique to this deployment
});
// Agent crashes here

// On restart, returns THE SAME inbox:
const inbox = await client.inboxes.create({
  username: 'support-bot',
  client_id: 'agent-deployment-prod-1'
});
// Returns existing inbox, no duplicate created

Network Failures

HTTP requests timeout, but you don't know if the server processed them:

try {
  await client.inboxes.send(inbox_id, message);
} catch (error) {
  if (error.code === 'NETWORK_TIMEOUT') {
    // Did the message send? You don't know!
    // Safe to retry with client_id:
    await client.inboxes.send(inbox_id, {
      ...message,
      client_id: 'send-msg-12345'
    });
    // If first attempt succeeded, this returns the existing message
    // If first attempt failed, this sends the message
  }
}

Rate Limit Retries

When backing off from rate limits, you need to avoid double-sends:

async function sendWithRetry(inbox_id, message) {
  const client_id = `send-${Date.now()}-${Math.random()}`;

  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await client.inboxes.send(inbox_id, {
        ...message,
        client_id  // Same ID across retries
      });
    } catch (error) {
      if (error.status === 429) {
        await sleep(error.retry_after * 1000);
        // Retry with same client_id - won't double-send
        continue;
      }
      throw error;
    }
  }
}

Supported Operations

Inbox Creation

Endpoint: POST /v1/inboxes

Idempotency key: client_id

Uniqueness constraint: (account_id, client_id)

// First call creates inbox
const inbox1 = await client.inboxes.create({
  username: 'agent-support',
  client_id: 'deployment-prod-inbox-1'
});
// Returns: { id: 'inbox_abc', address: 'agent-support@daimon.email', ... }

// Second call with same client_id returns existing inbox
const inbox2 = await client.inboxes.create({
  username: 'agent-support',  // Same or different, doesn't matter
  client_id: 'deployment-prod-inbox-1'  // Same client_id
});
// Returns: { id: 'inbox_abc', address: 'agent-support@daimon.email', ... }
// inbox1.id === inbox2.id (same inbox)
# First call creates inbox
inbox1 = client.inboxes.create({
    'username': 'agent-support',
    'client_id': 'deployment-prod-inbox-1'
})
# Returns: { 'id': 'inbox_abc', 'address': 'agent-support@daimon.email', ... }

# Second call with same client_id returns existing inbox
inbox2 = client.inboxes.create({
    'username': 'agent-support',  # Same or different, doesn't matter
    'client_id': 'deployment-prod-inbox-1'  # Same client_id
})
# Returns: { 'id': 'inbox_abc', 'address': 'agent-support@daimon.email', ... }
# inbox1['id'] == inbox2['id'] (same inbox)

Note

If client_id matches an existing inbox, the request returns 200 OK (not 201 Created). Check response.status to detect if an inbox was reused.

Message Sending

Endpoint: POST /v1/inboxes/{id}/send

Idempotency key: client_id

Uniqueness constraint: (inbox_id, client_id)

// First call sends message
const msg1 = await client.inboxes.send(inbox_id, {
  to: 'customer@example.com',
  subject: 'Order Confirmation',
  body: 'Your order #1234 has shipped.',
  client_id: 'order-1234-confirmation'
});
// Returns: { id: 'msg_xyz', status: 'queued', ... }

// Retry with same client_id (e.g., after network timeout)
const msg2 = await client.inboxes.send(inbox_id, {
  to: 'customer@example.com',
  subject: 'Order Confirmation',  // Same or different, doesn't matter
  body: 'Your order #1234 has shipped.',
  client_id: 'order-1234-confirmation'  // Same client_id
});
// Returns: { id: 'msg_xyz', status: 'sent', ... }
// msg1.id === msg2.id (same message, not a duplicate)
# First call sends message
msg1 = client.inboxes.send(inbox_id, {
    'to': 'customer@example.com',
    'subject': 'Order Confirmation',
    'body': 'Your order #1234 has shipped.',
    'client_id': 'order-1234-confirmation'
})
# Returns: { 'id': 'msg_xyz', 'status': 'queued', ... }

# Retry with same client_id (e.g., after network timeout)
msg2 = client.inboxes.send(inbox_id, {
    'to': 'customer@example.com',
    'subject': 'Order Confirmation',  # Same or different, doesn't matter
    'body': 'Your order #1234 has shipped.',
    'client_id': 'order-1234-confirmation'  # Same client_id
})
# Returns: { 'id': 'msg_xyz', 'status': 'sent', ... }
# msg1['id'] == msg2['id'] (same message, not a duplicate)

Webhook Creation

Endpoint: POST /v1/webhooks

Idempotency key: client_id

Uniqueness constraint: (account_id, client_id)

const webhook1 = await client.webhooks.create({
  url: 'https://agent.example.com/webhooks/daimon',
  events: ['message.received'],
  client_id: 'webhook-message-received-prod'
});

// Retry returns the same webhook
const webhook2 = await client.webhooks.create({
  url: 'https://agent.example.com/webhooks/daimon',
  events: ['message.received'],
  client_id: 'webhook-message-received-prod'
});
// webhook1.id === webhook2.id
webhook1 = client.webhooks.create({
    'url': 'https://agent.example.com/webhooks/daimon',
    'events': ['message.received'],
    'client_id': 'webhook-message-received-prod'
})

# Retry returns the same webhook
webhook2 = client.webhooks.create({
    'url': 'https://agent.example.com/webhooks/daimon',
    'events': ['message.received'],
    'client_id': 'webhook-message-received-prod'
})
# webhook1['id'] == webhook2['id']

Generating client_id Values

Deterministic IDs

For operations tied to external entities (orders, users, deployments):

// Order confirmation email
const client_id = `order-${orderId}-confirmation`;

// User signup verification
const client_id = `user-${userId}-signup-verification`;

// Deployment-specific inbox
const client_id = `${deploymentId}-inbox-primary`;

This ensures retries from the same external trigger use the same ID.

UUID-based IDs

For one-off operations without external context:

import { v4 as uuidv4 } from 'uuid';

// Generate once at the start of the operation
const client_id = `send-${uuidv4()}`;

// Store this ID if you might need to retry
await kvStore.set('last-send-client-id', client_id);

// Send with the stored ID
await client.inboxes.send(inbox_id, {
  ...message,
  client_id
});

Timestamp-based IDs

For debugging and traceability:

// Includes timestamp for log correlation
const client_id = `send-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

Warning

Never use just a timestamp or incrementing counter as client_id. These are not unique across restarts or parallel operations.

Response Codes

ScenarioStatus CodeBehavior
First creation (inbox, webhook)201 CreatedNew resource created
Idempotent retry (same client_id)200 OKExisting resource returned
First send (message)200 OKMessage queued/sent
Idempotent retry (message)200 OKExisting message returned (not re-sent)
const response = await fetch('https://api.daimon.email/v1/inboxes', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    username: 'agent-1',
    client_id: 'deploy-123-inbox'
  })
});

if (response.status === 201) {
  console.log('New inbox created');
} else if (response.status === 200) {
  console.log('Existing inbox returned (retry detected)');
}
response = requests.post(
    'https://api.daimon.email/v1/inboxes',
    headers={
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    },
    json={
        'username': 'agent-1',
        'client_id': 'deploy-123-inbox'
    }
)

if response.status_code == 201:
    print('New inbox created')
elif response.status_code == 200:
    print('Existing inbox returned (retry detected)')

Agent Patterns

Pattern: Persistent Deployment ID

Store a unique deployment ID and use it for all idempotency keys:

// On agent startup:
const DEPLOYMENT_ID = process.env.DEPLOYMENT_ID || `deploy-${Date.now()}`;

// Create inbox idempotently
const inbox = await client.inboxes.create({
  username: 'support-bot',
  client_id: `${DEPLOYMENT_ID}-inbox-primary`
});

// Create webhook idempotently
const webhook = await client.webhooks.create({
  url: 'https://agent.example.com/webhooks/daimon',
  events: ['message.received'],
  client_id: `${DEPLOYMENT_ID}-webhook-messages`
});

// Agent can restart and recover the same inbox/webhook

Pattern: Database-backed Idempotency

Store client IDs in your database to ensure they persist across restarts:

async function sendOrderConfirmation(orderId) {
  // Check if we already sent this
  let sendRecord = await db.findOne('email_sends', { order_id: orderId });

  if (!sendRecord) {
    // First attempt - generate client_id and store it
    sendRecord = await db.insert('email_sends', {
      order_id: orderId,
      client_id: `order-${orderId}-confirmation`,
      status: 'pending'
    });
  }

  // Send with stored client_id (safe to retry)
  const message = await client.inboxes.send(inbox_id, {
    to: order.customer_email,
    subject: `Order #${orderId} Confirmed`,
    body: `Your order has been confirmed.`,
    client_id: sendRecord.client_id
  });

  // Update record with message ID
  await db.update('email_sends', sendRecord.id, {
    message_id: message.id,
    status: 'sent'
  });
}

Pattern: Transactional Outbox

For guaranteed delivery with database transactions:

async function placeOrder(orderData) {
  const db = await beginTransaction();

  try {
    // Create order
    const order = await db.insert('orders', orderData);

    // Queue email in outbox (same transaction)
    await db.insert('email_outbox', {
      order_id: order.id,
      to: order.customer_email,
      subject: `Order #${order.id} Confirmed`,
      body: `Your order has been confirmed.`,
      client_id: `order-${order.id}-confirmation`,
      status: 'pending'
    });

    await db.commit();
  } catch (error) {
    await db.rollback();
    throw error;
  }
}

// Separate worker processes outbox
setInterval(async () => {
  const pending = await db.find('email_outbox', { status: 'pending' });

  for (const email of pending) {
    try {
      await client.inboxes.send(inbox_id, {
        to: email.to,
        subject: email.subject,
        body: email.body,
        client_id: email.client_id  // Ensures no duplicates
      });

      await db.update('email_outbox', email.id, { status: 'sent' });
    } catch (error) {
      // Retry on next pass
      console.error('Send failed:', error);
    }
  }
}, 10000);  // Every 10 seconds

Idempotency Window

daimon.email maintains idempotency for:

  • Inbox creation: Forever (once created, always returns same inbox)
  • Message sending: 24 hours (same client_id within 24 hours returns same message)
  • Webhook creation: Forever (same client_id always returns same webhook)

Note

After 24 hours, message client_id values can be reused. This is usually safe since retry windows are much shorter.

Limitations

Idempotency does not work if:

  • You omit client_id (each request creates new resource)
  • You use different client_id values for retries (each creates new resource)
  • You use the same client_id from different accounts (uniqueness is scoped per account)
// BAD: No client_id
const inbox1 = await client.inboxes.create({ username: 'agent' });
const inbox2 = await client.inboxes.create({ username: 'agent' });
// Creates TWO inboxes: agent@daimon.email and agent-1@daimon.email

// BAD: Different client_id on retry
const inbox1 = await client.inboxes.create({ username: 'agent', client_id: 'id-1' });
const inbox2 = await client.inboxes.create({ username: 'agent', client_id: 'id-2' });
// Creates TWO inboxes

// GOOD: Same client_id
const inbox1 = await client.inboxes.create({ username: 'agent', client_id: 'id-1' });
const inbox2 = await client.inboxes.create({ username: 'agent', client_id: 'id-1' });
// Returns the SAME inbox

Testing Idempotency

Verify your idempotency implementation:

import { describe, it, expect } from 'vitest';

describe('Idempotent inbox creation', () => {
  it('returns same inbox on retry', async () => {
    const client_id = 'test-idempotency-' + Date.now();

    const inbox1 = await client.inboxes.create({
      username: 'test-agent',
      client_id
    });

    const inbox2 = await client.inboxes.create({
      username: 'different-username',  // Doesn't matter
      client_id  // Same client_id
    });

    expect(inbox1.id).toBe(inbox2.id);
    expect(inbox1.address).toBe(inbox2.address);
  });

  it('creates different inboxes with different client_id', async () => {
    const inbox1 = await client.inboxes.create({
      username: 'test-agent',
      client_id: 'id-1'
    });

    const inbox2 = await client.inboxes.create({
      username: 'test-agent',
      client_id: 'id-2'
    });

    expect(inbox1.id).not.toBe(inbox2.id);
  });
});

Additional Resources