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 an active 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.
index is assigned by the server, so you can leave it out. When validation fails, the 400 body carries the reason as written.
[
{ "type": "rating", "text": "How did the sprint go?" },
{
"type": "multiple_choice",
"text": "Biggest blocker?",
"options": ["Scope", "Reviews", "Flaky tests"]
},
{ "type": "free_text", "text": "Anything else?" }
]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 active 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 active 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?" }
]
}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"
}| 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. |
| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No active 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",
"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 active 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. |
Close a survey, or reopen a closed one.
Request
{ "status": "closed" }200 response
{ "id": "fuzzy-sleepy-tornado", "status": "closed" }| 400 | {"error": "invalid JSON"} | The body did not parse. |
| 400 | {"error": "status must be \"open\" or \"closed\""} | Any other status value, or a body with no status. |
| 401 | {"error": "authentication required"} | Missing, malformed, revoked or expired token. |
| 403 | {"error": "upgrade required", "code": "entitlement_required"} | No active 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. |
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 |
| 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.