A REST API for creating surveys and reading the answers back. JSON in and out, authenticated with a bearer token. It is the same API the rifts.to MCP server runs on.
Every endpoint on this page requires a current rifts.to subscription, including GET /api/v1/me. The subscription is checked on each request rather than once when the token is created.
Send the token in an Authorization header. There is no query parameter form. A token in a query string gets the same 401 as no token at all.
Create a token in your account settings. It is shown once, it acts on the whole account, and an account can hold 10 at a time.
curl https://rifts.to/api/v1/me \
-H "Authorization: Bearer rifts_pat_..."Three types. multiple_choice needs at least one entry in options. free_text and rating take none.
A rating question is always answered on a fixed 1 to 10 scale. The scale is not configurable: a property asking for a different range is stored as sent and handed back unchanged, and nothing acts on it.
index is assigned by the server, so you can leave it out. When validation fails, the 400 body carries the reason as written.
requirement decides whether an answer is needed. Leave it out and the question is required, which is what every question was before the field existed, so nothing you already send changes meaning. Send { "mode": "optional" } to let a respondent skip it.
Send { "mode": "conditional" } with a when to require an answer only when earlier answers match. op is all for AND or any for OR, across at most five conditions. The question is shown either way: the rule decides whether an answer is required, never whether the question appears. This is not branching, and a survey built expecting questions to be hidden will disappoint the room.
A condition points at an earlier multiple_choice question by its position in this array, and each entry in values must be exactly one of that question's options. A condition pointing forward, or at a question of another type, is refused — which is also what makes a loop impossible to express rather than merely detected.
[
{ "type": "rating", "text": "How did the sprint go?" },
{
"type": "multiple_choice",
"text": "Biggest blocker?",
"options": ["Scope", "Reviews", "Flaky tests"]
},
{
"type": "free_text",
"text": "What made reviews slow?",
"requirement": {
"mode": "conditional",
"when": {
"op": "all",
"conditions": [{ "questionIndex": 1, "values": ["Reviews"] }]
}
}
},
{
"type": "free_text",
"text": "Anything else?",
"requirement": { "mode": "optional" }
}
]Check a token and see what it may do.
200 response
{
"user_id": "8f1c2b6e-2f4a-4f7d-9d6b-1e2a3c4d5e6f",
"scope": "surveys:read surveys:write",
"kind": "pat"
}| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No current subscription. |
| 429 | {"error": "rate limit exceeded"} | Over 600 calls in an hour on this token. |
List the surveys this account owns.
200 response
{
"surveys": [
{
"id": "fuzzy-sleepy-tornado",
"title": "Retro sentiment",
"status": "open",
"created_at": "2026-08-12T14:03:11.482Z",
"response_count": 24,
"url": "https://rifts.to/en/s/fuzzy-sleepy-tornado"
}
]
}| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No current subscription. |
| 429 | {"error": "rate limit exceeded"} | Over 600 calls in an hour on this token. |
Create a survey.
Request
{
"title": "Retro sentiment",
"questions": [
{ "type": "rating", "text": "How did the sprint go?" }
],
"theme": "ocean"
}201 response
{
"id": "fuzzy-sleepy-tornado",
"title": "Retro sentiment",
"url": "https://rifts.to/en/s/fuzzy-sleepy-tornado",
"admin_token": "4b2f9a1c-7d3e-4c58-9f10-2a6b8c4d1e33",
"admin_url": "https://rifts.to/en/admin/4b2f9a1c-7d3e-4c58-9f10-2a6b8c4d1e33",
"created_at": "2026-08-12T14:03:11.482Z",
"theme": "ocean"
}| 400 | {"error": "title is required"} | Empty or missing title. |
| 400 | {"error": "multiple_choice questions must have at least one option"} | The question list did not validate. The message is the reason. |
| 400 | {"error": "invalid JSON"} | The body did not parse. |
| 400 | {"error": "title is too long"} | Over 200 characters. |
| 400 | {"error": "invalid slug"} | The requested custom slug is not a legal slug. |
| 400 | {"error": "invalid theme"} | theme was neither a known preset key nor a pair of 6-digit hex colors. |
| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No current subscription. |
| 409 | {"error": "survey limit reached", "code": "survey_limit_reached"} | The account already owns 500 surveys. |
| 409 | {"error": "slug taken", "code": "slug_taken"} | The requested custom slug is in use. |
| 429 | {"error": "rate limit exceeded"} | Over 600 calls in an hour, or over 30 custom slug attempts. Only the first sends Retry-After. |
Read a survey's questions and every answer.
200 response
{
"id": "fuzzy-sleepy-tornado",
"title": "Retro sentiment",
"status": "open",
"created_at": "2026-08-12T14:03:11.482Z",
"expires_at": null,
"url": "https://rifts.to/en/s/fuzzy-sleepy-tornado",
"theme": "ocean",
"questions": [
{ "type": "rating", "text": "How did the sprint go?", "index": 0 }
],
"response_count": 1,
"responses": [
{
"id": "0a9c7f21-5b64-4a3e-8c11-9d0e7f6a5b43",
"submitted_at": "2026-08-12T14:11:52.907Z",
"answers": [4]
}
]
}| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No current subscription. |
| 404 | {"error": "not found"} | No such survey, or it belongs to another account. One answer covers both cases on purpose. |
| 429 | {"error": "rate limit exceeded"} | Over 600 calls in an hour on this token. |
Change a live survey: close or reopen it, repaint it, edit its questions, archive it.
Request
{ "status": "closed", "theme": "ocean" }200 response
{ "id": "fuzzy-sleepy-tornado", "status": "closed", "theme": "ocean" }| 400 | {"error": "invalid JSON"} | The body did not parse. |
| 400 | {"error": "nothing to change: send status, theme, questions or archived"} | The body carried none of the four fields. |
| 400 | {"error": "status must be \"open\" or \"closed\""} | Any other status value. |
| 400 | {"error": "invalid theme"} | theme was neither a known preset key nor a pair of 6-digit hex colors. |
| 400 | {"error": "cannot remove a question from a survey that already has responses"} | A question edit that would orphan collected answers. The message is the reason. |
| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No current subscription. |
| 404 | {"error": "not found"} | No such survey, or it belongs to another account. |
| 409 | {"error": "this survey has expired, so reopening it would not accept responses; change its expiry from the admin dashboard first", "code": "survey_expired"} | Reopening a survey whose expiry has passed. |
| 429 | {"error": "rate limit exceeded"} | Over 600 calls in an hour on this token. |
List the saved templates this account owns.
200 response
{
"templates": [
{
"id": "0f8c1d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f",
"name": "Weekly standup",
"questions": [
{ "index": 0, "type": "rating", "text": "How's the week going?" }
],
"created_at": "2026-08-01T09:00:00.000Z",
"updated_at": "2026-08-20T09:00:00.000Z"
}
]
}| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No current subscription. |
| 429 | {"error": "rate limit exceeded"} | Over 600 calls in an hour on this token. |
Save a question set for reuse.
Request
{
"name": "Weekly standup",
"questions": [
{ "type": "rating", "text": "How's the week going?" }
]
}201 response
{
"id": "0f8c1d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f",
"name": "Weekly standup",
"questions": [
{ "index": 0, "type": "rating", "text": "How's the week going?" }
],
"created_at": "2026-08-01T09:00:00.000Z",
"updated_at": "2026-08-01T09:00:00.000Z"
}| 400 | {"error": "name is required"} | Empty or missing name. |
| 400 | {"error": "invalid JSON"} | The body did not parse. |
| 400 | {"error": "multiple_choice questions must have at least one option"} | The question list did not validate. The message is the reason. |
| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No current subscription. |
| 409 | {"error": "template limit reached", "code": "template_limit_reached"} | The account already holds 200 templates. |
| 429 | {"error": "rate limit exceeded"} | Over 600 calls in an hour on this token. |
Launch a template as a new live survey.
Request
{ "theme": "ocean" }201 response
{
"id": "fuzzy-sleepy-tornado",
"title": "Weekly standup",
"url": "https://rifts.to/en/s/fuzzy-sleepy-tornado",
"admin_token": "4b2f9a1c-7d3e-4c58-9f10-2a6b8c4d1e33",
"admin_url": "https://rifts.to/en/admin/4b2f9a1c-7d3e-4c58-9f10-2a6b8c4d1e33",
"created_at": "2026-08-27T09:00:00.000Z",
"template_id": "0f8c1d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f",
"theme": "ocean"
}| 400 | {"error": "invalid slug"} | The requested custom slug is not a legal slug. |
| 400 | {"error": "invalid theme"} | theme was neither a known preset key nor a pair of 6-digit hex colors. |
| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No current subscription. |
| 404 | {"error": "not found"} | No such template, or it belongs to another account. One answer covers both on purpose. |
| 409 | {"error": "slug taken", "code": "slug_taken"} | The requested custom slug is in use. |
| 429 | {"error": "rate limit exceeded"} | Over 600 calls in an hour, or over 30 custom slug attempts. |
Calls and custom slugs are counted in separate buckets. One bounds how much traffic a token makes, the other bounds how many names an account claims, so a 429 means different things depending on which endpoint returned it.
The call limit sends a Retry-After header. The custom slug limit does not.
| Calls per token | 600 per hour |
| Custom slug attempts per account | 30 per hour |
| Surveys per account | 500 |
| Templates per account | 200 |
| Tokens per account | 10 |
| Title length | 200 characters |
Responses carry no respondent identifier. A row holds the answers, a random id, and the time it arrived. The table has no column for a name, an email, an account, or an IP address, and a test in the repository fails the build if a migration tries to add one.
That holds for anything built on top of it, because it is a property of the schema rather than a setting.
Every response carries an X-Request-Id header. Include it in a support email and the exact request can be looked up.
The whole surface is described in an OpenAPI 3.1 document, so you can generate a client rather than write one.
/openapi.jsonIf the caller is Claude or another MCP client, the rifts.to MCP server already wraps these endpoints as tools. It is published as @rifts_to/mcp and hosted at mcp.rifts.to. It uses the same tokens and needs the same subscription.
Setting up the MCP server