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/v1

Authentication

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

Never embed API keys in client-side code. Anyone with a key can read every submission in the workspace. Revoke leaked keys immediately from the dashboard.

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

GET/v1/formsList all forms in the workspace
GET/v1/forms/:idGet a single form with its schema
POST/v1/forms/:slug/submitSubmit data to a form programmatically
GET/v1/submissionsList submissions with filters and pagination
GET/v1/submissions/:idGet a single submission by ID
PATCH/v1/submissions/:id/statusChange submission status

Listing forms

bashGET https://gatherino.com/api/v1/forms

Example 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=50

Query parameters

  • formId — filter to a single form
  • statusNEW, IN_PROGRESS, DONE, PAID, REJECTED, ARCHIVED
  • search — full-text search across answer values
  • dateFrom / dateTo — ISO 8601 dates
  • page — 1-based, default 1
  • limit — max 100, default 50
  • sortBy, 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

Call the API from your server, never from the browser. An API key in front-end JavaScript is readable by anyone who opens the page source, and it grants access to every submission in the workspace.

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 under fields
  • 401 — missing or invalid API key
  • 404 — form or submission not found, or it belongs to another workspace
  • 409 — conflict, e.g. a submission with the same contract ID already exists
  • 500 — 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.