API Reference

The FormPop AI REST API lets you programmatically create forms, retrieve responses, and manage your account. All endpoints return JSON and use standard HTTP methods.

Business plan required. API access is available exclusively on the Business plan ($39/mo). Upgrade in Dashboard → Billing to get started.

Authentication

Authenticate every request by including your API key in the Authorization header. Create and manage API keys in Dashboard → Settings → API Keys.

All requests must be made over HTTPS. Requests over plain HTTP will be rejected.

Base URL

https://formpop.ai/api/v1

Authorization Header

HTTP Header
Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx
Keep your keys safe. API keys carry the same privileges as your account. Do not share them in client-side code, public repositories, or unencrypted channels. If a key is compromised, revoke it immediately in your dashboard.

Rate Limits

The API enforces a limit of 100 requests per minute per API key. Rate limit status is communicated via response headers on every request.

HeaderDescription
X-RateLimit-LimitMaximum requests allowed per minute
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets

When the rate limit is exceeded, the API responds with a 429 Too Many Requests status code:

429 Response
{
  "error": "rate_limit_exceeded",
  "message": "Too many requests. Please wait before retrying.",
  "retry_after": 23
}

Endpoints

All endpoints are relative to the base URL https://formpop.ai/api/v1.

GET/forms

List Forms

Retrieve a paginated list of all forms in your account. Results are ordered by creation date, newest first.

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerResults per page, 1-100 (default: 20)
statusstringFilter by status: "draft", "published", or "archived"

Response

JSON
{
  "data": [
    {
      "id": "frm_a1b2c3d4",
      "title": "Customer Feedback",
      "slug": "customer-feedback-x7k",
      "status": "published",
      "response_count": 142,
      "created_at": "2026-03-01T10:00:00Z",
      "updated_at": "2026-03-15T14:30:00Z"
    }
  ],
  "meta": {
    "total": 12,
    "page": 1,
    "limit": 20
  }
}

Example Request

cURL
curl -X GET "https://formpop.ai/api/v1/forms?page=1&limit=20&status=published" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"
POST/forms

Create Form

Create a new form. You can pass a full schema with questions and settings, or just a title to create a blank form for editing later.

Request Body

FieldTypeRequiredDescription
titlestringRequiredForm title (1-200 characters)
schemaobjectOptionalFull form schema with questions, settings, and logic. See Response Types for structure.

Response

JSON
{
  "data": {
    "id": "frm_e5f6g7h8",
    "title": "New Hire Onboarding",
    "slug": "new-hire-onboarding-m2p",
    "status": "draft",
    "schema": { ... },
    "response_count": 0,
    "created_at": "2026-03-27T09:00:00Z",
    "updated_at": "2026-03-27T09:00:00Z"
  }
}

Example Request

cURL
curl -X POST "https://formpop.ai/api/v1/forms" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "New Hire Onboarding",
    "schema": {
      "version": "1",
      "questions": [
        {
          "id": "q_001",
          "type": "short_text",
          "label": "What is your full name?",
          "required": true
        }
      ],
      "settings": {
        "behavior": {
          "show_progress_bar": true,
          "thank_you_message": "Welcome aboard!"
        }
      }
    }
  }'
GET/forms/:formId

Get Form

Retrieve a single form by its ID, including the full schema with all questions, logic rules, and settings.

Response

JSON
{
  "data": {
    "id": "frm_a1b2c3d4",
    "title": "Customer Feedback",
    "slug": "customer-feedback-x7k",
    "status": "published",
    "schema": {
      "version": "1",
      "questions": [
        {
          "id": "q_abc123",
          "type": "rating",
          "label": "How would you rate our service?",
          "required": true,
          "validation": { "min": 1, "max": 5 }
        },
        {
          "id": "q_def456",
          "type": "long_text",
          "label": "Any additional feedback?",
          "required": false,
          "placeholder": "Tell us more..."
        }
      ],
      "settings": {
        "theme": {
          "primary_color": "#7c3aed",
          "background_color": "#ffffff",
          "font_family": "Inter",
          "border_radius": "8px"
        },
        "behavior": {
          "show_progress_bar": true,
          "allow_back_navigation": true,
          "thank_you_message": "Thanks for your feedback!"
        }
      }
    },
    "response_count": 142,
    "created_at": "2026-03-01T10:00:00Z",
    "updated_at": "2026-03-15T14:30:00Z"
  }
}

Example Request

cURL
curl -X GET "https://formpop.ai/api/v1/forms/frm_a1b2c3d4" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"
PUT/forms/:formId

Update Form

Update a form's title, schema, or status. Only include the fields you want to change. Schema updates replace the entire schema object.

Request Body

FieldTypeRequiredDescription
titlestringOptionalNew form title
schemaobjectOptionalReplacement form schema (full object, not a partial merge)
statusstringOptional"draft" or "published"

Response

JSON
{
  "data": {
    "id": "frm_a1b2c3d4",
    "title": "Updated Feedback Form",
    "slug": "customer-feedback-x7k",
    "status": "published",
    "schema": { ... },
    "response_count": 142,
    "created_at": "2026-03-01T10:00:00Z",
    "updated_at": "2026-03-27T11:00:00Z"
  }
}

Example Request

cURL
curl -X PUT "https://formpop.ai/api/v1/forms/frm_a1b2c3d4" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Feedback Form",
    "status": "published"
  }'
DELETE/forms/:formId

Archive Form

Soft-delete a form by archiving it. Archived forms are no longer accessible to respondents and do not appear in the default forms list. This action can be undone from the dashboard.

Response

JSON
{
  "data": {
    "id": "frm_a1b2c3d4",
    "archived": true
  }
}

Example Request

cURL
curl -X DELETE "https://formpop.ai/api/v1/forms/frm_a1b2c3d4" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"
GET/forms/:formId/responses

List Responses

Retrieve a paginated list of completed responses for a form. Use the since and until parameters to filter by submission date.

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerResults per page, 1-100 (default: 20)
sincestringISO 8601 timestamp, return responses after this date
untilstringISO 8601 timestamp, return responses before this date

Response

JSON
{
  "data": [
    {
      "id": "res_z9y8x7w6",
      "form_id": "frm_a1b2c3d4",
      "answers": {
        "q_abc123": 5,
        "q_def456": "Great service, very responsive team!"
      },
      "is_complete": true,
      "metadata": {
        "ip_country": "US",
        "device": "mobile",
        "browser": "Safari"
      },
      "submitted_at": "2026-03-20T08:15:00Z"
    }
  ],
  "meta": {
    "total": 142,
    "page": 1,
    "limit": 20
  }
}

Example Request

cURL
curl -X GET "https://formpop.ai/api/v1/forms/frm_a1b2c3d4/responses?limit=50&since=2026-03-01T00:00:00Z" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"
GET/forms/:formId/responses/:responseId

Get Response

Retrieve a single response by its ID, including all answers and metadata.

Response

JSON
{
  "data": {
    "id": "res_z9y8x7w6",
    "form_id": "frm_a1b2c3d4",
    "answers": {
      "q_abc123": 5,
      "q_def456": "Great service, very responsive team!"
    },
    "is_complete": true,
    "metadata": {
      "ip_country": "US",
      "device": "mobile",
      "browser": "Safari"
    },
    "submitted_at": "2026-03-20T08:15:00Z"
  }
}

Example Request

cURL
curl -X GET "https://formpop.ai/api/v1/forms/frm_a1b2c3d4/responses/res_z9y8x7w6" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"
DELETE/forms/:formId/responses/:responseId

Delete Response

Permanently delete a single response. This action cannot be undone.

Response

JSON
{
  "data": {
    "id": "res_z9y8x7w6",
    "deleted": true
  }
}

Example Request

cURL
curl -X DELETE "https://formpop.ai/api/v1/forms/frm_a1b2c3d4/responses/res_z9y8x7w6" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"
POST/forms/:formId/publish

Publish Form

Publish a draft form, making it accessible to respondents at its public URL. The form must have at least one question.

Response

JSON
{
  "data": {
    "id": "frm_a1b2c3d4",
    "status": "published",
    "public_url": "https://formpop.ai/f/customer-feedback-x7k"
  }
}

Example Request

cURL
curl -X POST "https://formpop.ai/api/v1/forms/frm_a1b2c3d4/publish" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"
POST/forms/:formId/unpublish

Unpublish Form

Revert a published form to draft status. The public URL will return a 404 until the form is published again. Existing responses are preserved.

Response

JSON
{
  "data": {
    "id": "frm_a1b2c3d4",
    "status": "draft"
  }
}

Example Request

cURL
curl -X POST "https://formpop.ai/api/v1/forms/frm_a1b2c3d4/unpublish" \
  -H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"

Error Codes

All errors follow a consistent structure with an error code and a human-readable message.

Error Response Shape
{
  "error": "not_found",
  "message": "The requested form does not exist."
}
StatusError CodeDescription
400bad_requestThe request body is malformed or missing required fields.
401unauthorizedMissing or invalid API key.
403forbiddenYour plan does not include API access. Business plan required.
404not_foundThe requested resource does not exist or does not belong to your account.
429rate_limit_exceededToo many requests. Wait and retry after the reset window.
500internal_errorAn unexpected error occurred. If it persists, contact support.

Webhooks

Configure webhooks in your form settings to receive real-time POST notifications when a form receives a new submission. FormPop sends the complete response payload to your endpoint.

Payload

Webhook POST Body
{
  "event": "response.completed",
  "form_id": "frm_a1b2c3d4",
  "response": {
    "id": "res_z9y8x7w6",
    "answers": {
      "q_abc123": 5,
      "q_def456": "Great service!"
    },
    "is_complete": true,
    "submitted_at": "2026-03-20T08:15:00Z"
  }
}

Signature Verification

Every webhook request includes an X-FormPop-Signature header for verifying authenticity. The header value has the format:

t=1711540200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

To verify the signature, compute an HMAC-SHA256 of the string {timestamp}.{raw_body} using your webhook secret and compare it with the v1 value.

Verification Example

Node.js
const crypto = require("crypto");

function verifyWebhook(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("="))
  );

  const timestamp = parts["t"];
  const signature = parts["v1"];

  // Reject requests older than 5 minutes to prevent replay attacks
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (age > 300) {
    throw new Error("Webhook timestamp too old");
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    throw new Error("Invalid webhook signature");
  }

  return JSON.parse(rawBody);
}

// Usage in an Express handler
app.post("/webhooks/formpop", (req, res) => {
  try {
    const event = verifyWebhook(
      req.body,                              // raw body string
      req.headers["x-formpop-signature"],    // signature header
      process.env.FORMPOP_WEBHOOK_SECRET     // your webhook secret
    );
    console.log("Verified event:", event);
    res.status(200).json({ received: true });
  } catch (err) {
    console.error("Webhook verification failed:", err.message);
    res.status(400).json({ error: "Invalid signature" });
  }
});
Retry policy. If your endpoint returns a non-2xx status code, FormPop retries delivery up to 3 times with exponential backoff (1 min, 5 min, 30 min). After 3 failures, the webhook is marked as failing in your dashboard.

Response Types

TypeScript interfaces for the main objects returned by the API.

Form

TypeScript
interface Form {
  id: string;                 // "frm_..." prefixed ID
  title: string;
  slug: string;               // URL-safe slug for public access
  status: "draft" | "published" | "archived";
  schema: FormSchema;
  response_count: number;
  created_at: string;         // ISO 8601
  updated_at: string;         // ISO 8601
}

interface FormSchema {
  version: string;
  questions: Question[];
  settings: {
    theme: {
      primary_color: string;
      background_color: string;
      font_family: string;
      border_radius: string;
    };
    branding: {
      logo_url: string | null;
      show_powered_by: boolean;
    };
    behavior: {
      show_progress_bar: boolean;
      allow_back_navigation: boolean;
      auto_save_partial: boolean;
      redirect_url: string | null;
      thank_you_message: string;
    };
  };
}

interface Question {
  id: string;                 // "q_..." prefixed ID
  type: QuestionType;
  label: string;
  description?: string;
  required: boolean;
  placeholder?: string;
  options?: Option[];         // For multi_choice, dropdown, ranking
  validation?: Record<string, any>;
  logic?: {
    show_if: {
      question_id: string;
      operator: "equals" | "not_equals" | "contains"
               | "greater_than" | "less_than";
      value: any;
    };
  };
}

type QuestionType =
  | "short_text"
  | "long_text"
  | "email"
  | "phone"
  | "number"
  | "url"
  | "multi_choice"
  | "dropdown"
  | "date"
  | "rating"
  | "opinion_scale"
  | "file_upload"
  | "payment"
  | "statement"
  | "ranking";

interface Option {
  id: string;
  label: string;
  value: string;
}

Response

TypeScript
interface FormResponse {
  id: string;                 // "res_..." prefixed ID
  form_id: string;
  answers: Record<string, any>;  // { question_id: answer_value }
  is_complete: boolean;
  metadata: {
    ip_country: string;
    device: string;
    browser: string;
  };
  submitted_at: string;       // ISO 8601
}