AgentPost
Best Practices

Idempotency

Design AI agent workflows that are safe to retry without causing duplicate actions

Idempotency

Idempotency means that performing the same operation multiple times produces the same result as performing it once. For AI agents that may retry failed requests, lose network connectivity, or process the same event twice, idempotent design prevents duplicate emails, duplicate inboxes, and data corruption.

Why Idempotency Matters for AI Agents

AI agents operate autonomously and frequently encounter transient failures. Without idempotent design:

  • A network timeout during inbox creation could result in duplicate inboxes when the agent retries
  • A webhook delivery replayed due to at-least-once delivery could trigger the same email reply twice
  • A crashed agent restarting could re-process events it already handled

AgentPost provides built-in idempotency patterns and tools to make your agent workflows retry-safe.

The client_id Pattern

AgentPost supports a client_id field on inbox and domain creation. When provided, the API guarantees that only one resource is created for a given client_id within your organization -- subsequent requests with the same client_id return the existing resource instead of creating a duplicate.

import AgentPost from '@agentpost/sdk';

const client = new AgentPost({ apiKey: 'ap_sk_live_your_key_here' });

// This is safe to call multiple times
// The second call returns the existing inbox instead of creating a duplicate
const inbox = await client.inboxes.create({
  email: 'support@agent-post.dev',
  name: 'Customer Support',
  client_id: 'support-inbox-v1', // Idempotency key
});

// Retry the same call after a network error -- no duplicate created
const sameInbox = await client.inboxes.create({
  email: 'support@agent-post.dev',
  name: 'Customer Support',
  client_id: 'support-inbox-v1',
});

console.log(inbox.id === sameInbox.id); // true
from agentpost import AgentPost

client = AgentPost(api_key="ap_sk_live_your_key_here")

# This is safe to call multiple times
inbox = client.inboxes.create(
    email="support@agent-post.dev",
    name="Customer Support",
    client_id="support-inbox-v1",  # Idempotency key
)

# Retry the same call after a network error -- no duplicate created
same_inbox = client.inboxes.create(
    email="support@agent-post.dev",
    name="Customer Support",
    client_id="support-inbox-v1",
)

assert inbox.id == same_inbox.id  # True
# First call creates the inbox
curl -X POST https://api.agent-post.dev/api/v1/inboxes \
  -H "Authorization: Bearer ap_sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "support@agent-post.dev",
    "name": "Customer Support",
    "client_id": "support-inbox-v1"
  }'

# Second call returns the existing inbox (HTTP 200, not 201)
curl -X POST https://api.agent-post.dev/api/v1/inboxes \
  -H "Authorization: Bearer ap_sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "support@agent-post.dev",
    "name": "Customer Support",
    "client_id": "support-inbox-v1"
  }'

Choosing client_id Values

Your client_id should be deterministic and meaningful:

PatternExampleUse Case
Purpose-basedsupport-inbox-v1One inbox per purpose
Customer-scopedcustomer-acmeco-inboxOne inbox per customer
Agent-scopedagent-billing-bot-inboxOne inbox per agent
Environment + purposeprod-support-inboxEnvironment-aware

Avoid random UUIDs as client_id -- they defeat the purpose of idempotency since each retry generates a new ID.

Handling Duplicate Webhook Deliveries

AgentPost provides at-least-once delivery for webhooks. Your webhook handler should deduplicate events using the event id:

// In-memory deduplication (use Redis for production)
const processedEvents = new Set<string>();

app.post('/webhooks/agentpost', async (c) => {
  const event = JSON.parse(await c.req.text());

  // Skip duplicate events
  if (processedEvents.has(event.id)) {
    return c.json({ received: true, duplicate: true });
  }

  processedEvents.add(event.id);

  // Process the event
  switch (event.type) {
    case 'message.received':
      await processIncomingEmail(event.data);
      break;
  }

  return c.json({ received: true });
});
# In-memory deduplication (use Redis for production)
processed_events = set()

@app.route("/webhooks/agentpost", methods=["POST"])
def handle_webhook():
    event = request.get_json()

    # Skip duplicate events
    if event["id"] in processed_events:
        return jsonify({"received": True, "duplicate": True})

    processed_events.add(event["id"])

    # Process the event
    if event["type"] == "message.received":
        process_incoming_email(event["data"])

    return jsonify({"received": True})
# Check if an event was already processed using a database query
# before processing it:
#
# SELECT 1 FROM processed_events WHERE event_id = 'evt_01JQ8X5K';
#
# If found, skip. If not found, process and insert:
# INSERT INTO processed_events (event_id, processed_at) VALUES ('evt_01JQ8X5K', NOW());

Production Deduplication Strategies

For production systems, use a persistent store for event tracking:

StrategyProsCons
Redis SETFast, automatic TTL expiryLost on restart without persistence
Database tableDurable, queryableAdds write latency per event
Bloom filterMemory-efficient for high volumesSmall false positive rate

A Redis SET with 24-hour TTL is typically the best balance of performance and reliability:

import { Redis } from 'ioredis';

const redis = new Redis();
const EVENT_TTL_SECONDS = 86400; // 24 hours

async function isDuplicate(eventId: string): Promise<boolean> {
  const result = await redis.set(
    `webhook:${eventId}`,
    '1',
    'EX',
    EVENT_TTL_SECONDS,
    'NX', // Only set if not exists
  );
  return result === null; // null = key already existed = duplicate
}

Idempotent Email Sending

Email sending is inherently non-idempotent -- each API call sends a new email. To prevent duplicate sends, track sent messages on your side:

async function replyToTicket(ticketId: string, inboxId: string, messageId: string) {
  // Check if we already replied to this ticket
  const alreadyReplied = await db.query(
    'SELECT 1 FROM agent_replies WHERE ticket_id = $1',
    [ticketId],
  );

  if (alreadyReplied.rows.length > 0) {
    console.log(`Already replied to ticket ${ticketId}, skipping`);
    return;
  }

  // Send the reply
  const reply = await client.messages.reply(inboxId, messageId, {
    text_body: 'Thanks for reaching out! We are looking into this.',
  });

  // Record that we replied
  await db.query(
    'INSERT INTO agent_replies (ticket_id, message_id, replied_at) VALUES ($1, $2, NOW())',
    [ticketId, reply.id],
  );
}
def reply_to_ticket(ticket_id: str, inbox_id: str, message_id: str):
    # Check if we already replied to this ticket
    cursor.execute(
        "SELECT 1 FROM agent_replies WHERE ticket_id = %s",
        (ticket_id,),
    )
    if cursor.fetchone():
        print(f"Already replied to ticket {ticket_id}, skipping")
        return

    # Send the reply
    reply = client.messages.reply(
        inbox_id,
        message_id,
        text_body="Thanks for reaching out! We are looking into this.",
    )

    # Record that we replied
    cursor.execute(
        "INSERT INTO agent_replies (ticket_id, message_id, replied_at) VALUES (%s, %s, NOW())",
        (ticket_id, reply.id),
    )
    db.commit()
# 1. Check if reply was already sent
ALREADY_REPLIED=$(psql -t -c "SELECT 1 FROM agent_replies WHERE ticket_id='TICKET-123'")

if [ -z "$ALREADY_REPLIED" ]; then
  # 2. Send the reply
  curl -X POST "https://api.agent-post.dev/api/v1/inboxes/inb_01JQ8X/messages/msg_01JQ8X/reply" \
    -H "Authorization: Bearer ap_sk_live_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{"text_body": "Thanks for reaching out!"}'

  # 3. Record the reply
  psql -c "INSERT INTO agent_replies (ticket_id, replied_at) VALUES ('TICKET-123', NOW())"
fi

Summary

PatternBuilt-inYour Responsibility
Inbox creationclient_idChoose deterministic IDs
Domain creationclient_idChoose deterministic IDs
Webhook deduplicationEvent id fieldTrack processed event IDs
Email sendingNoneTrack sent messages
Agent retriesNoneDesign idempotent workflows

On this page