Skip to main content
Technology & EngineeringForm Survey Services259 lines

Surveymonkey

Integrate SurveyMonkey's REST API to create surveys, collect responses via

Quick Summary21 lines
You are an expert at integrating SurveyMonkey via its v3 REST API. You design surveys programmatically, manage collectors for distribution, analyze response data, and react to completion events through webhooks with type-safe TypeScript patterns.

## Key Points

- **Using `/responses` instead of `/responses/bulk`** for detailed answer data. The basic endpoint returns only metadata.
- **Ignoring rate limits (120 req/min)**. Always implement backoff on 429 responses.
- **Hardcoding `choice_id` values** instead of mapping them from the survey detail endpoint. IDs are opaque and change across survey copies.
- **Fetching all responses without date or status filters**. Use `start_created_at`, `end_created_at`, and `status` params.
- Running structured market research or customer satisfaction surveys at scale.
- Building dashboards that aggregate and visualize survey response data.
- Automating survey creation for recurring feedback programs (quarterly NPS, post-event).
- Distributing surveys across multiple channels (web, email, embedded) with per-channel analytics.
- Triggering workflows (CRM updates, alerts) when survey responses are completed.

## Quick Example

```bash
SURVEYMONKEY_ACCESS_TOKEN=your-access-token
```
skilldb get form-survey-services-skills/SurveymonkeyFull skill: 259 lines
Paste into your CLAUDE.md or agent config

SurveyMonkey API Skill

You are an expert at integrating SurveyMonkey via its v3 REST API. You design surveys programmatically, manage collectors for distribution, analyze response data, and react to completion events through webhooks with type-safe TypeScript patterns.

Core Philosophy

Survey-Collector-Response Hierarchy

SurveyMonkey's data model has three layers: surveys define questions, collectors define distribution channels (web links, email, embedded), and responses hold answers tied to a collector. Always scope API calls appropriately — fetch responses per collector when you need channel-specific metrics.

Bulk Collection, Selective Retrieval

SurveyMonkey surveys can accumulate millions of responses. Use the bulk endpoint with date filters and status parameters to fetch only what you need. Request per_page up to 100 and paginate with cursors. Never load all responses into memory at once.

OAuth-First Authentication

SurveyMonkey uses OAuth 2.0 for third-party apps. For internal tools, use a long-lived access token from your app settings. Always include the token in the Authorization header and respect the 120 requests/minute rate limit with exponential backoff.

Setup

Configure the API client:

const SM_ACCESS_TOKEN = process.env.SURVEYMONKEY_ACCESS_TOKEN;
const BASE_URL = "https://api.surveymonkey.com/v3";

async function smFetch<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`${BASE_URL}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${SM_ACCESS_TOKEN}`,
      "Content-Type": "application/json",
      ...init?.headers,
    },
  });
  if (res.status === 429) {
    const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return smFetch<T>(path, init);
  }
  if (!res.ok) throw new Error(`SurveyMonkey ${res.status}: ${await res.text()}`);
  return res.json() as Promise<T>;
}

Environment variables:

SURVEYMONKEY_ACCESS_TOKEN=your-access-token

Key Patterns

Create a Survey with Pages and Questions

Do this — structure surveys with typed page and question definitions:

interface SMChoice { text: string }
interface SMQuestion {
  family: "single_choice" | "multiple_choice" | "open_ended" | "matrix" | "demographic";
  subtype: "vertical" | "essay" | "single" | "rating" | "international";
  headings: Array<{ heading: string }>;
  answers?: { choices?: SMChoice[]; rows?: Array<{ text: string }> };
  required?: { text: string; type: "all" | "at_least"; amount?: string };
}

// Step 1: Create empty survey
const survey = await smFetch<{ id: string }>("/surveys", {
  method: "POST",
  body: JSON.stringify({ title: "Q1 Customer Satisfaction" }),
});

// Step 2: Add a page with questions
const page = await smFetch<{ id: string }>(`/surveys/${survey.id}/pages`, {
  method: "POST",
  body: JSON.stringify({ title: "Satisfaction" }),
});

// Step 3: Add questions
await smFetch(`/surveys/${survey.id}/pages/${page.id}/questions`, {
  method: "POST",
  body: JSON.stringify({
    family: "single_choice",
    subtype: "vertical",
    headings: [{ heading: "How satisfied are you with our service?" }],
    answers: {
      choices: [
        { text: "Very Satisfied" },
        { text: "Satisfied" },
        { text: "Neutral" },
        { text: "Dissatisfied" },
        { text: "Very Dissatisfied" },
      ],
    },
    required: { text: "This question is required.", type: "all" },
  } satisfies SMQuestion),
});

Not this — creating surveys in the UI then hardcoding survey IDs. Use the API for reproducible, version-controlled survey creation.

Fetch Responses with Pagination

Do this — paginate with cursor and filter by status:

interface SMResponse {
  id: string;
  survey_id: string;
  collector_id: string;
  date_created: string;
  response_status: "completed" | "partial" | "overquota" | "disqualified";
  pages: Array<{
    id: string;
    questions: Array<{
      id: string;
      answers: Array<{ choice_id?: string; text?: string; row_id?: string }>;
    }>;
  }>;
}

interface SMPaginatedResponse<T> {
  data: T[];
  per_page: number;
  total: number;
  page: number;
  links: { next?: string };
}

async function fetchAllResponses(surveyId: string, status = "completed"): Promise<SMResponse[]> {
  const all: SMResponse[] = [];
  let path: string | undefined = `/surveys/${surveyId}/responses/bulk?per_page=100&status=${status}`;
  while (path) {
    const page = await smFetch<SMPaginatedResponse<SMResponse>>(path);
    all.push(...page.data);
    path = page.links.next ? new URL(page.links.next).pathname + new URL(page.links.next).search : undefined;
  }
  return all;
}

Not this — fetching /responses (summary only) instead of /responses/bulk when you need answer details.

Configure Webhooks

Do this — register a webhook for completed responses:

interface SMWebhook {
  id: string;
  name: string;
  event_type: "response_completed" | "response_updated" | "response_disqualified";
  subscription_url: string;
}

const webhook = await smFetch<SMWebhook>("/webhooks", {
  method: "POST",
  body: JSON.stringify({
    name: "Response Completed Hook",
    event_type: "response_completed",
    object_type: "survey",
    object_ids: [surveyId],
    subscription_url: "https://myapp.com/webhooks/surveymonkey",
  }),
});

Not this — polling the responses endpoint on a timer instead of using webhooks for real-time processing.

Common Patterns

Create a Web Link Collector

interface SMCollector {
  id: string;
  type: "weblink" | "email" | "embedded";
  url?: string;
  status: "open" | "closed";
}

const collector = await smFetch<SMCollector>(`/surveys/${surveyId}/collectors`, {
  method: "POST",
  body: JSON.stringify({
    type: "weblink",
    thank_you_message: "Thanks for your feedback!",
    allow_multiple_responses: false,
  }),
});
console.log("Survey link:", collector.url);

Analyze Response Distribution

function analyzeChoiceDistribution(responses: SMResponse[], questionId: string) {
  const counts = new Map<string, number>();
  for (const response of responses) {
    for (const page of response.pages) {
      const q = page.questions.find((q) => q.id === questionId);
      for (const answer of q?.answers ?? []) {
        if (answer.choice_id) {
          counts.set(answer.choice_id, (counts.get(answer.choice_id) ?? 0) + 1);
        }
      }
    }
  }
  return Object.fromEntries(counts);
}

Get Survey Details with Question Mapping

interface SMSurveyDetail {
  id: string;
  title: string;
  pages: Array<{
    id: string;
    questions: Array<{
      id: string;
      headings: Array<{ heading: string }>;
      answers?: { choices?: Array<{ id: string; text: string }> };
    }>;
  }>;
}

const detail = await smFetch<SMSurveyDetail>(`/surveys/${surveyId}/details`);
const choiceMap = new Map<string, string>();
for (const page of detail.pages) {
  for (const q of page.questions) {
    for (const choice of q.answers?.choices ?? []) {
      choiceMap.set(choice.id, choice.text);
    }
  }
}

Anti-Patterns

  • Using /responses instead of /responses/bulk for detailed answer data. The basic endpoint returns only metadata.
  • Ignoring rate limits (120 req/min). Always implement backoff on 429 responses.
  • Hardcoding choice_id values instead of mapping them from the survey detail endpoint. IDs are opaque and change across survey copies.
  • Fetching all responses without date or status filters. Use start_created_at, end_created_at, and status params.

When to Use

  • Running structured market research or customer satisfaction surveys at scale.
  • Building dashboards that aggregate and visualize survey response data.
  • Automating survey creation for recurring feedback programs (quarterly NPS, post-event).
  • Distributing surveys across multiple channels (web, email, embedded) with per-channel analytics.
  • Triggering workflows (CRM updates, alerts) when survey responses are completed.

Install this skill directly: skilldb add form-survey-services-skills

Get CLI access →