Skip to main content
Technology & EngineeringScheduling Services296 lines

Doodle

Doodle API integration for group scheduling polls, 1:1 booking pages, and meeting coordination

Quick Summary23 lines
You are an expert in integrating the Doodle API for group scheduling polls and meeting coordination.

## Key Points

- Enable the "if need be" option on polls to capture soft availability; this significantly increases the chance of finding a time that works for large groups.
- Set a deadline on polls to prevent them from staying open indefinitely; automate a reminder or auto-close workflow as the deadline approaches.
- Use the hidden poll setting for sensitive scheduling (e.g., interviews) so participants cannot see each other's responses.
- Distribute the `participantUrl` (not the `adminUrl`) to participants; the admin URL grants edit and close permissions.
- For recurring group meetings, create polls programmatically with a template of time options based on historical preferences.
- Doodle's free tier has API rate limits and restricted access; premium plans are required for full API functionality including webhooks and booking pages.
- Poll options are positional — when analyzing preferences, match by index rather than by option ID, since the preferences array aligns with the options array order.
- Closing a poll is irreversible; once finalized, the poll cannot be reopened. Always confirm with the organizer before programmatically closing.
- The API does not natively send calendar invites upon finalization; your integration must generate and send ICS files or create calendar events separately.
- Participant names are free-text and not deduplicated; the same person can submit multiple responses under different names unless you enforce email-based identity through poll settings.

## Quick Example

```bash
# Obtain an API key from Doodle's developer portal
export DOODLE_API_KEY="your-api-key"
```
skilldb get scheduling-services-skills/DoodleFull skill: 296 lines
Paste into your CLAUDE.md or agent config

Doodle — Scheduling Integration

You are an expert in integrating the Doodle API for group scheduling polls and meeting coordination.

Core Philosophy

Overview

Doodle is a scheduling platform best known for group polling — finding a time that works for multiple participants. Beyond polls, Doodle offers 1:1 Booking Pages for direct appointment scheduling. The REST API enables programmatic creation and management of polls, retrieval of participant responses, and automation of meeting finalization. Doodle is strongest when you need consensus-based scheduling among groups where not everyone shares the same calendar system.

Setup & Configuration

API Key

# Obtain an API key from Doodle's developer portal
export DOODLE_API_KEY="your-api-key"

Client Setup

import axios, { AxiosInstance } from "axios";

function createDoodleClient(apiKey: string): AxiosInstance {
  return axios.create({
    baseURL: "https://doodle.com/api/v2.0",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
  });
}

const doodle = createDoodleClient(process.env.DOODLE_API_KEY!);

Core Patterns

Creating a Group Poll

interface PollOption {
  start: string; // ISO 8601 datetime
  end: string;
}

interface CreatePollParams {
  title: string;
  description?: string;
  options: PollOption[];
  settings?: {
    deadline?: string;
    multiDay?: boolean;
    ifNeedBe?: boolean; // allow "if need be" responses
    hidden?: boolean; // hide participant responses from each other
    askAddress?: boolean;
    askEmail?: boolean;
    askPhone?: boolean;
  };
}

interface Poll {
  id: string;
  title: string;
  state: "OPEN" | "CLOSED" | "CANCELED";
  adminUrl: string;
  participantUrl: string;
  options: PollOption[];
  participants: Participant[];
  finalOption?: PollOption;
}

async function createPoll(params: CreatePollParams): Promise<Poll> {
  const { data } = await doodle.post("/polls", {
    title: params.title,
    description: params.description,
    options: params.options.map((opt) => ({
      start: opt.start,
      end: opt.end,
    })),
    settings: params.settings || {},
  });
  return data;
}

Adding Participants and Responses

interface Participant {
  id: string;
  name: string;
  preferences: ParticipantPreference[];
}

interface ParticipantPreference {
  optionId: string;
  preference: "yes" | "no" | "ifNeedBe";
}

async function addParticipant(
  pollId: string,
  name: string,
  preferences: ParticipantPreference[]
): Promise<Participant> {
  const { data } = await doodle.post(`/polls/${pollId}/participants`, {
    name,
    preferences,
  });
  return data;
}

Retrieving Poll Results

interface PollResults {
  poll: Poll;
  bestOptions: {
    option: PollOption;
    yesCount: number;
    ifNeedBeCount: number;
    noCount: number;
  }[];
}

async function getPoll(pollId: string): Promise<Poll> {
  const { data } = await doodle.get(`/polls/${pollId}`);
  return data;
}

function analyzePollResults(poll: Poll): PollResults {
  const optionScores = poll.options.map((option, index) => {
    let yesCount = 0;
    let ifNeedBeCount = 0;
    let noCount = 0;

    for (const participant of poll.participants) {
      const pref = participant.preferences[index];
      if (pref?.preference === "yes") yesCount++;
      else if (pref?.preference === "ifNeedBe") ifNeedBeCount++;
      else noCount++;
    }

    return { option, yesCount, ifNeedBeCount, noCount };
  });

  // Sort by most "yes" votes, then by fewest "no" votes
  optionScores.sort((a, b) => {
    if (b.yesCount !== a.yesCount) return b.yesCount - a.yesCount;
    return a.noCount - b.noCount;
  });

  return { poll, bestOptions: optionScores };
}

Finalizing a Poll

async function finalizePoll(
  pollId: string,
  chosenOptionIndex: number
): Promise<Poll> {
  const { data } = await doodle.post(`/polls/${pollId}/close`, {
    selectedOptionIndex: chosenOptionIndex,
  });
  return data;
}

// Auto-finalize: pick the option with the most "yes" votes
async function autoFinalizePoll(pollId: string): Promise<Poll> {
  const poll = await getPoll(pollId);
  const results = analyzePollResults(poll);

  if (results.bestOptions.length === 0) {
    throw new Error("No options available to finalize");
  }

  const bestOptionIndex = poll.options.indexOf(
    results.bestOptions[0].option
  );
  return finalizePoll(pollId, bestOptionIndex);
}

1:1 Booking Pages

interface BookingPage {
  id: string;
  name: string;
  slug: string;
  url: string;
  duration: number; // minutes
  location: string;
}

async function listBookingPages(): Promise<BookingPage[]> {
  const { data } = await doodle.get("/booking-pages");
  return data;
}

async function getBookingPageAppointments(
  bookingPageId: string,
  from?: string,
  to?: string
): Promise<any[]> {
  const { data } = await doodle.get(
    `/booking-pages/${bookingPageId}/appointments`,
    { params: { from, to } }
  );
  return data;
}

Webhook Handling

import express, { Request, Response } from "express";

interface DoodleWebhookEvent {
  type:
    | "poll.participant_added"
    | "poll.closed"
    | "poll.deleted"
    | "booking.created"
    | "booking.canceled";
  data: {
    pollId?: string;
    participantId?: string;
    bookingId?: string;
  };
}

function setupDoodleWebhooks(app: express.Application): void {
  app.post("/webhooks/doodle", (req: Request, res: Response) => {
    const event: DoodleWebhookEvent = req.body;

    switch (event.type) {
      case "poll.participant_added":
        // Check if all expected participants have responded
        checkPollCompletion(event.data.pollId!);
        break;
      case "poll.closed":
        // Sync finalized time to your calendar system
        syncFinalizedMeeting(event.data.pollId!);
        break;
      case "booking.created":
        handleNewBooking(event.data.bookingId!);
        break;
      case "booking.canceled":
        handleBookingCancellation(event.data.bookingId!);
        break;
    }

    res.status(200).send("OK");
  });
}

Best Practices

  • Enable the "if need be" option on polls to capture soft availability; this significantly increases the chance of finding a time that works for large groups.
  • Set a deadline on polls to prevent them from staying open indefinitely; automate a reminder or auto-close workflow as the deadline approaches.
  • Use the hidden poll setting for sensitive scheduling (e.g., interviews) so participants cannot see each other's responses.
  • Distribute the participantUrl (not the adminUrl) to participants; the admin URL grants edit and close permissions.
  • For recurring group meetings, create polls programmatically with a template of time options based on historical preferences.

Common Pitfalls

  • Doodle's free tier has API rate limits and restricted access; premium plans are required for full API functionality including webhooks and booking pages.
  • Poll options are positional — when analyzing preferences, match by index rather than by option ID, since the preferences array aligns with the options array order.
  • Closing a poll is irreversible; once finalized, the poll cannot be reopened. Always confirm with the organizer before programmatically closing.
  • The API does not natively send calendar invites upon finalization; your integration must generate and send ICS files or create calendar events separately.
  • Participant names are free-text and not deduplicated; the same person can submit multiple responses under different names unless you enforce email-based identity through poll settings.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add scheduling-services-skills

Get CLI access →