API Reference
Base URL: https://surveyz.co.za/api · Version alias: /api/v1
Overview
The Surveyz REST API lets you integrate survey creation, response collection, and data export into your own products and workflows. All requests and responses use JSON. The API is designed for B2B integrations: data is scoped to the authenticated account and all writes are audited.
Developer portal: Manage your API key, view usage, and configure webhooks at surveyz.co.za/developer. You must be on an Enterprise or Business plan to access the API.
Quick-start
Three steps to your first API call:
- Log in and go to Developer Portal → generate an API key.
- Store the key securely — it is shown only once.
- Pass it in every request as the
X-Api-Keyheader.
# List all your surveys
curl https://surveyz.co.za/api/surveys \
-H "X-Api-Key: sr_your_key_here"
# Get a single survey
curl https://surveyz.co.za/api/surveys/<survey-id> \
-H "X-Api-Key: sr_your_key_here"
# Export responses as CSV
curl https://surveyz.co.za/api/response/survey/<survey-id>/export/csv \
-H "X-Api-Key: sr_your_key_here" \
--output responses.csv
Authentication
Two authentication methods are supported. Use whichever fits your integration.
API key (recommended for server-to-server)
Generate a key in the Developer Portal. Pass it on every request:
X-Api-Key: sr_abc123...
Keys are prefixed with sr_ and stored as SHA-256 hashes — the plaintext is shown only at generation time. Lost keys must be rotated.
JWT Bearer token (recommended for user-context calls)
Obtain a token via POST /api/auth/login or OAuth, then include it as:
Authorization: Bearer <token>
Tokens expire after a configurable period. Use POST /api/auth/refresh with your refresh token to renew.
Scopes
API keys can be restricted to read-only scope. Read-only keys cannot call any endpoint that uses POST, PUT, PATCH, or DELETE — the API returns 403 immediately. Configure this in the Developer Portal → Key Settings.
IP allow-list
Optionally restrict which IP addresses may use a key. Requests from unlisted IPs receive 403 Request IP not in API key allow-list. Configure comma-separated IPs in Key Settings.
Versioning
The current API has no breaking version. A URL alias /api/v1/ is provided for forward-compatibility — requests to /api/v1/* are transparently rewritten to /api/* server-side. You may use either prefix.
# Both of these are equivalent:
GET https://surveyz.co.za/api/surveys
GET https://surveyz.co.za/api/v1/surveys
When breaking changes are introduced, a new version prefix (/api/v2/) will be added and the old one kept alive with a sunset period of at least 6 months.
Rate limits
Limits are enforced per caller using a sliding 1-minute window:
| Caller type | Limit |
|---|---|
| Authenticated (API key or JWT) | 300 requests / minute |
| Anonymous | 60 requests / minute |
When exceeded, the API returns 429 Too Many Requests with these headers:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
RateLimit-Limit: 300
RateLimit-Remaining: 0
Content-Type: application/problem+json
{
"type": "https://surveyz.co.za/errors/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Please back off and retry.",
"retryAfterSeconds": 60
}
Back off exponentially and retry after the number of seconds in Retry-After.
Errors
All error responses use RFC 7807 Problem Details with Content-Type: application/problem+json:
{
"type": "https://surveyz.co.za/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "No valid credentials were provided.",
"instance": "/api/surveys",
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
Use traceId when contacting support — it links your request to server logs.
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created (survey, webhook) |
| 204 | No Content (survey deleted) |
| 400 | Bad Request — invalid input, check detail |
| 401 | Unauthorized — missing or invalid credentials |
| 403 | Forbidden — read-only key used for a write, or IP not in allow-list |
| 404 | Not Found — resource doesn't exist or belongs to another user |
| 422 | Unprocessable Entity — business rule violation (e.g. survey access restrictions) |
| 429 | Too Many Requests — rate limit exceeded |
| 500 | Internal Server Error — unexpected server failure |
Surveys
List surveys
Returns all surveys owned by the authenticated user.
Response 200
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Customer Satisfaction Q1",
"slug": "csat-q1-2026",
"status": "published",
"visibility": "public",
"questionCount": 8,
"responseCount": 142,
"createdAt": "2026-01-15T08:00:00Z",
"updatedAt": "2026-03-01T12:34:00Z"
}
]
Get a survey
Returns full survey detail including questions.
| Path param | Type | Description |
|---|---|---|
| idrequired | UUID | Survey ID |
Create a survey
Requires an active subscription. Fails with 402 if the account is on a free or expired plan.
POST /api/surveys
Content-Type: application/json
X-Api-Key: sr_...
{
"title": "Employee Pulse Check",
"description": "Monthly team wellbeing survey",
"visibility": "private",
"questions": [
{
"text": "How satisfied are you with your current workload?",
"type": "rating",
"required": true,
"options": []
},
{
"text": "Any comments for the team?",
"type": "text",
"required": false,
"options": []
}
]
}
| Field | Type | Description |
|---|---|---|
| titlerequired | string | Survey title |
| description | string | Optional description shown to respondents |
| visibility | public | private | unlisted | Default: public |
| questions | array | Initial questions — can also be set via PUT /surveys/{id}/questions |
Question types: text, multiple_choice, single_choice, rating, boolean, date, file, matrix, ranking, slider, nps
Response 201 — full survey object with assigned id and slug.
Update a survey
Partial update — include only the fields you want to change.
{
"title": "Employee Pulse Check — April",
"visibility": "private"
}
Replace all questions on a survey. Send the full ordered array — the existing questions are overwritten.
Delete a survey
Permanently deletes the survey and all its responses. This is irreversible.
Response 204 No Content
Publish a survey
Makes the survey live and accepting responses. Fires the survey.published webhook event.
Close a survey
Stops accepting new responses. Fires the survey.closed webhook event.
{
"reason": "Target response count reached"
}
Responses
List responses
Returns all submitted responses for a survey owned by the authenticated user.
[
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"surveyId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"submittedAt": "2026-04-01T14:22:00Z",
"answers": [
{ "questionId": "...", "value": "4" },
{ "questionId": "...", "value": "Great team culture." }
]
}
]
Analytics
Returns aggregated analytics: response counts, completion rates, per-question breakdowns, and NPS score if applicable.
Export responses
text/csv
application/json
application/xml
# Download CSV export
curl https://surveyz.co.za/api/response/survey/<id>/export/csv \
-H "X-Api-Key: sr_..." \
--output responses.csv
Developer
API key info
Returns metadata about the current API key — prefix, timestamps, scope, and IP allow-list. The key hash is never returned.
{
"apiKeyPrefix": "sr_abc123",
"hasKey": true,
"apiKeyCreatedAt": "2026-03-01T10:00:00Z",
"apiKeyLastUsedAt":"2026-05-06T18:33:00Z",
"apiKeyScopes": "full",
"apiKeyAllowedIps": null
}
Key settings
Update the scope or IP allow-list for the current key. Omit a field to leave it unchanged.
{
"scopes": "read_only",
"allowedIps": "203.0.113.5, 198.51.100.0"
}
| Field | Type | Description |
|---|---|---|
| scopes | "full" | "read_only" | Key permission scope |
| allowedIps | string | null | Comma-separated IPv4 addresses. null or empty string removes the restriction. |
Audit log
Returns a paginated log of all API calls made by the authenticated user. Useful for debugging integrations and compliance reporting.
| Query param | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| pageSize | integer | Results per page, 1–200 (default: 50) |
| method | string | Filter by HTTP method: GET, POST, etc. |
| statusCode | integer | Filter by exact HTTP status code |
| from | ISO 8601 | Start of time range |
| to | ISO 8601 | End of time range |
{
"total": 892,
"page": 1,
"pageSize": 50,
"logs": [
{
"id": "a1b2c3d4-...",
"method": "GET",
"path": "/api/surveys",
"query": null,
"statusCode": 200,
"durationMs": 34,
"ipAddress": "203.0.113.5",
"authMethod": "apikey",
"createdAt": "2026-05-07T09:12:00Z"
}
]
}
Webhooks
Webhooks deliver real-time event notifications to a URL you control. All payloads are signed with HMAC-SHA256 — always verify the signature before processing.
List webhooks
[
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"url": "https://your-server.com/hooks/surveyz",
"events": "survey.published,response.submitted",
"secretPrefix": "whsec_ab12cd",
"isActive": true,
"createdAt": "2026-03-10T08:00:00Z",
"updatedAt": "2026-04-01T12:00:00Z"
}
]
Create a webhook
The signing secret is returned only once in the creation response. Store it immediately in a secrets vault — it cannot be retrieved again. If lost, delete and recreate the webhook.
POST /api/webhooks
Content-Type: application/json
X-Api-Key: sr_...
{
"url": "https://your-server.com/hooks/surveyz",
"events": "survey.published,response.submitted"
}
| Field | Type | Description |
|---|---|---|
| urlrequired | string | HTTPS endpoint that will receive POST requests |
| eventsrequired | string | Comma-separated event names (see Event payloads) |
Response 200
{
"id": "f47ac10b-...",
"url": "https://your-server.com/hooks/surveyz",
"events": "survey.published,response.submitted",
"secretPrefix": "whsec_ab12cd",
"isActive": true,
"createdAt": "2026-05-07T10:00:00Z",
"secret": "whsec_ab12cd34ef56..." // ← save this NOW, shown once
}
Update a webhook
Update URL, events, or active status. Omit fields to leave them unchanged.
{
"url": "https://new-server.com/hooks/surveyz",
"events": "response.submitted",
"isActive": false
}
Delete a webhook
Response 200 { "message": "Webhook deleted." }
Verifying signatures
Every webhook delivery includes an X-Surveyz-Signature header. Verify it to ensure the request came from Surveyz and was not tampered with.
X-Surveyz-Signature: sha256=3d25f98a5a...
X-Surveyz-Timestamp: 1715078400
To verify:
- Concatenate:
{timestamp}.{raw request body} - Compute HMAC-SHA256 of that string using your signing secret
- Compare the hex digest to the value after
sha256=in the header (constant-time comparison) - Reject if the timestamp is more than 5 minutes old (replay protection)
# Node.js example
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const timestamp = req.headers['x-surveyz-timestamp'];
const sig = req.headers['x-surveyz-signature'];
const payload = `${timestamp}.${req.rawBody}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const valid = crypto.timingSafeEqual(
Buffer.from(sig), Buffer.from(expected)
);
if (!valid) throw new Error('Invalid signature');
if (Date.now() / 1000 - parseInt(timestamp) > 300)
throw new Error('Stale timestamp — possible replay');
}
Event payloads
survey.published
Fired when a survey is published and made live.
{
"event": "survey.published",
"timestamp": "2026-05-07T10:00:00Z",
"data": {
"surveyId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Customer Satisfaction Q1",
"slug": "csat-q1-2026",
"userId": "a1b2c3d4-..."
}
}
survey.closed
Fired when a survey is closed or expires.
{
"event": "survey.closed",
"timestamp": "2026-06-01T00:00:00Z",
"data": {
"surveyId": "3fa85f64-...",
"title": "Customer Satisfaction Q1",
"slug": "csat-q1-2026",
"reason": "Target response count reached"
}
}
response.submitted
Fired each time a respondent submits a response.
{
"event": "response.submitted",
"timestamp": "2026-05-07T11:34:00Z",
"data": {
"responseId": "7c9e6679-...",
"surveyId": "3fa85f64-...",
"surveySlug": "csat-q1-2026",
"submittedAt":"2026-05-07T11:34:00Z"
}
}
Response answer data is not included in the webhook payload for privacy reasons. Fetch full answers via GET /api/response/survey/{surveyId} after receiving the event.
Retry policy
If your endpoint does not return 2xx within 10 seconds, Surveyz will retry with exponential backoff: after 1 min, 5 min, 30 min, 2 hr, and 8 hr. After 5 failures the subscription is automatically paused. Re-enable it via PATCH /api/webhooks/{id} with "isActive": true.
Need help? Contact us at surveyz.co.za/contact or include your traceId when reporting an issue.