Data & automation
REST API v1
Programmatic access to forms and submissions. JSON over HTTPS, Bearer-token auth, predictable endpoints.
Base URL
https://gatherino.com/api/v1Authentication
All requests require an API key. Create one from Settings → API keys in the dashboard. Keys start with gk_ and are shown once on creation — store them in a secret manager or an environment variable on your server.
Pass the key either as a Bearer token or in an x-api-key header. Both are accepted; an invalid or revoked key returns 401.
bash# Either header works — pick one.
curl https://gatherino.com/api/v1/forms \
-H "Authorization: Bearer gk_************************"
curl https://gatherino.com/api/v1/forms \
-H "x-api-key: gk_************************"Heads up
Rate limits
The /api/v1 endpoints are currently exempt from the throttle that applies to the rest of the app, so there is no published per-minute quota. Treat that as a courtesy, not a guarantee: cache responses, avoid calling the API on every page view of your own site, and expect a limit to be introduced later.
Public form submissions (the endpoint respondents hit when they press Send) are throttled separately at 5 per minute and 30 per hour per IP.
Endpoints
/v1/formsList all forms in the workspace/v1/forms/:idGet a single form with its schema/v1/forms/:slug/submitSubmit data to a form programmatically/v1/submissionsList submissions with filters and pagination/v1/submissions/:idGet a single submission by ID/v1/submissions/:id/statusChange submission statusListing forms
bashGET https://gatherino.com/api/v1/formsExample response:
json{
"items": [
{
"id": "ckx...",
"name": "Customer feedback",
"slug": "customer-feedback",
"isPublished": true,
"createdAt": "2026-03-01T12:00:00.000Z",
"schema": { "sections": [ /* ... */ ], "logic": [] }
}
],
"total": 1,
"page": 1,
"totalPages": 1
}Listing submissions
bashGET https://gatherino.com/api/v1/submissions?formId=ckx...&status=NEW&page=1&limit=50Query parameters
formId— filter to a single formstatus—NEW,IN_PROGRESS,DONE,PAID,REJECTED,ARCHIVEDsearch— full-text search across answer valuesdateFrom/dateTo— ISO 8601 datespage— 1-based, default 1limit— max 100, default 50sortBy,sortOrder— e.g.submittedAt/desc
Example response:
json{
"items": [
{
"id": "sub_01HA...",
"formId": "ckx...",
"status": "NEW",
"createdAt": "2026-04-16T08:42:00.000Z",
"data": {
"field_1712345678901_a8fgh": "Jana Nováková",
"field_1712345678912_qw4er": "jana@example.com"
}
}
],
"total": 128,
"page": 1,
"limit": 50,
"totalPages": 3
}Field IDs and labels
Submissions store answers under field IDs like field_1712345678901_a8fgh, not under human labels — labels can be renamed without breaking historical data. Fetch the form detail once and build a lookup table:
js// The form detail carries the labels; submissions carry only field IDs.
const form = await (await fetch(
`https://gatherino.com/api/v1/forms/${formId}`,
{ headers: { Authorization: `Bearer ${key}` } },
)).json();
const labels = {};
for (const section of form.schema.sections ?? []) {
for (const field of section.fields ?? []) labels[field.id] = field.label;
}
// labels["field_1712345678901_a8fgh"] === "Jméno a příjmení"Field IDs are stable for the life of the field, so the mapping can be cached. A field deleted from the form keeps its answers on older submissions; those keys simply have no label any more.
Walking through pages
limit is capped at 100 regardless of what you send. Use totalPages from the response to know when to stop:
jslet page = 1;
const all = [];
while (true) {
const res = await fetch(
`https://gatherino.com/api/v1/submissions?formId=${formId}&page=${page}&limit=100`,
{ headers: { Authorization: `Bearer ${process.env.GATHERINO_KEY}` } },
);
const { items, totalPages } = await res.json();
all.push(...items);
if (page >= totalPages) break;
page++;
}Encoding
Requests and responses are UTF-8 JSON. Czech diacritics, emoji and other non-ASCII characters go through unchanged — if you see Jan VondráÄek in your output, the mis-decoding is on your side: make sure your HTTP client parses the body as UTF-8 and that the page rendering it declares charset=utf-8.
Publishing responses on your own site
Heads up
The safe shape is: browser → your server → Gatherino. Your server holds the key, picks the specific fields that are meant to be public, and returns only those. Do not forward the whole data object to the browser — it contains every answer, including the ones you did not intend to publish.
Publishing a list of names is a publication of personal data. Make sure you have a legal basis for it, or publish only what respondents were told would be public.
Submitting data
Post a body with a data object keyed by field ID. Missing required fields return 400 with a list of validation errors, one per field.
bashcurl -X POST https://gatherino.com/api/v1/forms/customer-feedback/submit \
-H "Authorization: Bearer gk_..." \
-H "Content-Type: application/json" \
-d '{
"data": {
"field_1712345678901_a8fgh": "Jane Doe",
"field_1712345678912_qw4er": "jane@example.com",
"field_1712345678923_r4t5y": 5
}
}'Updating status
bashcurl -X PATCH https://gatherino.com/api/v1/submissions/sub_01HA.../status \
-H "Authorization: Bearer gk_..." \
-H "Content-Type: application/json" \
-d '{ "status": "PAID" }'Errors
The API uses standard HTTP status codes:
400— malformed request, or a validation error; the body carries field-level messages underfields401— missing or invalid API key404— form or submission not found, or it belongs to another workspace409— conflict, e.g. a submission with the same contract ID already exists500— something went wrong on our side; safe to retry after a short delay
Versioning
The current version is v1, reflected in the URL path. Breaking changes will ship under a new version prefix; additive changes (new optional fields, new endpoints) may be rolled into v1.