POST /survey

Submit panelist votes for survey questions.

Request

POST /public/survey

Headers

HeaderRequiredDescription
AuthorizationYesCompound token from Tenant API
Content-TypeYesapplication/json

Request Body

FieldTypeDescription
votesobjectMap of questionId to the panelist’s answer
isFinalbooleanWhether this is the final submission for the survey (answer to the final question)

Note: If the survey allows partial responses, you can submit responses to questions as a panelist is completing them. See Partial Submissions.

{
	"votes": {
		"satisfaction": { "optionIds": ["3"] },
		"renew": { "optionIds": ["yes"] },
		"tools": { "optionIds": ["lint", "ci"] },
		"improvements": { "optionIds": [], "explanation": "Great product, love it!" }
	},
	"isFinal": true
}

Take every questionId and optionId from the survey returned by GET /public/survey. A single request can carry up to 97 answers.

Answer Object

FieldTypeDescription
optionIdsarrayThe optionId values of the options the panelist chose
explanationstring(Open-ended questions only) The panelist’s free-text answer, up to 5,000 characters

The shape of an answer depends on the question’s questionType and responseType:

questionTyperesponseTypeoptionIdsexplanation
singlesingleExactly one option IDNot used
likertsingleExactly one option IDNot used
multi-choiceindependentThe IDs of all the selected optionsNot used
kanocodependentTwo option IDs, one from each groupNot used
open-endedunaggregatedAn empty arrayThe answer text
textnoneDo not submit an answer for a display-only question 
Single-choice questions

(questionType: 'single', the default)

Submit the ID of the selected option as the single element of the optionIds array:

{ "optionIds": ["yes"] }

Always submit the option ID, never the option text. The results of a single-choice question count option IDs only. An answer with an empty optionIds array counts toward totalResponses, but it counts toward no option, so the survey results show nothing for it, even when explanation carries the text of an option.

{ "optionId": "yes" } is a shorter form of the same answer.

Likert questions

(questionType: 'likert')

Submit the ID of the selected point of the scale, exactly as for a single-choice question:

{ "optionIds": ["3"] }

Submit the optionId, not the label. On a reverse-scored question the two are different: the label tells your interface where to show the option on the scale, and the optionId is the value that the results count.

Multiple-choice questions

(questionType: 'multi-choice')

Submit the IDs of all the selected options in the optionIds array (the order is not important):

{ "optionIds": ["lint", "ci"] }

Each entry in optionIds must match an optionId from the survey’s answerOptions. Submitting an empty array ([]) records “no option selected”; the question still counts toward totalResponses.

Codependent questions (Kano)

(questionType: 'kano', responseType: 'codependent')

A Kano question has two groups of options: the functional question (“If this feature is present?”) and the dysfunctional question (“If this feature is absent?”). Submit both halves of the pair as a single answer, with one optionId from each group, in the order the survey defines them (functional first, dysfunctional second):

{ "optionIds": ["fl", "dd"] }
Open-ended questions

(questionType: 'open-ended', responseType: 'unaggregated')

Submit the free-text answer in explanation and leave optionIds empty:

{ "optionIds": [], "explanation": "Great product, love it!" }

The explanation field is the panelist’s free-text answer. Votito removes the white space at the start and at the end of the text. Open-ended responses increment totalResponses but are not aggregated into per-option counters.

explanation is for open-ended questions only. Do not use it to send the answer to a choice question.

Display-only questions

(questionType: 'text', responseType: 'none')

Do not include display-only questions in votes. They have no answer to record.

Response

{
	"message": "votes recorded",
	"count": 4
}

Response Fields

FieldTypeDescription
messagestringConfirmation message
countintegerNumber of answers recorded by this request

Examples

JavaScript

const submitVotes = async (publicKey, votes, isFinal = true) => {
	const response = await fetch('https://api.votito.com/public/survey', {
		method: 'POST',
		headers: {
			'Authorization': publicKey,
			'Content-Type': 'application/json'
		},
		body: JSON.stringify({ votes, isFinal })
	});

	if (!response.ok) {
		throw new Error(`Failed to submit: ${response.status}`);
	}

	return response.json();
};

// Usage - mix of question types in a single batch
const result = await submitVotes(publicKey, {
	'q1': { optionIds: ['4'] }, // single-choice
	'q2': { optionIds: ['lint', 'ci'] }, // multi-choice
	'q3': { optionIds: [], explanation: 'Faster onboarding.' } // open-ended
});
console.log(`Submitted ${result.count} responses`);

React Form Handler

const handleSubmit = async (event) => {
	event.preventDefault();
	const formData = new FormData(event.target);

	const votes = {};
	survey.questions.forEach((q) => {
		if (q.questionType === 'single' || q.questionType === 'likert') {
			const optionId = formData.get(q.questionId);
			if (optionId) {
				votes[q.questionId] = { optionIds: [optionId] };
			}
		}
	});

	try {
		await submitVotes(publicKey, votes);
		setSubmitted(true);
	} catch (error) {
		setError('Failed to submit survey');
	}
};

Give each radio button the optionId as its value, not the option text:

{q.answerOptions.map((option) => (
	<label key={option.optionId}>
		<input type="radio" name={q.questionId} value={option.optionId} />
		{option.optionText}
	</label>
))}

Python

import requests

def submit_votes(public_key, votes, is_final=True):
    response = requests.post(
        "https://api.votito.com/public/survey",
        headers={
            "Authorization": public_key,
            "Content-Type": "application/json"
        },
        json={
            "votes": votes,
            "isFinal": is_final
        }
    )
    response.raise_for_status()
    return response.json()

# Usage - mix of question types in a single batch
result = submit_votes(public_key, {
    "q1": {"optionIds": ["4"]},                                    # single-choice
    "q2": {"optionIds": ["lint", "ci"]},                           # multi-choice
    "q3": {"optionIds": [], "explanation": "Faster onboarding."}   # open-ended
})
print(f"Submitted {result['count']} responses")

Partial submissions

If the survey supports partial responses, you can submit responses multiple times, incrementally, with isFinal equal to false.

Partial responses are useful for longer surveys where you may expect that some people do not complete all the questions, but you want to capture the answers to the first few questions anyway.

You can submit the answers to the same question multiple times with partial submission, the last response overwrites the previous ones.

YOU MUST submit the final batch of answers with isFinal equal to true. Having all questions submitted is not a clear signal for the completion of a survey session, as the panelists can technically go back and modify answers to previous questions when partial submissions are enabled.

// Save progress without finalizing
await submitVotes(publicKey, { 'q1': { optionIds: ['3'] } }, false);

// Later, finalize the submission
await submitVotes(publicKey, { 'q2': { optionIds: ['yes'] } }, true);

Error Responses

Every error response carries the HTTP status text in message and an error code in error. Some errors add a details object:

{
	"message": "Bad Request",
	"error": "batch-too-large",
	"details": { "maxVotes": 97 }
}

400 Bad Request

errorCause
votes-must-be-non-emptyThe request has no votes field, or votes is not an object with at least one answer
batch-too-largeThe request carries more than details.maxVotes answers
survey-not-available-for-votingThe survey is not open
panelist-session-existsThe panelist already responded in another session, and the survey does not allow multiple responses
session-already-completedThis session already sent its final submission (isFinal: true)
text-exceeds-maximum-lengthAn explanation is longer than details.maxLength characters

401 Unauthorized

The Authorization header is missing.

403 Forbidden

The token is not valid, or it does not identify a panelist and a session. The token from the Tenant API identifies both.

429 Too Many Requests

The server is busy (resource-busy). Retry after a short delay.