What your key may do, calls this minute, and how we hold it

This page keeps your key only while this tab is open. It is never saved, never added to a web address and never shown in the curl line you copy.

Untick anything your key is not allowed to do
0 of 120 calls this minute

Developer API v1

Connect your own systems to your Survizo community

Your own systems can pull surveys, answers and members from your community, so dashboards stay current with no exports. They can invite people by email, or from your own WhatsApp Business number to people who have opted in, and hear the moment a survey is done. Each key reaches one community and does only what you allow. Ask on a fit call whether your plan includes the API.

Everything on this page comes from one reference file, which your developers can download from /api/v1/openapi.json.

Loading the API document…

Create a key and send it with each call

Create a key in Survizo under Admin → Developer API → Create key, and tick only what your connection needs to do. You see the full key once. We store only a one-way fingerprint of it, so nobody can read it back, including us. If you lose it, cancel it and create a new one.

Send it in the X-Api-Key header. Authorization: Bearer <key> is accepted too, and X-Api-Key wins if both are present. That way a leftover Authorization header added by a proxy along the way can never switch the key your call uses.

The key decides the community. You never name the community in a call, because the key already does. A key made for community A reads community A alone. A record in any other community answers 404 (not found), never 403 (refused), so a key never learns whether someone else's survey exists.

Base address

https://survizo.com/api/v1, the address named in the reference file (its host and basePath). The console on this page calls that same address directly, so it works with no setup on your side.

Check the body as well as the status. If you mistype the address, survizo.com answers with the website and a success code (200), so a 200 alone does not prove you reached the API. GET /health names the API in its body: {"status":"ok","api":"survizo-developer-api","version":"v1"}. Any other reply means you missed the API, so fix the address before trying a call with your key.

What each error means and what to do

Every refusal has the same three fields, whatever went wrong:

{ "status": "error", "code": "unauthorized", "message": "Invalid API key." }
StatuscodeWhen
401unauthorizedNo key, an unknown key, or a revoked key.
403forbiddenYour key is not allowed to make this call, or this community's plan does not include the API.
404not_foundNo such route, or no such record in this community.
413invalid_requestThe request body is larger than 1 MB. Split a large invitation batch into smaller calls.
422invalid_requestThe request has an unreadable body, a since value not in ISO 8601, a webhook address that is not public https, or more than 500 invites.
429rate_limitedOver the key's or the community's rate limit. Retry-After says how long to wait.

The message is a sentence you can act on, and nothing more. It never echoes what you asked for. Someone without a valid key learns only that the call failed, never what data is there.

How many calls you can make each minute

Each key can make 120 requests a minute, and each community 240 a minute across all its keys, counted in fixed one-minute blocks. The community limit is double the key limit, so an old key and its replacement both run at full speed while you switch. Every account gets the same limits, which stop one faulty script from using up a community's allowance. The meter in the key bar above counts what this console has sent in the last sixty seconds, so you can see where you are.

Go over the limit and you get the same error, plus a header that says when to try again:

HTTP/1.1 429 Too Many Requests
Retry-After: 37

{ "status": "error", "code": "rate_limited", "message": "Rate limit exceeded." }

Honour Retry-After. Retrying at once only uses up the next minute's allowance as well.

Long lists page by page, and how times are written

Every list route takes limit (1–200, default 50) and cursor. The answer carries nextCursor, which you pass back for the next page. When nextCursor is null, you have everything. Never edit or build a cursor yourself. Pass back exactly the one you were given, and use it within the day.

Every time is ISO 8601 UTC. Use since on /responses and /members to fetch only what is new, by submission time and join time. A since that is not a valid ISO time gets a 422. Ignoring it quietly would send you every record and look like success.

We filter by since after reading each page, so a page can hold fewer rows than limit. Keep paging until nextCursor is null, even after a short page.

How API invitations protect the people you invite

POST /invites is the one call that reaches real people, so it has firm safeguards. It invites by email, or from your own WhatsApp Business number to people who have opted in. The API emails each invitation unless told not to. This console sets sendEmail to false and asks before it sends anything.

  • 500 invites per call. Send 501 and you get a 422, and nothing is written, because the whole batch is checked before any row is created.
  • 24 hours per address. Invite the same address again within 24 hours and we update their details but send no second email, so nobody is pestered.
  • 5,000 invitation emails a day, per community. Past that, addresses are still stored, emailed comes back 0, and the answer carries an emailNote explaining why. Send the rest tomorrow, or add them with "sendEmail": false and send their invitations from the members list in Survizo.
  • Each address is kept once. Send it again, or twice in one batch, and that single record is updated.
  • Anyone who has unsubscribed is skipped, and nobody receives a second invitation email within 24 hours.

A stored address answers accepted whether the row was created or merged. Whether someone was already on your list counts as member information, so only a key holding members:read as well as invites:write sees separate added and updated totals.

Hear the moment something happens

Give Survizo an address with POST /webhooks, and we send it a message each time something happens, such as a finished survey or a new member. The signing secret comes back once, in that response, and GET /webhooks never returns it. Unsubscribe with DELETE at exactly the address the Location header gave you.

Each event needs the permission of the call that reads that data, as well as webhooks:manage. response.completed needs responses:read. member.joined, member.left and the three whatsapp.* events carry an email address, so they need members:read. segment.membership.changed needs segments:read. Only poll.closed needs nothing more.

A webhook lives as long as the key that registered it. When that key is cancelled or expires, Survizo stops delivering to its webhooks. They stay on your list until you delete them. Rotating a key keeps its webhooks. Choose Rotate in Survizo under Admin → Developer API, or call POST /keys/self/rotate with the key itself. Survizo creates the new key, moves every webhook onto it, then cancels the old one. The new key is shown once, and a key rotating itself keeps its expiry.

The delivery

HeaderValue
x-survizo-eventThe event name.
x-survizo-deliveryThe same on every attempt for one event, so you can spot repeats.
x-survizo-signaturet=<unix seconds>,v1=<hex hmac>

If your server answers with a 5xx or does not answer at all, we try again after 0.5 s and once more after 2 s. A 4xx is your receiver's answer, and we take it as final. The URL must be public https. Addresses on http, localhost, *.internal or a private range are refused with a 422 when you register them. We never follow redirects.

Check each delivery really came from us

The signature is HMAC-SHA256 of <timestamp>.<raw body> with your endpoint's signing secret. Verify against the raw bytes, before you parse.

The signature covers the timestamp and the body only. Decide what to do from the event field inside the verified body, never from the x-survizo-event header. Anything that can reach your endpoint can set a header. Only the body is signed.

Node

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const ts = Number(parts.t);
  if (!Number.isFinite(ts)) return false;
  if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false; // replay
  const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1 ?? '', 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Python

import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    try:
        ts = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - ts) > tolerance_sec:          # replay
        return False
    signed = f"{ts}.".encode() + raw_body              # raw bytes, not the parsed body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Always run the check. A webhook URL is public by design, and the signature is the only proof that a delivery came from us.

Every call, with a console to try it

Each call below lists the permission it needs, what it accepts and every answer it can give. Copy its curl line, or run it on your own community in the console. GET /health and GET /openapi.json work without a key.

Before this runs