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.emailWith 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 createdNetwork 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.idwebhook1 = 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
| Scenario | Status Code | Behavior |
|---|---|---|
| First creation (inbox, webhook) | 201 Created | New resource created |
| Idempotent retry (same client_id) | 200 OK | Existing resource returned |
| First send (message) | 200 OK | Message queued/sent |
| Idempotent retry (message) | 200 OK | Existing 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/webhookPattern: 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 secondsIdempotency Window
daimon.email maintains idempotency for:
- Inbox creation: Forever (once created, always returns same inbox)
- Message sending: 24 hours (same
client_idwithin 24 hours returns same message) - Webhook creation: Forever (same
client_idalways 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_idvalues for retries (each creates new resource) - You use the same
client_idfrom 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 inboxTesting 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);
});
});