Submissions
Once a respondent submits a form, you retrieve the data through the API. This page covers how to list submissions and how to read the answers back.
Listing submissions
List the completed submissions for a form:
curl -X GET "https://app.penneo.com/collect/api/v1/forms/{formId}/submissions?limit=1000&offset=0" \
-H "Authorization: Bearer YOUR_TOKEN"Only completed submissions are returned. Query parameters:
| Parameter | Description |
|---|---|
limit | Page size, 1–1000. Default 1000. |
offset | Number of results to skip. Default 0. |
completedAfter | Only submissions completed strictly after this UTC time (ISO-8601, e.g. 2026-01-01T00:00:00Z). |
completedBefore | Only submissions completed strictly before this UTC time (ISO-8601). |
The response is a page of submission summaries. Use completedAfter to fetch only the submissions completed since your last check:
{
"items": [
{
"submissionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"requestId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"submissionStartedAt": "2024-03-01T10:00:00Z",
"submissionCompletedAt": "2024-03-01T10:05:32Z",
"casefileCompletedAt": "2024-03-01T10:10:00Z"
}
],
"limit": 1000,
"offset": 0
}Take a submissionId from the list and fetch its full answers below.
Retrieving answers
Two endpoints return the same response shape. Use whichever ID you have.
By submission — using a submissionId from the list above:
curl -X GET https://app.penneo.com/collect/api/v1/forms/{formId}/submissions/{submissionId}/answers \
-H "Authorization: Bearer YOUR_TOKEN"By request — for a prefilled request, using its requestId:
curl -X GET https://app.penneo.com/collect/api/v2/forms/{formId}/requests/{requestId}/answers \
-H "Authorization: Bearer YOUR_TOKEN"The form must be ACTIVE or ARCHIVED, and you must have access to it.
Downloading the signed document
Once signing has completed, download the signed PDF directly. The response is streamed as application/pdf with a Content-Disposition attachment filename. Both endpoints take the same IDs you already use to read answers.
By submission — using a submissionId:
curl -X GET https://app.penneo.com/collect/api/v1/forms/{formId}/submissions/{submissionId}/signed-document \
-H "Authorization: Bearer YOUR_TOKEN" \
-o signed-document.pdfBy request — using a requestId:
curl -X GET https://app.penneo.com/collect/api/v2/forms/{formId}/requests/{requestId}/signed-document \
-H "Authorization: Bearer YOUR_TOKEN" \
-o signed-document.pdfThe signed document is available only after signing has finished. Until then, these endpoints return 404 Not Found.
Response shape
The response is grouped by section. Each section and each field carries a multipleEntries flag that decides how its answers are shaped.
Single-entry (the common case)
{
"formId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"submissionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"requestId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"version": 1,
"submissionStartedAt": "2024-03-01T10:00:00Z",
"submissionCompletedAt": "2024-03-01T10:05:32Z",
"casefileCompletedAt": "2024-03-01T10:10:00Z",
"sections": [
{
"title": "Personal",
"multipleEntries": false,
"answers": [
{ "name": "full_name", "label": "Full Name", "type": "TEXT", "multipleEntries": false, "value": "Jane Smith" },
{ "name": "email", "label": "Email", "type": "EMAIL", "multipleEntries": false, "value": "[email protected]" },
{ "name": "department", "label": "Department","type": "CHOICE", "multipleEntries": false, "value": ["product"] },
{ "name": "id_card", "label": "ID Card", "type": "FILE", "multipleEntries": true,
"values": [{ "filename": "id.pdf", "size": 12345, "link": "https://…signed-link…" }] }
]
}
],
"respondents": [
{ "fullName": "Jane Smith", "email": "[email protected]", "initiator": true, "added": false },
{ "fullName": "John Doe", "email": "[email protected]", "initiator": false, "added": true }
]
}Multi-entry section
A multi-entry section carries instances instead of answers, one entry per submitted instance:
{
"sections": [
{
"title": "Applicants",
"multipleEntries": true,
"instances": [
{ "answers": [{ "name": "name", "label": "Name", "type": "TEXT", "multipleEntries": false, "value": "Ada" }] },
{ "answers": [{ "name": "name", "label": "Name", "type": "TEXT", "multipleEntries": false, "value": "Grace" }] }
]
}
]
}Multi-entry field
A multi-entry field carries values (plural), the ordered list:
{
"sections": [
{
"title": "Contact",
"multipleEntries": false,
"answers": [
{ "name": "phones", "label": "Phones", "type": "PHONE", "multipleEntries": true, "values": ["+4511111111", "+4522222222"] }
]
}
]
}Property reference
| Property | Type | Description |
|---|---|---|
formId | UUID | The form this submission belongs to. |
submissionId | UUID | The submission's ID. |
requestId | UUID or null | The request this submission came from, if any. |
version | int | The form version this submission was filled against. |
submissionStartedAt | ISO 8601 | When the form was first opened. |
submissionCompletedAt | ISO 8601 or null | When it was submitted. null if not yet submitted. |
casefileCompletedAt | ISO 8601 or null | When the signing process finished. null until then. |
sections[].title | string | The section's title. |
sections[].multipleEntries | bool | true when the section is multi-entry; it then carries instances. |
sections[].answers[] | array | Present when multipleEntries: false. The section's answers. |
sections[].instances[].answers[] | array | Present when multipleEntries: true. The answers for each instance. |
answers[].name | string | The field's internal name. |
answers[].label | string | The field's display label. |
answers[].type | string | The field type. |
answers[].multipleEntries | bool | true when the field is multi-entry; it then carries values (plural). |
answers[].value | varies | Present when multipleEntries: false. Shape depends on type (see below). |
answers[].values | array | Present when multipleEntries: true. The ordered list of values. |
respondents | array | Signers associated with the submission, if any. |
respondents[].fullName | string | The signer's full name. |
respondents[].email | string | The signer's email. |
respondents[].initiator | bool | true if this signer initiated the request. |
respondents[].added | bool | true if this signer was added while the form was filled, rather than predefined on the form. |
Value shapes by type
| Type | Shape (single value) |
|---|---|
TEXT | string |
EMAIL | string |
PHONE | string |
DATE | ISO 8601 date string (YYYY-MM-DD) |
DATETIME | ISO 8601 datetime string |
NUMBER | JSON number |
CHOICE | array of strings: the selected option value(s) |
NIN | { "value": string, "countryCode": string } |
BUSINESS_ID | { "value": string, "countryCode": string } |
FILE | { "filename": string, "size": int, "link": string } |
For multi-entry fields, each element of values has the shape above.
If
submissionCompletedAtisnull, the form has been opened but not submitted;sectionsmay be empty or partial.
Errors
| Status | When |
|---|---|
404 Not Found | The form, submission, or request doesn't exist, you lack access, or the signed document isn't ready yet. |
Updated about 2 months ago
