Forms and surveys
A Form stores structured data. It has field sets, each field set has fields, and choice fields have options. Every saved submission belongs to a Form and holds one answer per filled-in field. Surveys built in the survey builder keep their questions in a Form: each respondent's run links to the submissions that hold its answers.
| Model | Endpoints |
|---|---|
Form | GET/POST /workspaces/:workspace_id/forms, GET/PUT/DELETE /forms/:id |
Forms::FieldSet | GET/POST /forms/:form_id/field_sets, GET/PUT/DELETE /forms/field_sets/:id |
Forms::Field | GET/POST /forms/field_sets/:field_set_id/fields, GET/PUT/DELETE /forms/fields/:id |
Forms::Fields::Option | GET/POST /forms/fields/:field_id/options, GET/PUT/DELETE /forms/fields/options/:id |
Forms::Submission | GET/POST /forms/:form_id/submissions, GET/PUT/DELETE /forms/submissions/:id |
Forms::Submissions::Answer | GET/POST /forms/submissions/:submission_id/answers, GET/PUT/DELETE /forms/submissions/answers/:id |
SurveyWorkflow (read-only) | GET /workspaces/:workspace_id/survey_workflows, GET /survey_workflows/:id |
SurveyWorkflowRun (read-only) | GET /survey_workflows/:survey_workflow_id/runs, GET /survey_workflows/runs/:id |
Create a survey and its question flow in the survey builder. The API reads surveys and their complete responses, and the Forms resources edit question labels, choices and saved answers. A completed survey response is also delivered by webhook.
1. Prepare access
Use a token granted the survey's workspace. forms:read covers Form, survey and response reads; forms:write is required to create or edit Forms and saved answers. Add webhooks:write to manage outgoing subscriptions. Reads include anonymous respondents. Fetching contact details separately needs the appropriate Contacts permission.
export API_ROOT='https://your-workspace.myclickfunnels.com/api/v2'
export WORKSPACE_ID='3'
# Set TOKEN to your authorized token in your shell; do not put it in source control.All GET requests have no body. Numeric IDs and public_id values can be used in paths. A token without access to the workspace receives 403 with a recovery hint; a missing record receives 404.
2. Build a Form and save a submission
Create the Form, a field set, a field and, for a choice field, its options. Then save a submission with an answer per field.
curl --fail-with-body -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/workspaces/$WORKSPACE_ID/forms" --data '{"form":{"name":"Lead details"}}'
curl --fail-with-body -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/$FORM_ID/field_sets" --data '{"forms_field_set":{"title":"About you"}}'
curl --fail-with-body -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/field_sets/$FIELD_SET_ID/fields" --data '{"forms_field":{"key":"goal","label":"What is your goal?","field_type":"text"}}'
curl --fail-with-body -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/$FORM_ID/submissions" --data '{"forms_submission":{"contact_id":324}}'
curl --fail-with-body -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/submissions/$SUBMISSION_ID/answers" --data '{"forms_submissions_answer":{"field_id":331,"entry":"More leads"}}'Choice answers use fields_option_ids (several choices) or fields_option_id (one choice) instead of entry. Submissions created this way are not linked to any survey run.
3. Find the survey and its Form
curl --fail-with-body -sS -H "Authorization: Bearer $TOKEN" "$API_ROOT/workspaces/$WORKSPACE_ID/survey_workflows" > surveys.jsonEach listed survey has the same shape as a single survey: its details plus its current questions catalog. To work with one survey, fetch it by ID:
export SURVEY_ID=$(jq -r '.[0].id' surveys.json)
curl --fail-with-body -sS -H "Authorization: Bearer $TOKEN" "$API_ROOT/survey_workflows/$SURVEY_ID" > survey.json
export FORM_ID=$(jq -r '.form_id' survey.json)Example response captured from a local API walkthrough (use the IDs returned by your own workspace throughout this guide). The questions catalog includes questions on every branch, even before anyone answers. Question order is ID order, not the respondent's traversal path. An unanswered question has no synthetic answer row.
{
"id": 235,
"public_id": "YvebeJ",
"name": "Business goals",
"site_id": 2,
"form_id": 339,
"enabled": true,
"archived": null,
"created_at": "2026-10-22T18:57:28.239Z",
"updated_at": "2026-10-22T18:57:28.239Z",
"questions": [
{
"id": 812,
"public_id": "EjRDme",
"type": "choice",
"title": "Which channels do you use?",
"field_set_id": 468,
"fields": [
{
"id": 330,
"public_id": "mJLkRj",
"key": "channels",
"label": "Which channels do you use?",
"field_type": "choice",
"allow_multiple": true,
"captured_contact_attribute": null,
"options": [
{
"id": 305,
"public_id": "kNWrBY",
"label": "Email",
"image": null
},
{
"id": 306,
"public_id": "GJPDwJ",
"label": "Social",
"image": null
}
]
}
]
},
{
"id": 814,
"public_id": "RJgWEJ",
"type": "text",
"title": "What is your goal?",
"field_set_id": 468,
"fields": [
{
"id": 331,
"public_id": "BjZpZY",
"key": "business_goal",
"label": "What is your goal?",
"field_type": "text",
"allow_multiple": false,
"captured_contact_attribute": null,
"options": []
}
]
}
]
}The survey's name is separate from its Form's name. Changing form.name does not rename a survey. archived is a nullable timestamp; disabled and archived surveys remain readable. Legacy surveys can have form_id: null or empty question fields.
4. Map question IDs to editable Form data
flowchart LR S[Survey] --> Q[Question catalog] S --> F[Form] Q --> FS[Field set] FS --> FF[Fields] FF --> O[Available choices] S --> R[Runs] R --> SU[Associated submissions] SU --> A[Stored answer rows] A --> FF A --> SO[Selected choices]
Use the field IDs in questions[].fields[] for Form edits. A question ID identifies the displayed question; it is not interchangeable with a field ID. A contact-info question has several fields under one question, and its title belongs to the field set. Other question titles come from the ordinary field label. Multiple questions can reference the same field set, so inspect the catalog before editing shared data.
curl --fail-with-body -sS -H "Authorization: Bearer $TOKEN" "$API_ROOT/forms/$FORM_ID/field_sets"
# Use the chosen field_set_id returned above:
curl --fail-with-body -sS -H "Authorization: Bearer $TOKEN" "$API_ROOT/forms/field_sets/468/fields"5. Edit a supported question label or contact-info title
For the ordinary text question above:
curl --fail-with-body -sS -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/fields/331" --data '{"forms_field":{"label":"What would you like to grow?"}}'The Form field response includes the updated label; re-fetch survey.json to see the question's current title and field label. For a contact-info title, patch its field_set_id instead:
curl --fail-with-body -sS -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/field_sets/468" --data '{"forms_field_set":{"title":"How can we reach you?"}}'Edit the contact-info subfield labels through their individual field IDs. Creating or reordering Form fields does not create or reorder survey question steps. Use the survey builder for question creation, order, branching, and type changes; generic field-type writes do not perform a supported survey-type conversion. Survey metadata and run lifecycle are read-only through these survey resources.
6. Add or rename a choice label
curl --fail-with-body -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/fields/330/options" --data '{"forms_fields_option":{"label":"Podcast"}}'
curl --fail-with-body -sS -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/fields/options/306" --data '{"forms_fields_option":{"label":"Social media"}}'Use the returned option ID for later selections. The current Forms option write API accepts labels; it does not configure survey choice images, scores, or visibility. Question descriptions and contact-capture configuration are also not writable through these generic Form parameters.
Available choices in definitions include only presentable options. A saved response still includes selected hidden/generated Other choices. Image-only choices may have no label; use their image information when presenting available choices.
7. Retrieve the entire response
curl --fail-with-body -sS -G -H "Authorization: Bearer $TOKEN" --data-urlencode 'filter[status]=completed' --data-urlencode 'per_page=20' "$API_ROOT/survey_workflows/$SURVEY_ID/runs" > runs.jsonLists return a bare array of full run objects. To fetch one full object:
curl --fail-with-body -sS -H "Authorization: Bearer $TOKEN" "$API_ROOT/survey_workflows/runs/136" > run.json{
"id": 136,
"public_id": "OJMzEj",
"survey_workflow_id": 235,
"contact_id": 324,
"completed_at": "2026-10-22T18:57:29.000Z",
"canceled_at": null,
"created_at": "2026-10-22T18:57:29.266Z",
"updated_at": "2026-10-22T18:57:29.307Z",
"status": "completed",
"preview": false,
"form_submissions": [
{
"id": 120,
"public_id": "wJVvEJ",
"form_id": 339,
"contact_id": 324,
"answers": [
{
"id": 152,
"public_id": "wYGknY",
"field": {
"id": 330,
"public_id": "mJLkRj",
"key": "channels",
"label": "Which channels do you use?"
},
"entry": null,
"created_at": "2026-10-22T18:57:29.293Z",
"updated_at": "2026-10-22T18:57:29.296Z",
"selected_options": [
{
"id": 305,
"public_id": "kNWrBY",
"label": "Email"
},
{
"id": 306,
"public_id": "GJPDwJ",
"label": "Social media"
}
]
},
{
"id": 151,
"public_id": "QJOEeN",
"field": {
"id": 331,
"public_id": "BjZpZY",
"key": "business_goal",
"label": "What would you like to grow?"
},
"entry": "Grow my consulting business",
"created_at": "2026-10-22T18:56:29.282Z",
"updated_at": "2026-10-22T18:57:29.283Z",
"selected_options": []
},
{
"id": 150,
"public_id": "RYEvBN",
"field": {
"id": 331,
"public_id": "BjZpZY",
"key": "business_goal",
"label": "What would you like to grow?"
},
"entry": "More leads",
"created_at": "2026-10-22T18:55:29.273Z",
"updated_at": "2026-10-22T18:57:29.274Z",
"selected_options": []
}
]
}
]
}Every associated submission and stored answer row is included. Within each submission, answers are ordered by created_at descending and then ID descending. Repeated answers to one field remain separate. Correcting an older answer changes its updated_at without moving its creation-history position. Text is in entry; choices are in selected_options, merging single-choice and multi-choice storage without duplicate option IDs.
Submissions can be shared by multiple runs. These are the answers currently stored in submissions associated with the run, not proof that each row originated during that run. Labels and keys are current; edits do not preserve old values, and deleted answers are absent. This API is not an immutable answer audit log.
To choose the most recently created answer per field on the client:
jq '.form_submissions | map(.answers[]) | group_by(.field.id) | map(max_by([.created_at, .id]))' run.json8. Filter and paginate runs
A survey's runs list returns every run, finished or not. Runs from the survey builder's preview have preview: true and send no webhook. Filter by one status (active, paused, completed or canceled) and by contact_id (comma-separated contact IDs). completed_at alone does not establish success because cancellation also sets it; use status. paused often means waiting at a question, not a respondent choosing to pause. Closing a browser leaves an incomplete, resumable run; it does not record cancellation.
curl --fail-with-body -sS -G -D page-headers.txt -H "Authorization: Bearer $TOKEN" --data-urlencode 'filter[status]=completed' --data-urlencode 'filter[contact_id]=324' --data-urlencode 'per_page=20' "$API_ROOT/survey_workflows/$SURVEY_ID/runs"Lists use the standard Link / Pagination-Next headers and after cursor, with 20 records by default and a cap of 100. Follow the returned next link until none remains. No nested answer page is required.
9. Correct stored answer content through Forms
Use the answer ID in run.json; this is different from the question and field IDs.
curl --fail-with-body -sS -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/submissions/answers/151" --data '{"forms_submissions_answer":{"entry":"Grow my consulting business"}}'
curl --fail-with-body -sS -X PATCH -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/forms/submissions/answers/152" --data '{"forms_submissions_answer":{"fields_option_ids":[305,306]}}'Use fields_option_id for the legacy single-choice value. If both single-choice and multi-choice storage are present, clear the one you no longer want as part of the correction. An empty array clears joined choices; null clears the single-choice value. Adding or deleting an answer also uses the existing Form answer resources. Deleting the final answer produces an empty answer array in the next response.
Form corrections update saved data but send no webhook, and they do not advance, identify, opt in, or complete the respondent. Creating a generic Form submission does not link it to a survey run, and changing its contact does not coordinate the run's contact. The structured Form submission resources here are separate from page-submission tracking logs.
Deleting fields, choices, or submissions can remove associated stored answers or selections. A choice referenced by legacy single-choice answers cannot be deleted until those references are corrected or cleared. Current labels change how older answers are displayed. Retrieve and retain your own copy if you need independent historical records.
10. Subscribe and verify
curl --fail-with-body -sS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API_ROOT/workspaces/$WORKSPACE_ID/webhooks/outgoing/endpoints" --data '{"webhooks_outgoing_endpoint":{"name":"Survey integration","url":"https://integrations.example.com/clickfunnels/surveys","event_type_ids":["survey_workflow_run.completed"]}}'Discover the exact key through GET /api/v2/webhooks/outgoing/event_types. The V2 event carries the same full run object as GET /survey_workflows/runs/:id in data, plus the standard event ID, workspace, subject, time, and optional page/funnel context.
| Event | When it is issued |
|---|---|
survey_workflow_run.completed | A respondent completes the survey. Not sent for cancellation or builder previews. Runs that were already in progress when this shipped send it when they complete. |
sequenceDiagram participant Respondent participant Survey participant Receiver Respondent->>Survey: Submit the final answer Survey->>Survey: Save the answer, resolve the contact, complete the run Survey-->>Receiver: completed (full saved response)
The payload is captured when the run completes. Anonymous respondents are included. Later answer corrections send no webhook, so re-fetch the run when you need the current saved answers.
Keep your receiver idempotent using event_id: retries retain the original ID and payload. See Webhook event types and How to work with webhooks for the envelope, signatures and retry behavior.
Common errors
| Status | Check |
|---|---|
401 | Supply a valid bearer token. |
403 | Check Forms permissions, workspace grants and subscription access; use the returned hint. |
404 | Check the survey or run ID and its workspace. |
422 | Use one of the supported status values. |
For an AI integration, discover the survey_workflow and survey_workflow_run resources in the Forms MCP category, or read the Forms and surveys skill.
Updated about 2 hours ago