Responses API

8 min read

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

ParameterTypeDefaultDescription
pageinteger1Page number
per_pageinteger25Results per page (max 100)
sortstringdescSort by submission time. Options: asc, desc
sincestringnoneISO 8601 timestamp. Only return responses submitted after this date
untilstringnoneISO 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

StatusCodeDescription
401UNAUTHENTICATEDNo API key was provided
401KEY_INVALIDThe key is wrong, revoked, or expired
403INSUFFICIENT_SCOPEThe key lacks the scope this endpoint requires
403RESPONSE_LIMIT_REACHEDThe form owner has reached their response limit
404NOT_FOUNDForm or response ID does not exist
415UNSUPPORTED_MEDIA_TYPEThe request was not sent as application/json
422VALIDATION_ERRORMissing required field, invalid field value, or malformed JSON
422FORM_NOT_PUBLISHEDThe form is not published, so it cannot accept responses
422FORM_CLOSEDThe form is closed and no longer accepts responses
429RATE_LIMITEDToo many requests

Ready to Build Forms That Actually Convert?

Get lifetime access for just $37. No subscriptions or recurring fees. Create beautiful forms and own the tool forever.

14-day money-back· Instant access
Responses API