Integrations

API authentication

Use an API token or sign in, send a bearer token, and read the one error format, the rate limits and the pagination.

The AnswerStack API is the same API the admin app uses. Anything a person can do in the app, your systems can do too — with the same roles and the same limits.

This page is for a developer. Everything below has been checked against a running API.

For automation, use an API token#

For a script, a scheduled job or a partner's system, an API token is now the recommended way in. You make it in the app's Developer area (Personal settings → Developer), give it only the scopes the job needs, and send it as a bearer token. There is no password to store and no sign-in to refresh, and revoking it takes effect at once. Tokens work on the routes that list a scope; see API tokens and service identities.

Signing in as an AnswerStack user, below, still works on every route, and is what you need for a route that does not accept tokens.

Base URL and versioning#

https://api.<your-domain>/v1/...

Versioning is in the path. There is no version header. Your account manager gives you the exact host.

Health checks (/healthz, /readyz) are the only unversioned routes and are not part of the customer API.

Sign in#

AnswerStack uses Supabase Auth. You exchange an AnswerStack user's email and password for a short-lived access token, then send that token to the API.

Warning

Use a dedicated AnswerStack user for automation, with the smallest role that does the job. A token carries that user's role and community scope exactly as the app would.

js
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_PUBLISHABLE_KEY);

const { data, error } = await supabase.auth.signInWithPassword({
email: process.env.ANSWERSTACK_EMAIL,
password: process.env.ANSWERSTACK_PASSWORD,
});
if (error) throw error;

const accessToken = data.session.access_token;

SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY are the two values your account manager gives you alongside the API host. The publishable key is not a secret and is safe in a browser; the user's password is, so keep it in your secret store.

Access tokens last about an hour. The Supabase client refreshes them for you; if you are using curl, sign in again when a call returns 401.

Send the token#

bash
curl -sS "$ANSWERSTACK_API_URL/v1/me" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
json
{
  "user": { "id": "8d873670-…", "email": "[email protected]" },
  "account": {
    "id": "0b9c…",
    "name": "Sunrise Senior Living",
    "slug": "sunrise",
    "status": "active",
    "vocabulary": {
      "group": "community",
      "groupPlural": "communities",
      "meeting": "tour",
      "meetingPlural": "tours",
      "contact": "prospect",
      "contactPlural": "prospects"
    }
  },
  "role": "owner",
  "memberStatus": "active",
  "groups": [{ "id": "…", "name": "Maple Grove", "status": "active" }],
  "permissions": ["members.manage", "playbooks.publish", "…"]
}

GET /v1/me is the right first call: it tells you the account, the role, which communities the user can see, and exactly which permissions they hold.

Info

The only public endpoint is POST /v1/public/demo-requests, which takes no token. Everything else needs one.

Roles and permissions#

The API checks the caller's role on every route, and the database checks it again underneath, so a token can never read another account's data.

PermissionOwnerAdminEditorGroup managerViewer
members.manageYes
billing.manageYes
playbooks.publishYesYes
playbooks.locksYesYes
playbooks.editYesYesYes
groups.manageYesYes
groups.customizeYesYesYes
knowledge.manageYesYes
connections.manageYesYes
numbers.assignYesYes
testcall.runYesYesYes
usage.readYesYesYes
calls.readYesYesYesYesYes
analytics.readYesYesYesYesYes
account.readYesYesYesYesYes

A group manager's token is scoped to named communities. Anything outside that scope answers 404, not 403, so a token cannot be used to discover which communities exist.

One error format#

Every error — validation, permissions, rate limits, server problems — has the same shape.

json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that call.",
    "requestId": "1c98f870-1216-4d1c-ac0d-22d927b04b9c"
  }
}
FieldNotes
codeA stable machine-readable string. Branch on this, never on message.
messageOne sentence, safe to show a person.
detailsOptional, and its shape depends on the code.
requestIdAlso returned as the x-request-id header on every response. Quote it when you contact support.

Status codes#

StatusCommon codesMeaning
400validation_errorThe request body or query is wrong. details.fields lists each problem.
401unauthorized, token_staleThe token is missing, invalid, expired, or the user's access changed. Sign in again.
403forbidden, lockedThe user's role does not allow this.
403insufficient_scopeAn API token lacks a scope this route needs. The message names it.
404not_foundIt does not exist, or is outside this user's community scope.
409conflictSomething changed under you, for example a draft edited elsewhere. Reload and retry.
422publish_blocked, connection_failedThe request was understood but refused. details says why.
429rate_limitedToo many requests. See below.
5xxinternal_error, bad_gatewayOur problem. Retry with backoff and quote the requestId.

A validation error in full:

json
{
  "error": {
    "code": "validation_error",
    "message": "Check body.name: too small: expected string to have >=1 characters.",
    "details": {
      "fields": [
        { "path": "body.name", "message": "Too small: expected string to have >=1 characters" },
        { "path": "body.email", "message": "Invalid email address" }
      ]
    },
    "requestId": "0f00b153-1449-401e-8528-25187d5b9d78"
  }
}

Rate limits#

ScopeLimit
Everything, per IP address600 requests per minute
POST /v1/public/demo-requests, per IP address5 requests per 10 minutes
Each API token60 requests per minute

Every response carries the current state:

http
x-ratelimit-limit: 600
x-ratelimit-remaining: 597
x-ratelimit-reset: 48
x-request-id: 78fd7845-6d4f-42f5-a669-2e725b5e7487

A 429 adds retry-after in seconds and says how long to wait:

json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Try again in 42 seconds.",
    "requestId": "…"
  }
}

Honour retry-after. Backing off on x-ratelimit-remaining before you hit zero is kinder still.

Pagination#

Two endpoints paginate, with an opaque cursor: GET /v1/account/calls and GET /v1/account/playbooks/{playbookId}/changes.

ParameterCallsChanges
limitdefault 50, maximum 200default 50, maximum 100
cursorThe nextCursor from the previous pageSame
json
{
  "calls": [ … ],
  "nextCursor": "eyJ0IjoiMjAyNi0wOS0yMlQxNjowMDowMC4wMDBa…"
}

nextCursor is null on the last page. Treat the value as opaque: do not parse it or build one.

Every other list endpoint returns the whole set under a named key — { "groups": [...] }, { "members": [...] }, { "numbers": [...] }, { "invoices": [...] } — with no cursor.

A worked example: last week's booked calls#

bash
#!/usr/bin/env bash
set -euo pipefail

export ANSWERSTACK_API_URL="https://api.<your-domain>"

ACCESS_TOKEN=$(curl -sS \
  -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \
 -H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
  -H "content-type: application/json" \
  -d "{\"email\":\"$ANSWERSTACK_EMAIL\",\"password\":\"$ANSWERSTACK_PASSWORD\"}" \
 | jq -r .access_token)

FROM=$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)
CURSOR=""

while :; do
RESPONSE=$(curl -sS -G "$ANSWERSTACK_API_URL/v1/account/calls" \
 -H "Authorization: Bearer $ACCESS_TOKEN" \
    --data-urlencode "outcome=booked" \
    --data-urlencode "from=$FROM" \
 --data-urlencode "limit=200" \
 ${CURSOR:+--data-urlencode "cursor=$CURSOR"})

echo "$RESPONSE" | jq -r '.calls[] | [.startedAt, .groupName, .ruleFired] | @tsv'

CURSOR=$(echo "$RESPONSE" | jq -r '.nextCursor // empty')
[ -z "$CURSOR" ] && break
done

Good manners#

  • Send Authorization and content-type only. Those, plus x-request-id, are the headers the API accepts from a browser.
  • Log x-request-id for every failed call. It is the fastest way for us to find what happened.
  • Retry 429 and 5xx with backoff. Never retry 400, 403, 404 or 422: the answer will not change.
  • Poll politely. For call volume, once a minute is plenty; the cursor makes catching up cheap.

Every endpoint#

The API reference is generated from the API itself, so it is never out of date. It covers /v1/me, everything under /v1/account, and the public demo-request endpoint, with the parameters, the request and response fields and a ready-to-run curl example for each one.