Responses API
The Responses API lets you list, retrieve, create, and delete form submissions. Use it to build custom dashboards, export data, or programmatically submit entries.
Base URL: https://wittyform.com/api/v1
List responses
GET /api/v1/forms/:form_id/responses
Returns paginated responses for a specific form. Requires read scope.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
per_page | integer | 25 | Results per page (max 100) |
sort | string | desc | Sort by submission time. Options: asc, desc |
since | string | none | ISO 8601 timestamp. Only return responses submitted after this date |
until | string | none | ISO 8601 timestamp. Only return responses submitted before this date |
Example request
curl -X GET "https://wittyform.com/api/v1/forms/b3f1c2d4-1a2b-4c3d-9e8f-001122334455/responses?page=1&per_page=10&since=2025-09-01T00:00:00Z" \
-H "Authorization: Bearer wf_live_abc123"Example response
Each response's answers object is keyed by field ID. Use the Forms API to look up the label for each field ID.
{
"success": true,
"data": {
"responses": [
{
"id": "e6a4f5b7-4d5e-4f70-91b2-334455667788",
"form_id": "b3f1c2d4-1a2b-4c3d-9e8f-001122334455",
"answers": {
"9a1b2c3d-4e5f-4061-8273-8495a6b7c8d9": "Jane Smith",
"1f2e3d4c-5b6a-4798-9a8b-7c6d5e4f3a2b": "[email protected]",
"7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f": "I'd like to learn more about your Pro plan."
},
"metadata": {},
"device_type": "desktop",
"browser": "Chrome",
"os": "macOS",
"started_at": "2025-09-15T14:31:40.000Z",
"submitted_at": "2025-09-15T14:32:07.000Z",
"completion_time_seconds": 27
},
{
"id": "f7b5a6c8-5e6f-4081-a2c3-445566778899",
"form_id": "b3f1c2d4-1a2b-4c3d-9e8f-001122334455",
"answers": {
"9a1b2c3d-4e5f-4061-8273-8495a6b7c8d9": "John Doe",
"1f2e3d4c-5b6a-4798-9a8b-7c6d5e4f3a2b": "[email protected]",
"7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f": "Can I schedule a demo?"
},
"metadata": {},
"device_type": "mobile",
"browser": "Safari",
"os": "iOS",
"started_at": "2025-09-14T09:14:30.000Z",
"submitted_at": "2025-09-14T09:15:00.000Z",
"completion_time_seconds": 30
}
],
"pagination": {
"total": 142,
"page": 1,
"per_page": 10,
"total_pages": 15
}
}
}Cursor pagination
Page-based (page / per_page) pagination is simple, but it has a known weakness on large or live datasets: when new responses arrive while you are paging through, the offset shifts and you can skip rows or see the same row twice. For stable pagination, pass a cursor parameter instead. The list endpoint then uses keyset pagination, which is anchored to the last row you saw rather than a numeric offset, so concurrently-arriving responses cannot cause skips or duplicates.
When you pass cursor, the pagination object changes shape: instead of { total, page, per_page, total_pages } you get { per_page, next_cursor }. Pass the returned next_cursor back as the ?cursor= value to fetch the next page. A next_cursor of null means there are no more rows. The per_page, sort, since, and until parameters still apply in cursor mode. For a full export, cursor mode is the recommended approach.
First page (cursor mode)
curl -X GET "https://wittyform.com/api/v1/forms/b3f1c2d4-1a2b-4c3d-9e8f-001122334455/responses?cursor=&per_page=100" \
-H "Authorization: Bearer wf_live_abc123"Or omit cursor on the very first call and switch to cursor mode once you have a next_cursor. The response carries the keyset-style pagination block:
{
"success": true,
"data": {
"responses": [ /* up to per_page responses */ ],
"pagination": {
"per_page": 100,
"next_cursor": "MjAyNS0wOS0xNFQwOToxNTowMC4wMDBafGY3YjVhNmM4LTVlNmYtNDA4MS1hMmMzLTQ0NTU2Njc3ODg5OQ"
}
}
}Next page
curl -X GET "https://wittyform.com/api/v1/forms/b3f1c2d4-1a2b-4c3d-9e8f-001122334455/responses?per_page=100&cursor=MjAyNS0wOS0xNFQwOToxNTowMC4wMDBafGY3YjVhNmM4LTVlNmYtNDA4MS1hMmMzLTQ0NTU2Njc3ODg5OQ" \
-H "Authorization: Bearer wf_live_abc123"Keep following next_cursor until it comes back null. Treat the cursor as an opaque token: do not parse or construct it yourself, just echo back the value the API gave you.
Get a single response
GET /api/v1/forms/:form_id/responses/:id
Returns a single response by ID. Requires read scope.
Example request
curl -X GET "https://wittyform.com/api/v1/forms/b3f1c2d4-1a2b-4c3d-9e8f-001122334455/responses/e6a4f5b7-4d5e-4f70-91b2-334455667788" \
-H "Authorization: Bearer wf_live_abc123"Example response
{
"success": true,
"data": {
"response": {
"id": "e6a4f5b7-4d5e-4f70-91b2-334455667788",
"form_id": "b3f1c2d4-1a2b-4c3d-9e8f-001122334455",
"answers": {
"9a1b2c3d-4e5f-4061-8273-8495a6b7c8d9": "Jane Smith",
"1f2e3d4c-5b6a-4798-9a8b-7c6d5e4f3a2b": "[email protected]",
"7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f": "I'd like to learn more about your Pro plan."
},
"metadata": {},
"device_type": "desktop",
"browser": "Chrome",
"os": "macOS",
"started_at": "2025-09-15T14:31:40.000Z",
"submitted_at": "2025-09-15T14:32:07.000Z",
"completion_time_seconds": 27
}
}
}Create a response
POST /api/v1/forms/:form_id/responses
Programmatically submit a response to a form. Requires write scope. This is useful for importing data from other systems or submitting from a server-side process.
Request body
Provide an answers object keyed by field ID, and optionally a metadata object. Send the request with Content-Type: application/json (other content types return a 415). The form must be published to accept new responses.
{
"answers": {
"9a1b2c3d-4e5f-4061-8273-8495a6b7c8d9": "Alice Johnson",
"1f2e3d4c-5b6a-4798-9a8b-7c6d5e4f3a2b": "[email protected]",
"7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f": "Submitted via API"
}
}Idempotent submits
To make a submit safe to retry, send an Idempotency-Key header with a unique value (a UUID works well). If the request reaches WittyForm but your client never sees the response (a dropped connection, a timeout), retry with the same key: instead of recording a duplicate response, the API replays the original result and adds an Idempotent-Replayed: true header. Reusing the same key with a different request body returns 422 (VALIDATION_ERROR). Keys are remembered for 24 hours. This makes it safe to retry imports without worrying about double-submitting.
Example request
curl -X POST "https://wittyform.com/api/v1/forms/b3f1c2d4-1a2b-4c3d-9e8f-001122334455/responses" \
-H "Authorization: Bearer wf_live_abc123" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7b9c1d2e-3f40-4a51-8b62-9c0d1e2f3a4b" \
-d '{
"answers": {
"9a1b2c3d-4e5f-4061-8273-8495a6b7c8d9": "Alice Johnson",
"1f2e3d4c-5b6a-4798-9a8b-7c6d5e4f3a2b": "[email protected]",
"7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f": "Submitted via API"
}
}'Example response (201 Created)
{
"success": true,
"data": {
"response": {
"id": "a8c6b7d9-6f70-4192-b3d4-556677889900",
"form_id": "b3f1c2d4-1a2b-4c3d-9e8f-001122334455",
"answers": {
"9a1b2c3d-4e5f-4061-8273-8495a6b7c8d9": "Alice Johnson",
"1f2e3d4c-5b6a-4798-9a8b-7c6d5e4f3a2b": "[email protected]",
"7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f": "Submitted via API"
},
"metadata": {
"source": "api"
},
"device_type": null,
"browser": null,
"os": null,
"started_at": "2025-09-15T18:00:00.000Z",
"submitted_at": "2025-09-15T18:00:00.000Z",
"completion_time_seconds": null
}
}
}Responses created via the API have metadata.source set to "api" so you can distinguish them from browser submissions.
Delete a response
DELETE /api/v1/forms/:form_id/responses/:id
Permanently deletes a single response. Requires admin scope. This action cannot be undone.
Example request
curl -X DELETE "https://wittyform.com/api/v1/forms/b3f1c2d4-1a2b-4c3d-9e8f-001122334455/responses/e6a4f5b7-4d5e-4f70-91b2-334455667788" \
-H "Authorization: Bearer wf_live_abc123"Example response (204 No Content)
An empty response body with status 204 indicates success.
Exporting all responses
To pull a full dataset over the API, use cursor pagination: request the first page (omit cursor or send an empty one) with the maximum per_page of 100, then keep passing the returned next_cursor as ?cursor= until it comes back null. Cursor mode is recommended for full exports because it will not skip or duplicate rows when new responses arrive mid-export, which offset pagination can.
Page-based pagination (page / per_page until pagination.total_pages is reached) also works for smaller or static datasets. Either way, you can narrow the range with since and until, then build your own CSV or JSON from the answers objects.
For one-off downloads, the dashboard also offers a CSV and JSON export on each form's Responses tab.
Error codes
| Status | Code | Description |
|---|---|---|
401 | UNAUTHENTICATED | No API key was provided |
401 | KEY_INVALID | The key is wrong, revoked, or expired |
403 | INSUFFICIENT_SCOPE | The key lacks the scope this endpoint requires |
403 | RESPONSE_LIMIT_REACHED | The form owner has reached their response limit |
404 | NOT_FOUND | Form or response ID does not exist |
415 | UNSUPPORTED_MEDIA_TYPE | The request was not sent as application/json |
422 | VALIDATION_ERROR | Missing required field, invalid field value, or malformed JSON |
422 | FORM_NOT_PUBLISHED | The form is not published, so it cannot accept responses |
422 | FORM_CLOSED | The form is closed and no longer accepts responses |
429 | RATE_LIMITED | Too many requests |