Submissions API
Page through submissions, mark them read, starred or spam, delete them, download uploaded files and export CSV with the Nisuform API.
Every request needs an API key, and the form must be in the key's team. Otherwise the API answers 404.
The submission object
{
"id": "8d0f5a4e-3b9c-4f7a-9e21-6c4d2b1a0f93",
"formId": "2f6b7c1e-9a4d-4c3e-8b21-7d5e6f4a3b2c",
"data": {
"name": "Jane Doe",
"email": "jane@example.com",
"message": "Hello!"
},
"meta": {
"ip": "203.0.113.7",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
"referrer": "https://example.com/contact",
"country": "DE",
"deviceClass": "desktop"
},
"read": false,
"starred": false,
"isSpam": false,
"isTest": false,
"createdAt": "2026-09-29T09:29:59.412Z"
}| Field | Description |
|---|---|
id | The submission ID. |
formId | The form it was sent to. |
data | The answers, as field name and text value pairs. Special fields like _gotcha are never stored. |
meta.ip | The visitor's IP address. Left out when the form doesn't store IP addresses. |
meta.userAgent | The visitor's browser. |
meta.referrer | The page the form was sent from, or an empty string. |
meta.country | Two-letter country code, when known. |
meta.deviceClass | mobile, desktop, tablet or bot, when known. |
meta.files | Uploaded files, each with name, mime and size in bytes. Only present when files were attached. |
read | Whether it was opened or marked read. |
starred | Whether it is starred. |
isSpam | Whether it is in the spam folder. |
isTest | Whether it is a test submission sent from the dashboard or the API. |
createdAt | When it was received. |
When answers don't pass your form's field checks, the submission is still stored, arrives unread and has an extra data._invalid value: a JSON string that lists each problem, like [{"fieldId":"email","reason":"invalid_email"}].
List submissions
GET /v1/forms/{formId}/submissions| Query parameter | Default | Description |
|---|---|---|
filter | all | all (everything except spam), unread (unread and not spam), starred or spam. |
page | 1 | The page to return. |
pageSize | 15 | Submissions per page, 1 to 100. |
around | A submission ID. Returns the page that holds it instead of page, when it matches the filter. |
curl "https://api.nisuform.com/v1/forms/FORM_ID/submissions?filter=unread&pageSize=100" \
-H "Authorization: Bearer $NISUFORM_API_KEY"Submissions come newest first. Each item is a submission object with a preview, the longest answer cut to a short snippet.
{
"items": [
{
"id": "8d0f5a4e-3b9c-4f7a-9e21-6c4d2b1a0f93",
"preview": "Hello!",
"data": { "name": "Jane Doe", "email": "jane@example.com", "message": "Hello!" },
"read": false
}
],
"page": 1,
"pageSize": 100,
"pageCount": 1,
"total": 1,
"unreadCount": 1,
"asOf": "2026-09-29T10:00:00.000Z"
}| Field | Description |
|---|---|
items | The submissions on this page. |
page | The page you got. Asking for a page past the end returns the last page. |
pageSize | Submissions per page. |
pageCount | Number of pages, at least 1. |
total | Submissions that match the filter. |
unreadCount | Unread submissions in the form, whatever the filter. |
asOf | When the list was read. Pass it to update or delete by filter. |
Page through every submission
const API = 'https://api.nisuform.com/v1'
const headers = { Authorization: `Bearer ${process.env.NISUFORM_API_KEY}` }
async function allSubmissions(formId) {
const seen = new Map()
for (let page = 1; ; page++) {
const response = await fetch(`${API}/forms/${formId}/submissions?page=${page}&pageSize=100`, { headers })
if (!response.ok) throw new Error(`Nisuform answered ${response.status}`)
const body = await response.json()
for (const item of body.items) seen.set(item.id, item)
if (page >= body.pageCount) return [...seen.values()]
}
}Submissions that arrive while you page move older ones back, so the same submission can show up on two pages. Keep them by id, as above. For a complete copy in one request, use the CSV export.
Get a submission
GET /v1/forms/{formId}/submissions/{id}Returns one submission object, or 404. Getting a submission doesn't mark it read.
Update submissions
PATCH /v1/forms/{formId}/submissionsSets read, starred or isSpam on many submissions at once. Pass at least one of them, and a target.
| Body field | Description |
|---|---|
target | Either { "ids": [...] } with 1 to 1,000 submission IDs, or { "filter": "...", "asOf": "..." } to act on every submission that matches the filter and was received up to asOf. |
read | true or false. |
starred | true or false. |
isSpam | true moves them to spam, false moves them back. |
curl -X PATCH https://api.nisuform.com/v1/forms/FORM_ID/submissions \
-H "Authorization: Bearer $NISUFORM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target":{"ids":["8d0f5a4e-3b9c-4f7a-9e21-6c4d2b1a0f93"]},"read":true}'Answers { "count": 1 } with the number of submissions that changed. Submissions that already had the value aren't counted. IDs from other forms are ignored.
To mark everything read that you just fetched, pass the list's asOf, so submissions that arrived in the meantime stay unread:
{ "target": { "filter": "unread", "asOf": "2026-09-29T10:00:00.000Z" }, "read": true }Delete submissions
DELETE /v1/forms/{formId}/submissionsTakes a JSON body with the same target as update, and deletes the submissions with their uploaded files. Answers { "count": 3 } with the number deleted. This can't be undone.
curl -X DELETE https://api.nisuform.com/v1/forms/FORM_ID/submissions \
-H "Authorization: Bearer $NISUFORM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target":{"filter":"spam","asOf":"2026-09-29T10:00:00.000Z"}}'Deleted submissions still count toward this month's usage.
Download a file
GET /v1/forms/{formId}/submissions/{id}/files/{index}index is the file's position in meta.files, starting at 0. Answers with a signed link:
{
"url": "https://api.nisuform.com/attachments/...",
"name": "brief.pdf",
"mime": "application/pdf",
"expiresAt": "2026-09-29T10:15:00.000Z"
}The link works without an API key and expires after 15 minutes. Ask for a new one each time you need the file.
Export as CSV
GET /v1/forms/{formId}/export.csv| Query parameter | Default | Description |
|---|---|---|
filter | all | all, unread, starred or spam. |
curl "https://api.nisuform.com/v1/forms/FORM_ID/export.csv" \
-H "Authorization: Bearer $NISUFORM_API_KEY" \
-o submissions.csvStreams every matching submission as a CSV file, newest first, with the same columns as the dashboard export. It counts as one request toward the rate limit, however many rows it has.