> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.slotflow.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.slotflow.dev/_mcp/server.

# AI Sales Agent

> Build an AI SDR that autonomously qualifies leads and books demos with your sales team.

This guide walks through building an AI SDR (Sales Development Representative) that qualifies inbound leads and books demo calls with your sales reps — fully autonomously.

## The problem

Your AI agent can research prospects, craft emails, and qualify leads. But when it's time to book a demo, the workflow breaks. The agent drops a Calendly link, the lead ignores it, and the opportunity goes cold.

With Slotflow, your agent books the demo directly — no links, no friction, no dropped leads.

## Architecture

```
Inbound Lead → AI SDR Agent → Qualifies lead
                            → Calls Slotflow GET /slots
                            → Presents times to lead
                            → Calls Slotflow POST /bookings
                            → Webhook fires → CRM updated
```

## Setup

### 1. Create your sales reps

Create a human for each sales rep who takes demos:

```javascript
const reps = [
  { name: "Sarah Chen", email: "sarah@acme.com", role: "ae_enterprise", timezone: "America/New_York" },
  { name: "Marcus Johnson", email: "marcus@acme.com", role: "ae_midmarket", timezone: "America/Chicago" },
];

for (const rep of reps) {
  const response = await fetch("https://api.slotflow.dev/v1/humans", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(rep),
  });
  const human = await response.json();
  console.log(`Created ${human.name}: ${human.id}`);
}
```

### 2. Set availability for each rep

```javascript
async function setAvailability(humanId) {
  await fetch(`https://api.slotflow.dev/v1/humans/${humanId}/availability`, {
    method: "PUT",
    headers: {
      "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      working_days: [1, 2, 3, 4, 5],  // Monday-Friday
      work_start: "09:00",
      work_end: "17:00",
      meeting_durations: [30, 60],      // 30-min intro, 60-min deep dive
    }),
  });
}
```

### 3. Block recurring meetings

Sales reps have team standups and pipeline reviews. Block those times:

```javascript
// Block the Tuesday 2-3pm team standup for Sarah
await fetch(`https://api.slotflow.dev/v1/humans/${sarahId}/overrides`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "block",
    title: "Team standup",
    all_day: false,
    start_date: "2026-03-01",
    start_time: "14:00",
    end_time: "15:00",
    recurrence: { freq: "weekly", days: [2] },  // Every Tuesday
  }),
});
```

### 4. Register a webhook

Get notified when bookings are confirmed so you can update your CRM:

```javascript
await fetch("https://api.slotflow.dev/v1/webhooks", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://your-agent.com/webhooks/slotflow",
    events: ["booking.confirmed", "booking.cancelled"],
  }),
});
```

## Agent booking flow

This is the core function your AI agent calls when it's ready to book a demo:

```javascript
async function bookDemo({ leadId, conversationId, leadName, leadEmail, repId }) {
  const API_KEY = process.env.SLOTFLOW_API_KEY;
  const BASE = "https://api.slotflow.dev/v1";
  const headers = {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  };

  // Step 1: Get available slots for the next 5 business days
  const today = new Date();
  const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
  const dateFrom = today.toISOString().split("T")[0];
  const dateTo = nextWeek.toISOString().split("T")[0];

  const slotsRes = await fetch(
    `${BASE}/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=30`,
    { headers }
  );
  const { slots, timezone } = await slotsRes.json();

  if (slots.length === 0) {
    return { success: false, reason: "No available slots this week" };
  }

  // Step 2: Pick the best slot (your agent's logic here)
  // For example: earliest available, or match lead's timezone preference
  const selectedSlot = slots[0];

  // Step 3: Book it with metadata for workflow tracking
  const bookingRes = await fetch(`${BASE}/bookings`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      human_id: repId,
      starts_at: selectedSlot.starts_at,
      duration: 30,
      attendee_name: leadName,
      attendee_email: leadEmail,
      metadata: {
        lead_id: leadId,
        conversation_id: conversationId,
        source: "ai_sdr",
        qualified_at: new Date().toISOString(),
      },
    }),
  });

  // Step 4: Handle the response
  if (bookingRes.status === 201) {
    const booking = await bookingRes.json();
    return { success: true, booking };
  }

  if (bookingRes.status === 409) {
    // Slot was taken between query and booking — retry with next slot
    return bookDemo({ leadId, conversationId, leadName, leadEmail, repId });
  }

  const error = await bookingRes.json();
  return { success: false, reason: error.error.message };
}
```

## Handling race conditions

When multiple agents are booking simultaneously, a slot might get taken between your `GET /slots` call and your `POST /bookings` call. Slotflow returns `409 SLOT_UNAVAILABLE` when this happens.

Build retry logic into your agent:

```javascript
async function bookWithRetry({ repId, duration, leadName, leadEmail, metadata, maxRetries = 3 }) {
  const headers = {
    "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
    "Content-Type": "application/json",
  };

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    // Fresh slot query on each attempt
    const dateFrom = new Date().toISOString().split("T")[0];
    const dateTo = new Date(Date.now() + 7 * 86400000).toISOString().split("T")[0];

    const { slots } = await fetch(
      `https://api.slotflow.dev/v1/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=${duration}`,
      { headers }
    ).then(r => r.json());

    if (slots.length === 0) break;

    const res = await fetch("https://api.slotflow.dev/v1/bookings", {
      method: "POST",
      headers,
      body: JSON.stringify({
        human_id: repId,
        starts_at: slots[attempt % slots.length].starts_at,  // Try different slots
        duration,
        attendee_name: leadName,
        attendee_email: leadEmail,
        metadata,
      }),
    });

    if (res.status === 201) return await res.json();
    if (res.status !== 409) break;  // Only retry on slot conflicts
  }

  return null;  // All retries exhausted
}
```

## Webhook handler

When a booking is confirmed, Slotflow POSTs to your webhook URL. Use this to update your CRM:

```javascript
app.post("/webhooks/slotflow", (req, res) => {
  const { event, data } = req.body;

  if (event === "booking.confirmed") {
    // Update CRM with the booked demo
    crm.updateLead(data.metadata.lead_id, {
      status: "demo_scheduled",
      demo_time: data.starts_at,
      demo_rep: data.human_id,
      booking_id: data.id,
    });
  }

  if (event === "booking.cancelled") {
    // Re-queue lead for rebooking
    crm.updateLead(data.metadata.lead_id, {
      status: "needs_rebooking",
    });
  }

  res.sendStatus(200);  // Always respond 200 quickly
});
```

## Multi-rep routing

If you have multiple sales reps, your agent can find the first available one:

```javascript
async function findFirstAvailableRep(repIds, dateFrom, dateTo, duration) {
  const headers = { "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}` };

  for (const repId of repIds) {
    const { slots } = await fetch(
      `https://api.slotflow.dev/v1/humans/${repId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=${duration}`,
      { headers }
    ).then(r => r.json());

    if (slots.length > 0) {
      return { repId, slots };
    }
  }

  return null;  // No reps available
}
```

## Key takeaways

* **Use metadata** to pass lead context through the booking → your webhook handler can update your CRM with all the context it needs
* **Handle 409 errors** with retry logic — race conditions are expected in high-volume environments
* **Block recurring meetings** with schedule overrides so your agent never double-books internal time
* **Query slots fresh** before each booking attempt to minimize conflicts