# SurVoyce — full documentation

# Getting started

Create your account, build your first form, and share it.

## 1. Create your account

Sign up to get a workspace and 10 free response credits — no credit card required.

## 2. Describe your first form

From your dashboard, choose “New form” and describe what you want to collect. SurVoyce drafts the questions, types, and order for you to review.

## 3. Review and edit

Adjust prompts, change question types, mark questions required or optional, and set form-level options like a submission limit.

## 4. Preview and publish

Preview the voice experience, then publish to get a shareable link, an embed snippet, and a phone form code.

## 5. Share and collect

Send the link or number to respondents. Completed submissions appear in your dashboard, where you can view detail and export.

---

# Building a form

Question types, validation, skip logic, and form settings.

## Question types

Open-ended, single-choice, multiple-choice, rating/scale (including NPS and CSAT), and structured data (date, number, email, phone).

## Validation & retries

Structured questions are validated during the conversation. If an answer doesn’t fit, the agent rephrases and retries before moving on or marking it skipped.

## Skip logic & follow-ups

Route respondents past questions that don’t apply, and add an optional follow-up probe to dig deeper on open-ended answers.

## Form settings

Set a welcome and completion message, a submission limit, which channels are enabled, and whether to collect a name or email up front.

---

# Web & phone channels

How respondents complete forms in the browser and over the phone.

## Web

Respondents open the form link and choose how to answer. By voice, they grant microphone access and start talking: the question they are on is shown on screen — with the fixed options numbered, so they can just say the number — alongside a live transcript of the conversation. Or they choose “Fill out with keyboard instead” and complete a plain form with the same questions on one page. If the microphone is denied or unavailable, the form appears automatically, so nobody hits a dead end. Typed responses have no transcript, and results mark them as Typed rather than Voice.

## Phone

Phone-enabled forms are reachable through one shared SurVoyce number. The caller hears a brief disclosure, consents by pressing a key, enters the form code, and completes the form by voice.

## Consent & disclosure

Every session — web or phone — begins with a clear notice that the respondent is interacting with AI, and consent is recorded before the conversation starts.

## Embedding on your site

Every published form can be embedded in your own pages with a plain iframe pointing at the form’s /embed URL (the editor’s Share card generates this snippet, plus a downloadable QR code, for you). The allow="microphone" attribute is required for voice — without it, respondents automatically get the keyboard form with the same questions. The iframe is a fixed height and scrolls internally; the consent screen and AI disclosure appear inside the embed exactly as on the full page.

```html
<iframe
  src="https://survoyce.com/embed/YOUR_FORM_ID"
  width="100%" height="620" style="border:0; max-width: 480px;"
  allow="microphone"
  title="Voice form"
></iframe>
```

---

# Credits & billing

The free tier, how credits are spent, and unlocking submissions.

## The free tier

Every new workspace starts with 10 response credits. No credit card is required to begin.

## How credits are spent

One credit is spent per completed submission. Sessions that are abandoned partway through never cost a credit.

## Locked submissions

If your balance is zero when a respondent finishes, the submission is still captured and stored — it stays locked until you add credits.

## Buying & unlocking

Buy credits in packs from the billing page. When credits are added, any locked submissions unlock automatically, in order, until the new balance is used up.

---

# Agents & API

Self-serve API keys, MCP, A2A, and REST — operate SurVoyce end-to-end from an AI agent.

## Get an API key

Registration is a single unauthenticated request. The apiKey is shown once — store it. New agent workspaces start unverified, with trial credits and reduced daily caps. Send `organization` to name who your forms are run for: respondents hear it in the AI disclosure that opens every voice session. It is optional, and you can set it later with set_organization.

```bash
curl -X POST https://api.survoyce.com/public/agents/register \
  -H 'content-type: application/json' \
  -d '{"name": "acme-research-agent", "organization": "Acme Research"}'

# → 201
# {"workspaceId": "…", "keyId": "…", "apiKey": "sv_…",
#  "organization": "Acme Research",
#  "verified": false, "trialCredits": 3, "message": "…"}
```

## Authenticate and rotate

Send the key as a bearer token on every request: Authorization: Bearer <apiKey>. The header is the only accepted form — a key passed in the query string is refused with 401, because URLs end up in access logs, proxies and browser history in a way headers do not. Rotate with POST /v1/keys/rotate — the old key keeps working for a short grace window so in-flight work doesn’t break.

## Connect over MCP

The MCP server speaks streamable HTTP at https://api.survoyce.com/api/mcp/survoyce/mcp and SSE at https://api.survoyce.com/api/mcp/survoyce/sse. Any MCP client works — for Claude Desktop, Claude Code, or Cursor, the config is:

```json
{
  "mcpServers": {
    "survoyce": {
      "url": "https://api.survoyce.com/api/mcp/survoyce/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}
```

## Available tools

Read: get_workspace, list_forms, get_form, list_submissions, get_answers (keyed JSON answers plus transcripts where retained). Write: create_form, add_question, update_question, delete_question, publish_form, unpublish_form, set_organization. Billing: list_credit_packs, purchase_credits (Stripe). Webhooks: list_webhook_events, create_webhook, list_webhooks, get_webhook, update_webhook, delete_webhook, rotate_webhook_secret, test_webhook, list_webhook_deliveries, replay_webhook_delivery — see the webhooks doc.

## A2A

Prefer agent-to-agent? The intake agent's card is at https://api.survoyce.com/api/.well-known/intake/agent-card.json and JSON-RPC execution at POST https://api.survoyce.com/api/a2a/intake (bearer auth). It can list your forms and hand out respondent session links; humans always consent on the session page before any conversation starts.

## REST

The intake agent is also reachable over plain REST at https://api.survoyce.com/api/agents/intake, and the OpenAPI spec is served at https://api.survoyce.com/api/openapi.json.

## Limits & verification

Unverified workspaces have trial credits, reduced daily caps on form creation and credit purchases, and no access to outbound calling. Requests are rate-limited per key. Verification mechanisms are still evolving — contact us if you need higher limits.

## The respondent experience

Humans answer your form by natural voice conversation, on the web or by phone — or, on the web, with the keyboard instead. Every session opens with a mandatory AI disclosure — it cannot be disabled. Sessions are designed to finish within about eight minutes, and abandoned sessions never cost a credit. Answers come back in the same structured shape either way; only voice sessions carry a transcript. Published form URLs from publish_form / create_session_link can also be embedded in any web page via the iframe snippet described in the channels doc (swap /f/ for /embed/ and include allow="microphone").

---

# Webhooks

Get signed, retried HTTP callbacks when things happen — submissions, forms, credits, calls.

## What you get

Register an HTTPS endpoint and SurVoyce POSTs a JSON envelope to it as events happen. Every request is signed, failures are retried with exponential backoff, and every attempt is logged where you can inspect and replay it. Manage endpoints in Settings → Webhooks, over the REST API, or with the MCP tools — all three share one implementation, so they behave identically.

## The envelope

Every delivery has the same top-level shape. `id` is the event id — stable across every endpoint subscribed to it and across replays, so it is what you should deduplicate on. The per-attempt delivery id travels in the SurVoyce-Delivery-Id header instead.

```http
POST /your-endpoint
content-type: application/json
SurVoyce-Event: submission.completed
SurVoyce-Event-Id: 8f1c…            # dedupe on this
SurVoyce-Delivery-Id: 3ab9…         # this attempt
SurVoyce-Signature: t=1755264000,v1=9f3a…

{
  "id": "8f1c…",
  "type": "submission.completed",
  "created_at": "2026-08-15T12:00:00.000Z",
  "workspace_id": "…",
  "data": {
    "submission_id": "…",
    "session_id": "…",
    "form": { "id": "…", "title": "Customer NPS" },
    "status": "accessible",
    "channel": "phone",
    "duration_seconds": 184,
    "submitted_at": "2026-08-15T12:00:00.000Z"
  }
}
```

## Verifying the signature

The signature is an HMAC-SHA256 over `${timestamp}.${rawBody}`, keyed with your signing secret — the same construction Stripe uses, so existing tooling and examples apply. Verify against the RAW body, before any JSON parsing, and reject timestamps outside a tolerance window: that is what makes a captured request impossible to replay later.

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

function verify(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=').map((s) => s.trim())),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;

  const expected = createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(parts.v1, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && timingSafeEqual(a, b);
}
```

## Subscribing to events

Subscriptions accept exact names ("submission.completed"), prefix wildcards ("form.*"), or "*" for everything. Available events cover forms, questions, sessions, answers, submissions, credits, outbound calls, campaigns, workspace settings and API keys. Fetch the full catalogue with descriptions from GET /v1/webhook-events (no auth needed) or the list_webhook_events MCP tool. A few events — answer.recorded, session.started, and the no-answer/voicemail call outcomes — fire many times per session and are flagged high-volume; do not point a chat channel at them.

```bash
curl https://api.survoyce.com/v1/webhook-events
```

## Answers and locked submissions

By default a submission event carries ids, the form title, the channel and timings — not the answers themselves. Set include_answers to add the keyed answers and respondent contact details. Two rules always hold regardless of that flag: a LOCKED submission never includes answers, because it is data the workspace has not yet spent a credit to unlock; and the respondent origin (IP address for web, caller ID for phone) is never included in any webhook payload. Both are available through get_answers and the results export, which are authenticated and stay inside your account.

## Retries and failures

A 2xx is success. 5xx, 429, 408, timeouts and connection errors are retried after roughly 10 seconds, 1 minute, 5 minutes, 30 minutes and 2 hours (jittered) — six attempts in total. Other 4xx responses stop immediately, since no retry will fix them, and redirects count as failures because SurVoyce never follows them. A 410 Gone disables the endpoint straight away — that is the clean way to decommission an integration. After a sustained run of failures an endpoint is disabled automatically; you can subscribe a second endpoint to webhook.endpoint_disabled to hear about it, and re-enable from the dashboard once fixed.

## Reshaping the payload

If your receiver expects a particular shape — a Slack or Discord incoming webhook, say — give the endpoint a JSON template. Strings in it may contain {{path}} references into the envelope. A string that is exactly one placeholder keeps the referenced value's type; mixed with other text it interpolates. This is substitution only: there are no conditionals, loops or expressions.

```javascript
{ "text": "New response to {{data.form.title}} ({{data.status}})" }

// becomes
{ "text": "New response to Customer NPS (accessible)" }
```

## Managing endpoints over REST

All routes take the same bearer API key as the rest of the API. Creating an endpoint returns the signing secret exactly once; it is never retrievable afterwards, only replaceable via rotate-secret.

```bash
POST   https://api.survoyce.com/v1/webhooks                        # create — returns the secret ONCE
GET    https://api.survoyce.com/v1/webhooks                        # list
GET    https://api.survoyce.com/v1/webhooks/{id}
PATCH  https://api.survoyce.com/v1/webhooks/{id}
DELETE https://api.survoyce.com/v1/webhooks/{id}
POST   https://api.survoyce.com/v1/webhooks/{id}/test              # queue a test delivery
POST   https://api.survoyce.com/v1/webhooks/{id}/rotate-secret
GET    https://api.survoyce.com/v1/webhooks/{id}/deliveries        # the delivery log
POST   https://api.survoyce.com/v1/webhook-deliveries/{id}/replay
GET    https://api.survoyce.com/v1/webhook-events                  # catalogue (public)

curl -X POST https://api.survoyce.com/v1/webhooks \
  -H 'authorization: Bearer YOUR_API_KEY' \
  -H 'content-type: application/json' \
  -d '{"url": "https://example.com/hook",
       "events": ["submission.completed", "form.*"],
       "includeAnswers": true}'
```

## Requirements and limits

Endpoints must be HTTPS and resolve to a public address — private, loopback, link-local and cloud-metadata addresses are refused, and are re-checked before every delivery attempt, not only at registration. Each request has a 10-second budget. Response bodies are stored truncated in the log for debugging. Up to 20 endpoints per workspace. Delivery logs are pruned after 30 days.

> A newly created or re-enabled endpoint begins receiving events within about a minute. If a test delivery does not arrive, check the delivery log on the endpoint page — it records the HTTP status, the response body, and any connection error.
