Business · Read-only API
Your survey data, in your own tools.
Connect a reporting job or internal service to your surveys, responses and response media. Start with a connection check, then read the data you need.
Before you start
Try fictional data on any plan: open Account → Integration sandbox to run sample API routes, pagination and error examples. The sandbox uses no API key, makes no network request and reads no real survey data. Its webhook preview is not sent; use the survey’s webhook settings to send a synthetic event to your configured receiver.
The API requires an active Business subscription. Free and Pro do not include API access. In the app, open Account → Manage subscription (or Upgrade to Pro on Free), then choose Start Business on the plan screen, and complete the available checkout. If the screen says “Business checkout is not configured yet,” checkout is unavailable; a Pro subscription alone will not unlock the API.
Use a trusted server or private automation environment. Keep keys out of browser code, mobile app bundles, URLs, source control and logs. The API has no write endpoints or per-survey key scopes.
An API key authenticates requests your service sends to FormShark using Authorization: Bearer …. A webhook signing secret verifies events FormShark sends to your receiver. They are different credentials and cannot be exchanged. Set up outgoing webhooks on any plan.
Create, store and test a key
- Open Account → Business · media and API. Copy the API base URL displayed in this panel. Use that exact value; this guide does not supply an account URL.
- Name your integration. Enter a descriptive API key name, such as “Daily reporting,” then select Create API key. Names must contain 1–80 characters. You may be asked to sign in again before changing credentials.
- Copy the key once. Select Copy key and put it in your server’s secret store. The key cannot be revealed again: FormShark stores only a hash. Select Hide key after storing it. Leaving the app also clears its display.
- Verify access in the app. Enter the saved value into Paste key to test connection, then select Test connection. Success displays “Connection verified. No survey data was read.” The test input is cleared afterwards.
- Configure your own service. Set
ISURVEY_API_BASE_URLto the displayed base URL, without a trailing slash, andISURVEY_API_KEYto the complete saved key through your environment or secret manager. Run the request below.
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer ${ISURVEY_API_KEY}" \
"${ISURVEY_API_BASE_URL}/v1/connection"Expected HTTP 200 body: {"ok":true}. This verifies the key and Business access without reading survey answers. All examples read credentials from your environment; do not paste real credentials into this documentation.
Read your data
Append a route below to your account’s base URL. Every request uses GET and the bearer header. Responses are JSON. Replace {id} with a survey ID returned by the list endpoint.
| Route | Returns |
|---|---|
/v1/connection | {"ok":true}; no survey data. |
/v1/surveys | A page of owned surveys: data and nextCursor. |
/v1/surveys/{id} | One survey object, including its id and available definition fields. |
/v1/surveys/{id}/responses | A page of the owned survey’s responses: data and nextCursor. |
/v1/media/{assetId} | A temporary download url and expiresAt in Unix milliseconds, for committed media whose response still exists. |
Survey fields may include title, owner, formFields, createdTimeMillis, isPublic, deleted, maxResponses, expiresAtMillis, allowAnonymous and numberOfResponses. Responses may include title, formId, data, metaData, reference, startTimeMillis, submittedTimeMillis and attachments. Only id is guaranteed; handle absent or null optional fields. Answer structures depend on the survey.
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer ${ISURVEY_API_KEY}" \
--get --data-urlencode 'limit=50' \
"${ISURVEY_API_BASE_URL}/v1/surveys"Read one survey definition
Set ISURVEY_SURVEY_ID to an ID returned by the survey list. A successful result includes that ID and the available definition fields; for example {"id":"survey_example","title":"Example survey","formFields":[]}. This is fictional sample data.
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer ${ISURVEY_API_KEY}" \
"${ISURVEY_API_BASE_URL}/v1/surveys/${ISURVEY_SURVEY_ID}"
Read one survey’s responses
Copy a survey’s id from the list result into ISURVEY_SURVEY_ID. This request reads actual answers; run it only in your trusted environment. The ID is not the survey title or its public sharing URL.
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer ${ISURVEY_API_KEY}" \
--get --data-urlencode 'limit=50' \
"${ISURVEY_API_BASE_URL}/v1/surveys/${ISURVEY_SURVEY_ID}/responses"For example, a page containing one response could return {"data":[{"id":"response_example","formId":"survey_example","data":{"Question":"Example answer"}}],"nextCursor":null}. These values are illustrative. Use the survey definition and returned answer fields at runtime, and handle an empty data array.
Download response media
Use an assetId from a response’s attachments to request its download URL. Attachments are grouped by field ID. Download the file using the returned URL without forwarding your API bearer key to the storage host.
# Set ISURVEY_ASSET_ID from a response attachment.
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer ${ISURVEY_API_KEY}" \
"${ISURVEY_API_BASE_URL}/v1/media/${ISURVEY_ASSET_ID}"
Expected shape: {"url":"https://storage.example/temporary-signed-link","expiresAt":1788690000000}. This fictional URL is not usable. Use the actual returned URL before its expiry; never forward your API key to it. A missing, uncommitted or inaccessible asset returns an error instead.
The URL expires within five minutes, capped by Business coverage. Treat it as a credential. Revoking an API key prevents future authenticated reads but does not invalidate a download URL already issued; that URL remains usable until expiry.
Read every page
Both list endpoints accept limit (1–100; default 50) and cursor. Results are ordered by document ID, not submission time.
- Send your first request without a cursor.
- Process the returned
dataarray. - If
nextCursoris a string, send it unchanged as the next request’s URL-encodedcursorparameter on the same route, with the same owner. - Stop when
nextCursorisnull. An empty collection returns{"data":[],"nextCursor":null}.
Cursors are opaque and tied to the owner and route. Do not decode, edit or reuse them for another survey. A paginated read is not a frozen snapshot; data can change between requests.
# Set ISURVEY_CURSOR to the previous response's nextCursor value.
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer ${ISURVEY_API_KEY}" \
--get --data-urlencode 'limit=50' \
--data-urlencode "cursor=${ISURVEY_CURSOR}" \
"${ISURVEY_API_BASE_URL}/v1/surveys"The API allows 60 requests per minute per owner, shared across all keys and routes, including connection tests. On HTTP 429, wait the Retry-After interval (currently 60 seconds) before retrying.
Python: read all owned surveys
Save this as read_surveys.py and run python3 read_surveys.py after configuring the two environment variables above. It uses only the standard library, paginates automatically, retries rate limits and server errors up to three times, and prints a count without logging keys or survey answers.
import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
base_url = os.environ["ISURVEY_API_BASE_URL"].rstrip("/")
api_key = os.environ["ISURVEY_API_KEY"]
parsed = urlsplit(base_url)
if (parsed.scheme != "https" or not parsed.hostname
or parsed.username or parsed.password
or parsed.query or parsed.fragment):
raise SystemExit("Use the HTTPS base URL from Account.")
# Never forward the bearer key to a redirected destination.
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
client = build_opener(NoRedirect())
def get_page(path, params):
request = Request(
base_url + path + "?" + urlencode(params),
headers={"Authorization": "Bearer " + api_key},
)
for attempt in range(4):
try:
with client.open(request, timeout=20) as response:
return json.load(response)
except HTTPError as error:
status = error.code
retry_after = error.headers.get("Retry-After", "60")
error.close()
if attempt == 3 or (status != 429 and status < 500):
raise SystemExit(f"HTTP {status}; see troubleshooting.") from None
delay = int(retry_after) if retry_after.isdigit() else 60
time.sleep(max(1, delay) if status == 429 else 2 ** attempt)
except URLError:
raise SystemExit("Connection failed; check your network and base URL.") from None
cursor = None
count = 0
while True:
params = {"limit": 100}
if cursor is not None:
params["cursor"] = cursor
page = get_page("/v1/surveys", params)
# Process page["data"] here in your trusted environment.
count += len(page["data"])
cursor = page["nextCursor"]
if cursor is None:
break
print(f"Read {count} surveys.")To page through one survey’s responses, use /v1/surveys/YOUR_SURVEY_ID/responses in place of /v1/surveys and begin without a cursor.
Rotate or revoke credentials
Replace a working key
- In Business · media and API, choose Rotate beside the active key.
- Select Copy key to save the replacement immediately, then update your service’s secret store.
- Use Test connection with the replacement and verify your service can read its data.
- Select Revoke beside the previous key once the change is complete.
The previous key works for up to 24 hours, capped by any earlier expiry. Check Expires in the key list. There is only one replacement overlap per key lineage: revoke the previous key before rotating its replacement again.
If a key is lost or exposed
For an exposed key, select Revoke immediately, then create a new key and update your service. For a lost one-time replacement display, revoke the replacement and rotate the previous key again before its deadline; this does not extend that deadline. If the previous key has already expired, create a new key.
Key listing and revocation remain available after Business expires. Creating, rotating and using keys require active Business access. Creation is capped at 20 active keys; revoke unused keys before creating more. Credential changes may require a fresh sign-in; cancelling sign-in leaves working credentials unchanged.
Troubleshoot a request
API failures normally return a JSON body such as {"error":"invalid_api_key"}. Use the HTTP status and error value together.
| Status | Error | Next step |
|---|---|---|
| 400 | invalid_limit, invalid_cursor | Use an integer limit from 1–100. Pass the unchanged cursor for the same route and owner, or restart without one. |
| 401 | invalid_api_key | Use the complete API key in the bearer header. Check whether it is expired or revoked; webhook secrets cannot authenticate API requests. |
| 403 | permission-denied | Check that the key owner has active Business coverage. |
| 404 | not_found or not-found | Check the route, item ID and ownership. Media must be committed and its response must still exist. |
| 405 | method_not_allowed | Use GET. Creating or updating data through this API is unsupported. |
| 429 | rate_limit_exceeded | Wait for Retry-After, then retry. All of the owner’s keys share the limit. |
| 500 | internal_error | Retry with bounded backoff. If it persists, report the route and status without keys or respondent data. |
If the app shows “Connection unavailable,” confirm your network and the base URL in Account. Use server-side requests for your integration: browser CORS access is restricted to configured app origins.
Download the OpenAPI 3.1 reference for request parameters and response schemas. Replace its clearly marked placeholder server URL with the base URL shown in your account before using it in an API client.