Back to Surveyz

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:

  1. Log in and go to Developer Portal → generate an API key.
  2. Store the key securely — it is shown only once.
  3. Pass it in every request as the X-Api-Key header.
# 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 typeLimit
Authenticated (API key or JWT)300 requests / minute
Anonymous60 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.

StatusMeaning
200Success
201Created (survey, webhook)
204No Content (survey deleted)
400Bad Request — invalid input, check detail
401Unauthorized — missing or invalid credentials
403Forbidden — read-only key used for a write, or IP not in allow-list
404Not Found — resource doesn't exist or belongs to another user
422Unprocessable Entity — business rule violation (e.g. survey access restrictions)
429Too Many Requests — rate limit exceeded
500Internal Server Error — unexpected server failure

Surveys

List surveys

GET /api/surveys Auth required

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

GET /api/surveys/{id} Auth required

Returns full survey detail including questions.

Path paramTypeDescription
idrequiredUUIDSurvey ID

Create a survey

POST /api/surveys Auth required · full scope

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":  []
    }
  ]
}
FieldTypeDescription
titlerequiredstringSurvey title
descriptionstringOptional description shown to respondents
visibilitypublic | private | unlistedDefault: public
questionsarrayInitial 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

PUT /api/surveys/{id} Auth required · full scope

Partial update — include only the fields you want to change.

{
  "title":      "Employee Pulse Check — April",
  "visibility": "private"
}
PUT /api/surveys/{id}/questions Auth required · full scope

Replace all questions on a survey. Send the full ordered array — the existing questions are overwritten.

Delete a survey

DELETE /api/surveys/{id} Auth required · full scope

Permanently deletes the survey and all its responses. This is irreversible.

Response 204 No Content

Publish a survey

POST /api/surveys/{id}/publish Auth required · full scope

Makes the survey live and accepting responses. Fires the survey.published webhook event.

Close a survey

POST /api/surveys/{id}/close Auth required · full scope

Stops accepting new responses. Fires the survey.closed webhook event.

{
  "reason": "Target response count reached"
}

Responses

List responses

GET /api/response/survey/{surveyId} Auth required

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

GET /api/response/survey/{surveyId}/analytics Auth required

Returns aggregated analytics: response counts, completion rates, per-question breakdowns, and NPS score if applicable.

Export responses

GET /api/response/survey/{surveyId}/export/csv Auth required Returns text/csv
GET /api/response/survey/{surveyId}/export/json Auth required Returns application/json
GET /api/response/survey/{surveyId}/export/xml Auth required Returns 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

GET /api/developer/api-key Auth required

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

PATCH /api/developer/api-key Auth required · full scope

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"
}
FieldTypeDescription
scopes"full" | "read_only"Key permission scope
allowedIpsstring | nullComma-separated IPv4 addresses. null or empty string removes the restriction.

Audit log

GET /api/developer/audit-logs Auth required

Returns a paginated log of all API calls made by the authenticated user. Useful for debugging integrations and compliance reporting.

Query paramTypeDescription
pageintegerPage number (default: 1)
pageSizeintegerResults per page, 1–200 (default: 50)
methodstringFilter by HTTP method: GET, POST, etc.
statusCodeintegerFilter by exact HTTP status code
fromISO 8601Start of time range
toISO 8601End 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

GET /api/webhooks Auth required
[
  {
    "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

POST /api/webhooks Auth required · full scope

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"
}
FieldTypeDescription
urlrequiredstringHTTPS endpoint that will receive POST requests
eventsrequiredstringComma-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

PATCH /api/webhooks/{id} Auth required · full scope

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

DELETE /api/webhooks/{id} Auth required · full scope

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:

  1. Concatenate: {timestamp}.{raw request body}
  2. Compute HMAC-SHA256 of that string using your signing secret
  3. Compare the hex digest to the value after sha256= in the header (constant-time comparison)
  4. 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.