Log in Get Started

Developer Documentation

REST API, webhooks, SSO, and SCIM - working examples, not placeholders.

Authentication

Either generate a token from Panel → API Tokens, or trade your login credentials for one directly via POST /survey/api/v1/auth/token - both mint the same kind of token, listed and revocable in the panel either way. The password-grant endpoint doesn't work for accounts with MFA enabled; use the panel for those. Send the token as a bearer token on every other request. Requires a plan with API access.

curl -X POST https://yourapp.test/survey/api/v1/auth/token \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com", "password": "your-password"}' # 201 Created { "data": { "access_token": "sk_survey_xxxxxxxxxxxxxxxxxxxx", "token_type": "Bearer" } } curl https://yourapp.test/survey/api/v1/surveys \ -H "Authorization: Bearer sk_survey_xxxxxxxxxxxxxxxxxxxx"

Account

GET /survey/api/v1/account

Your plan, granted capabilities, per-metric limits and current usage, and your effective API rate limit - the fastest way to see why a request just returned 403/422 instead of guessing.

Idempotency

Every write request (POST/PUT/DELETE) requires an Idempotency-Key header. Replaying the same key returns the original response instead of repeating the action - safe to retry after a timeout without creating duplicates. A response includes Idempotent-Replayed: true when it was served from cache rather than freshly executed.

curl -X POST https://yourapp.test/survey/api/v1/surveys \ -H "Authorization: Bearer sk_survey_xxxx" \ -H "Idempotency-Key: 3f29a1-my-client-generated-id" \ -H "Content-Type: application/json" \ -d '{"title": "Customer Satisfaction Q3", "type": "free"}'

Surveys

GET /survey/api/v1/surveys

List your surveys. Supports page-based pagination by default. Pass ?use_cursor=1 for cursor pagination, and ?updated_since=2026-01-01T00:00:00Z to sync only what changed.

{ "data": [ { "id": "b6b1...uuid", "title": "Customer Satisfaction Q3", "status": "published", "type": "free", "response_count": 42, "created_at": "2026-08-01T10:00:00+00:00" } ], "meta": { "current_page": 1, "last_page": 1, "total": 1 } }

POST /survey/api/v1/surveys

Create a survey as a draft. Fields: title (required), description, type (free, subscription, or pay_per_response, required).

GET /survey/api/v1/surveys/{'{id}'} · PUT /survey/api/v1/surveys/{'{id}'}

Fetch a single survey with its questions, or update title/description/is_private/password (plus thank_you_message, thank_you_redirect_url, and closes_at when your plan includes those capabilities).

POST /survey/api/v1/surveys/{'{id}'}/publish

Publishes a survey. Returns 422 if it has no questions yet - add at least one with the Questions endpoints below first.

curl -X POST https://yourapp.test/survey/api/v1/surveys/b6b1.../publish \ -H "Authorization: Bearer sk_survey_xxxx" \ -H "Idempotency-Key: publish-b6b1-attempt-1"

POST /survey/api/v1/surveys/{'{id}'}/close · DELETE /survey/api/v1/surveys/{'{id}'}

Stop accepting responses, or permanently delete the survey along with its questions, responses, and answers.

Questions

Full question CRUD - the piece that lets a survey created via the API actually reach a publishable state without touching the panel UI.

POST /survey/api/v1/surveys/{'{id}'}/questions

curl -X POST https://yourapp.test/survey/api/v1/surveys/b6b1.../questions \ -H "Authorization: Bearer sk_survey_xxxx" \ -H "Idempotency-Key: q-1" \ -d '{"type": "single_choice", "title": "How satisfied are you?", "options": ["Very", "Somewhat", "Not at all"], "is_required": true}'

Fields depend on type: choice/ranking types take options[], matrix takes matrix_rows[]/matrix_columns[], slider takes slider_min/slider_max/slider_step. file_upload requires a plan with the file-upload-questions capability.

PUT /survey/api/v1/surveys/{'{id}'}/questions/{'{question_id}'} · DELETE /survey/api/v1/surveys/{'{id}'}/questions/{'{question_id}'}

Update or remove a question. The same field rules as creation apply to updates.

Responses

GET /survey/api/v1/surveys/{'{id}'}/responses · GET /survey/api/v1/surveys/{'{id}'}/responses/{'{response_id}'}

List completed responses with decoded answers (paginated 50 per page), or fetch one by id.

Async exports

For large surveys, queue an export instead of waiting on a synchronous download.

POST /survey/api/v1/exports

curl -X POST https://yourapp.test/survey/api/v1/exports \ -H "Authorization: Bearer sk_survey_xxxx" \ -H "Idempotency-Key: export-b6b1-2026-08-07" \ -d '{"survey_id": "b6b1...uuid", "format": "csv"}' # 202 Accepted { "data": { "id": "job-uuid", "status": "pending", "download_url": null } }

GET /survey/api/v1/jobs/{'{id}'}

Poll until status is completed (or failed), then fetch download_url.

{ "data": { "id": "job-uuid", "status": "completed", "download_url": "https://yourapp.test/store/survey-exports/job-uuid.csv" } }

Webhooks

Register a URL to receive an HTTP POST the moment a respondent completes a survey. Requires a plan with the webhooks capability.

POST /survey/api/v1/webhooks

curl -X POST https://yourapp.test/survey/api/v1/webhooks \ -H "Authorization: Bearer sk_survey_xxxx" \ -H "Idempotency-Key: webhook-create-1" \ -d '{"url": "https://yourapp.test/hooks/survey", "survey_id": "b6b1...uuid"}' # 201 Created - the secret is only ever shown in this response { "data": { "id": 12, "url": "...", "survey_id": "b6b1...", "is_active": true, "secret": "40-char-random-secret" } }

GET /survey/api/v1/webhooks · DELETE /survey/api/v1/webhooks/{'{id}'}

List or remove your webhooks.

Every delivery is signed with HMAC-SHA256 over the raw request body, sent in the X-Survey-Signature header. Verify it before trusting the payload:

$expected = hash_hmac('sha256', $rawRequestBody, $yourWebhookSecret); if (!hash_equals($expected, $_SERVER['HTTP_X_SURVEY_SIGNATURE'])) { http_response_code(401); exit; }

SSO & SCIM

SAML 2.0 and OpenID Connect SSO, plus SCIM 2.0 user/group provisioning, are configured per-organization from the panel rather than via this API - see the Security page for how each protocol is implemented. Your identity provider's own admin console is where you configure the connection; there's no separate developer setup needed beyond what's in the panel.

Errors & rate limits

Errors return a JSON body with an error key and an appropriate HTTP status (401 missing/invalid token, 403 plan doesn't include this capability, 404 not found or not yours, 422 validation failure). Each plan has its own requests-per-minute rate limit (see GET /account for yours); every response carries X-RateLimit-Limit/X-RateLimit-Remaining, and exceeding it returns 429 with a Retry-After header.