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.
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/v1Authorization Header
Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxxRate 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.
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per minute |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
When the rate limit is exceeded, the API responds with a 429 Too Many Requests status code:
{
"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.
/formsList Forms
Retrieve a paginated list of all forms in your account. Results are ordered by creation date, newest first.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
limit | integer | Results per page, 1-100 (default: 20) |
status | string | Filter by status: "draft", "published", or "archived" |
Response
{
"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 -X GET "https://formpop.ai/api/v1/forms?page=1&limit=20&status=published" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"/formsCreate 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
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Required | Form title (1-200 characters) |
schema | object | Optional | Full form schema with questions, settings, and logic. See Response Types for structure. |
Response
{
"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 -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!"
}
}
}
}'/forms/:formIdGet Form
Retrieve a single form by its ID, including the full schema with all questions, logic rules, and settings.
Response
{
"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 -X GET "https://formpop.ai/api/v1/forms/frm_a1b2c3d4" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"/forms/:formIdUpdate 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
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Optional | New form title |
schema | object | Optional | Replacement form schema (full object, not a partial merge) |
status | string | Optional | "draft" or "published" |
Response
{
"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 -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"
}'/forms/:formIdArchive 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
{
"data": {
"id": "frm_a1b2c3d4",
"archived": true
}
}Example Request
curl -X DELETE "https://formpop.ai/api/v1/forms/frm_a1b2c3d4" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"/forms/:formId/responsesList Responses
Retrieve a paginated list of completed responses for a form. Use the since and until parameters to filter by submission date.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
limit | integer | Results per page, 1-100 (default: 20) |
since | string | ISO 8601 timestamp, return responses after this date |
until | string | ISO 8601 timestamp, return responses before this date |
Response
{
"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 -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"/forms/:formId/responses/:responseIdGet Response
Retrieve a single response by its ID, including all answers and metadata.
Response
{
"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 -X GET "https://formpop.ai/api/v1/forms/frm_a1b2c3d4/responses/res_z9y8x7w6" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"/forms/:formId/responses/:responseIdDelete Response
Permanently delete a single response. This action cannot be undone.
Response
{
"data": {
"id": "res_z9y8x7w6",
"deleted": true
}
}Example Request
curl -X DELETE "https://formpop.ai/api/v1/forms/frm_a1b2c3d4/responses/res_z9y8x7w6" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"/forms/:formId/publishPublish Form
Publish a draft form, making it accessible to respondents at its public URL. The form must have at least one question.
Response
{
"data": {
"id": "frm_a1b2c3d4",
"status": "published",
"public_url": "https://formpop.ai/f/customer-feedback-x7k"
}
}Example Request
curl -X POST "https://formpop.ai/api/v1/forms/frm_a1b2c3d4/publish" \
-H "Authorization: Bearer fp_live_xxxxxxxxxxxxxxxxxxxxx"/forms/:formId/unpublishUnpublish 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
{
"data": {
"id": "frm_a1b2c3d4",
"status": "draft"
}
}Example Request
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": "not_found",
"message": "The requested form does not exist."
}| Status | Error Code | Description |
|---|---|---|
400 | bad_request | The request body is malformed or missing required fields. |
401 | unauthorized | Missing or invalid API key. |
403 | forbidden | Your plan does not include API access. Business plan required. |
404 | not_found | The requested resource does not exist or does not belong to your account. |
429 | rate_limit_exceeded | Too many requests. Wait and retry after the reset window. |
500 | internal_error | An 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
{
"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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdTo 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
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" });
}
});Response Types
TypeScript interfaces for the main objects returned by the API.
Form
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
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
}