FlexiSurveyUser guide

Developer Guide — API & Webhooks

Authenticate against the REST API, page and pull data incrementally, and verify signed webhooks — with copy-paste JavaScript and Python samples.

For: Integration engineers, BI developers, IT staffPlan: Team and above (API access)

FlexiSurvey exposes a versioned REST API and signed webhooks so you can integrate survey data into your own systems without waiting on anyone. This guide covers everything an implementer needs: authentication, the response envelope, pagination, incremental pulls, rate limits, idempotent submission, and webhook signature verification.

Base URL and versioning

All endpoints live under a versioned URI prefix:

https://api.flexisurvey.net/api/v1

Versioning is URI-based. v1 is the current and only supported version. Additive changes (new fields, new endpoints) can appear at any time; breaking changes only arrive as a new URI version. Responses echo the resolved version in the X-API-Version header, and deprecated call shapes carry an X-API-Deprecation-Warning header well before removal.

The complete machine-readable specification is public:

The same OpenAPI document is committed to the FlexiSurvey repository and checked against the runtime on every build, so the published spec cannot drift from behaviour.

Authentication

There are two credential types. Use API keys for machine integrations.

Credential Header Intended for
API key (fsk_…) X-API-Key: fsk_… Server-to-server integrations, BI pulls, ingestion
User JWT Authorization: Bearer … (cookie-based in the dashboard) The web dashboard and mobile apps

Mint API keys in the dashboard under Settings → API Keys (requires a Team plan or above). Each key has a role that bounds what it can do:

Key role Capabilities Rate limit
READ_ONLY Read surveys, responses, analytics 1,000 requests/hour
INGEST Submit responses and respondent data 5,000 requests/hour
FULL Read + write across the granted scope 2,000 requests/hour

Limits are per key and can be adjusted per key by an administrator. Keys are stored hashed; the plaintext is shown exactly once at creation.

A first request in fifteen seconds:

curl -H "X-API-Key: fsk_your_key_here" \
  "https://api.flexisurvey.net/api/v1/surveys?page=1&limit=10"
import requests

API = "https://api.flexisurvey.net/api/v1"
headers = {"X-API-Key": "fsk_your_key_here"}

surveys = requests.get(f"{API}/surveys", headers=headers).json()["data"]

Response envelope

Every JSON endpoint wraps its payload in one envelope:

{
  "success": true,
  "data": { "…the actual payload…": "…" },
  "meta": { "timestamp": "2026-07-22T12:00:00.000Z" }
}

Errors use HTTP status codes plus a machine-readable message key that is stable across releases (the human text is localized):

{
  "success": false,
  "message": "Survey not found",
  "statusCode": 404
}

File-download endpoints (CSV/XLSX/ZIP exports) stream the file directly with no envelope.

Pagination

List endpoints return { data: [...], total, page, limit } (some legacy routes use skip/take query parameters — the OpenAPI reference is authoritative per endpoint). Page until data.length < limit or you have total rows.

Incremental pulls (deltas)

For warehouse and BI pipelines, do not re-download everything nightly. The responses list supports keyset-paginated delta reads:

GET /api/v1/surveys/{surveyId}/responses?updatedSince=2026-07-01T00:00:00Z

The reply is { data, nextCursor, hasMore }, ordered by (updatedAt, id). Persist nextCursor and resume with it:

GET /api/v1/surveys/{surveyId}/responses?cursor={nextCursor}

Cursor pages are strictly non-overlapping under concurrent writes, and review-state changes (approve/reject/return) bump updatedAt, so state transitions arrive as deltas too. The CSV/JSON export routes accept the same updatedSince parameter, plus includeReview=1 to add Review Status and QA Flags columns.

import requests

API = "https://api.flexisurvey.net/api/v1"
headers = {"X-API-Key": "fsk_your_key_here"}
survey_id = "…"

params = {"updatedSince": "2026-07-01T00:00:00Z"}
while True:
    page = requests.get(
        f"{API}/surveys/{survey_id}/responses",
        headers=headers, params=params,
    ).json()["data"]
    upsert_rows(page["data"])          # your warehouse upsert
    if not page["hasMore"]:
        break
    params = {"cursor": page["nextCursor"]}

Idempotent submission

When submitting responses from your own capture layer, send a stable client-generated UUID as clientResponseId. Re-submitting the same clientResponseId for a survey returns the existing response instead of inserting a duplicate — safe retries for flaky networks and offline queues.

Rate limits

Every key role has an hourly budget (table above). When you exceed it the API returns 429 Too Many Requests; back off and retry after the window resets. Export endpoints additionally carry a per-user limit (10 exports/hour) and a 10,000-row cap per file — capped downloads say so loudly via X-Export-Truncated: true headers, a _PARTIAL filename marker, and an in-file notice.

Webhooks

Webhooks push events to your endpoint instead of you polling. Configure them in the dashboard (or via POST /api/v1/webhooks), choosing the events you want. The authoritative event catalog — names, descriptions, payload shapes — is self-describing at:

GET /api/v1/webhooks/meta/events

Event families: survey.* (created/updated/published/closed/deleted), response.* (started/submitted/updated/deleted, plus the review-pipeline events approved/rejected/returned/flagged/quarantined), user.*, team.member.*, analytics.*, export.*, quota.*, and system.*.

Delivery contract

Each delivery is an HTTP POST with a JSON body and these headers:

Header Meaning
X-FlexiSurvey-Signature sha256=<hex HMAC> of "{timestamp}.{rawBody}" using your webhook secret
X-FlexiSurvey-Timestamp Milliseconds since epoch when the delivery was signed
X-FlexiSurvey-Event The event name, e.g. response.submitted
X-FlexiSurvey-Webhook-ID Your webhook configuration id
X-FlexiSurvey-Delivery-ID Unique per delivery attempt — use it for de-duplication

Respond with any 2xx within 30 seconds. Anything else is retried with exponential backoff. Payloads are capped at 5 MB.

Verifying signatures

Always verify before trusting a delivery. Reject deliveries whose timestamp is older than 5 minutes (replay protection), then compare HMACs with a constant-time comparison.

Node.js:

const crypto = require('crypto');

function verifyFlexiSurveySignature(rawBody, headers, secret) {
  const timestamp = headers['x-flexisurvey-timestamp'];
  const signatureHeader = headers['x-flexisurvey-signature'] || '';
  const [algorithm, received] = signatureHeader.split('=');

  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) {
    return false; // stale — possible replay
  }

  const expected = crypto
    .createHmac(algorithm, secret) // 'sha256'
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(received || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: capture the RAW body — verifying a re-serialized JSON.parse'd
// body will fail on key order and whitespace.
// app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf.toString(); } }));

Python (Flask shown; any framework works the same way):

import hashlib, hmac, time

def verify_flexisurvey_signature(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("X-FlexiSurvey-Timestamp", "")
    algorithm, _, received = headers.get("X-FlexiSurvey-Signature", "").partition("=")

    if abs(time.time() * 1000 - float(timestamp or 0)) > 5 * 60 * 1000:
        return False  # stale — possible replay

    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, getattr(hashlib, algorithm)).hexdigest()
    return hmac.compare_digest(expected, received)

Retries and recovery

Failed deliveries are retried automatically with exponential backoff and jitter. From the webhook detail page you can inspect delivery history and manually retry or bulk-retry failures. Use X-FlexiSurvey-Delivery-ID to de-duplicate on your side, and treat webhook delivery as at-least-once: pair webhooks (push) with the incremental endpoints above (pull) for reconciliation.

Getting help

The interactive reference at /api/docs supports authenticated try-it-out calls. For anything the spec does not answer, contact support with your request id (X-Request-Id response header) — it links your call to our logs.