Creating a handful of Google Forms manually is tedious but manageable.
Generating hundreds of assessments dynamically requires a programmatic approach.
The Google Forms API allows developers to automate form creation, but adding items one by one wastes network requests and slows down execution.
Using the batchUpdate endpoint lets you inject multiple questions, set validation rules, and configure grading in a single payload.
What prerequisites do you need before calling the Google Forms API?
Before you can send your first batchUpdate request, Google requires a specific authentication environment. The Forms API reads and writes files directly to Google Drive, which means it relies heavily on proper scoping and user permissions.
Google Cloud project configuration: You must have an active project in the Google Cloud Console. Within this project, navigate to
APIs & Servicesand explicitly enable both the Google Forms API and the Google Drive API.OAuth 2.0 credentials: Create an OAuth 2.0 Client ID for a desktop or web application. Download the resulting
credentials.jsonfile to your local environment.Scope authorization: Your application must request the
https://www.googleapis.com/auth/forms.bodyscope to modify form content. If you also plan to read responses or change form settings, includehttps://www.googleapis.com/auth/forms.responses.readonlyandhttps://www.googleapis.com/auth/drive.Client library installation: While you can construct raw HTTP requests, using a client library handles token refresh cycles automatically. For Python, install the official packages by running
pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client.
Expert tip: If you use a Service Account instead of an OAuth Client ID, the forms you create via the API will live in the service account's isolated Drive. You will not see them in your personal Google Drive unless your script explicitly uses the Drive API to share the file with your email address or transfer ownership.
How do you structure a batchUpdate request payload?
The batchUpdate endpoint requires an array of specific mutation objects. Instead of sending a flat list of questions, you send a requests array. Each object in this array represents a single action - like creating an item, updating an item, or modifying the form's overall settings.
To add a new question, you use the createItem operation. The structure requires you to define the exact item type, the question text, the available choices, and the position of the question in the form.
Here is the exact anatomy of a batchUpdate request wrapper designed to add a single multiple-choice question:
{
"requests": [
{
"createItem": {
"item": {
"title": "Which protocol is used for secure web traffic?",
"questionItem": {
"question": {
"required": true,
"choiceQuestion": {
"type": "RADIO",
"options": [
{"value": "HTTP"},
{"value": "HTTPS"},
{"value": "FTP"}
]
}
}
}
},
"location": {
"index": 0
}
}
}
]
}
The payload hierarchy is strictly enforced by the API schema. The outer requests array can hold dozens of these operations. Inside createItem, the item object dictates the content.
The questionItem key signals that this is a standard user-facing question, rather than a textItem (a static block of text) or an imageItem. Inside question, you define validation like required: true.
The choiceQuestion object dictates the UI element. Setting the type to RADIO creates a standard multiple-choice question where only one answer is allowed. Changing it to CHECKBOX allows multiple selections, and DROP_DOWN creates a collapsed selection menu.
Finally, the location object is critical. The index integer determines where the new question appears. An index of 0 places it at the very top of the form. If you omit the location object entirely, the API appends the new question to the very bottom of the form by default.
How to add different question types in a single batch call?
Building forms dynamically usually means mixing multiple-choice questions, open-ended text fields, and rating scales. The true power of batchUpdate is combining all these different createItem shapes into one single network call.
The following Python script demonstrates how to authenticate, create a blank form, and then inject three distinct question types in one batch payload.
import os
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/forms.body']
creds = None
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
if not creds or not creds.valid:
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
with open('token.json', 'w') as token:
token.write(creds.to_json())
service = build('forms', 'v1', credentials=creds)
form_info = {
"info": {
"title": "API Generated Onboarding Form",
"documentTitle": "Onboarding Form 2024"
}
}
new_form = service.forms().create(body=form_info).execute()
form_id = new_form['formId']
print(f"Created blank form with ID: {form_id}")
batch_update_request = {
"requests": [
{
"createItem": {
"item": {
"title": "What is your primary email address?",
"questionItem": {
"question": {
"required": True,
"textQuestion": {
"paragraph": False
}
}
}
},
"location": {"index": 0}
}
},
{
"createItem": {
"item": {
"title": "Which department are you joining?",
"questionItem": {
"question": {
"required": True,
"choiceQuestion": {
"type": "RADIO",
"options": [
{"value": "Engineering"},
{"value": "Marketing"},
{"value": "Sales"}
]
}
}
}
},
"location": {"index": 1}
}
},
{
"createItem": {
"item": {
"title": "How comfortable are you with Python?",
"questionItem": {
"question": {
"required": True,
"scaleQuestion": {
"low": 1,
"high": 5,
"lowLabel": "Beginner",
"highLabel": "Expert"
}
}
}
},
"location": {"index": 2}
}
}
]
}
result = service.forms().batchUpdate(
formId=form_id,
body=batch_update_request
).execute()
print("Successfully added all questions in one batch.")
In practice, managing the index values requires attention. When you add multiple items in a single batch, the API processes them sequentially.
Because we want the email question first, we assign it index: 0. The department question gets index: 1, placing it immediately after. If we assigned them all index: 0, the last item in the array would end up at the top of the form, pushing the others down.
The text question uses "paragraph": False to create a standard short-answer line. Changing this to True expands the UI into a larger text box for longer responses. The linear scale requires specific boundaries defined by low and high, along with optional text labels for the extremes.
What are the best practices for handling API limits and idempotency?
When your script only adds five questions, a basic API call works perfectly. When you build systems that generate massive assessments or sync thousands of questions from a database, you will run into Google's rate limits and network instability.
Managing these constraints requires defensive programming. If a script crashes halfway through a migration, you need a way to retry without duplicating the questions that already succeeded.
Calculate your request chunks:
Google Forms does not document a strict maximum array size for a single
batchUpdaterequest, but payloads that are too large will trigger an HTTP 413 Payload Too Large error or simply timeout. Chunk yourrequestsarray into batches of 50 to 100 items.
- ❌ Weak: Sending fifty individual
createItemHTTP POST requests in aforloop. - ✅ Strong: Sending one
batchUpdateHTTP POST request containing an array of fiftycreateItemobjects.
Implement exponential backoff:
The Forms API enforces a quota on how many write requests a single project can make per minute. If you exceed this, the API returns a 429 Too Many Requests error. Wrap your
batchUpdateexecution in atry/exceptblock. If you catch a 429 error, pause the script for two seconds, try again, and double the wait time on subsequent failures.Fetch the current revision ID:
Every time a Google Form is modified, its internal
revisionIdchanges. Before you send a large batch update, fetch the form's metadata usingservice.forms().get(formId=form_id).execute(). Store therevisionIdstring returned in that response.Attach WriteControl to your payload:
Network requests can drop. Your script might send a batch, but the connection times out before Google confirms success. To prevent your script from retrying and accidentally injecting the same 50 questions twice, use idempotency controls. Add a
writeControlobject alongside yourrequestsarray.
{
"writeControl": {
"targetRevisionId": "00000021"
},
"requests": [
...
]
}
When Google receives this payload, it checks the live form. If the form's current revision is still 00000021, the update proceeds. If the previous attempt actually succeeded in the background, the form's revision will have incremented. Google will reject the new request with a 400 error, saving you from duplicating your entire question block.
How do you troubleshoot common batchUpdate API errors?
Working with deeply nested JSON arrays guarantees you will eventually format a request incorrectly. The Google Forms API is strict about schema adherence. A single missing boolean or out-of-bounds integer will cause the entire batch to fail - the API does not partially apply successful items.
Use this reference to map API error codes to their actual structural causes.
| API Error Code | Root Cause | Resolution Strategy |
|---|---|---|
| 400 Bad Request | Invalid JSON structure or out-of-bounds index. | Verify the location.index is not greater than the current number of items. Check that nested keys like choiceQuestion are spelled exactly as documented. |
| 400 Bad Request | Attempting to add grading properties to a non-quiz form. | Send an updateSettings request to set isQuiz: true before attempting to add point values or correct answers. |
| 401 Unauthorized | Expired or missing OAuth token. | Refresh the access token using your Google client library. Ensure token.json is being updated on disk. |
| 403 Forbidden | Missing the required API scope or hitting a hard quota limit. | Check your OAuth consent screen to ensure forms.body is approved. Implement exponential backoff if hitting the per-minute write limit. |
| 404 Not Found | The formId provided in the code is incorrect. |
Verify you are using the ID extracted from the form's edit URL (between /d/ and /edit), not the published view URL. |
| 429 Too Many Requests | Exceeding the Google Workspace API rate limits for your tier. | Batch your createItem objects into fewer HTTP requests and add time.sleep() between distinct batches. |
The most common silent failure happens when developers forget that Google API JSON keys are case-sensitive. Writing ChoiceQuestion instead of choiceQuestion will result in a 400 error stating that the requested field does not exist.
When should you build custom API scripts versus using document parsers?
Writing custom Python scripts is the right choice when forms must be generated entirely from a database, or when integrating form creation into an automated CI/CD pipeline.
However, if your source material is a stack of existing Word documents, PDFs, or plain-text briefs, forcing that unstructured data into a rigid JSON payload requires building a complex text parsing layer before you even touch the Google API.
| Situation | What to use | Why |
|---|---|---|
| You have structured data in a SQL database or a heavily formatted CSV file. | Manual API scripting | You can map database columns directly to JSON keys with high reliability and full control over the batchUpdate array. |
| You want to convert existing PDFs or Word documents into forms. | Automated parsing (e.g., document to Google Form) | Writing custom regex to extract questions and options from inconsistent PDF formatting is highly error-prone and time-consuming. |
| You need to trigger form creation programmatically from another app via webhooks. | Manual API scripting | A custom script can listen for a webhook payload, authenticate silently via a Service Account, and return the new formId instantly. |
| Non-technical staff need to turn meeting notes or raw text into structured surveys. | Automated parsing (e.g., Google Form from description) | Staff cannot maintain Python scripts or manage OAuth tokens; they need a UI that handles the API interactions behind the scenes. |
Building a robust text-to-JSON parser takes significantly more engineering time than mastering the Forms API itself. Evaluate where your question data actually lives before committing to a custom scripting path.
FAQ
Can I update existing questions using the batchUpdate endpoint?
Yes, you can modify existing questions by passing an updateItem object instead of a createItem object in your requests array. You must provide the specific itemId of the question you want to change, which you can find by fetching the form's metadata. You must also include an updateMask string (like "title,questionItem.question.required") to explicitly tell the API which fields to overwrite.
What is the maximum number of items I can add in a single batchUpdate request?
Google does not publish a strict numerical limit for the number of items in a single batchUpdate array. In practice, payloads that take too long to process or exceed 10MB will result in a server timeout or a 413 error. To ensure reliability across different network conditions, chunk your creation payloads into batches of 50 to 100 questions.
How do I set correct answers and point values programmatically using batchUpdate?
You cannot assign points to a standard form. You must first include an updateSettings request in your batch to set isQuiz: true. Once the form is a quiz, you can add a grading object inside your questionItem.question payload, specifying the pointValue integer and a correctAnswers array containing the exact string values of the right choices.
Integrating the Google Forms API requires upfront effort to manage OAuth credentials and navigate the deeply nested JSON schemas. Once configured, the batchUpdate endpoint provides a highly efficient way to turn raw data into interactive assessments instantly. If writing custom Python scripts and managing API quotas feels too heavy for your current project, tools like Doc2Form can handle the conversion of text and documents into Google Forms automatically, bypassing the need for manual API configuration entirely.