rifts.to

rifts.to API

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.

The API needs a paid plan

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.

  • Without a current subscription you cannot create a token. Minting is blocked in your account settings and on the OAuth consent screen.
  • If a subscription lapses, or a trial ends without a payment, the next call answers 403 with the code entitlement_required.
  • A lapse does not revoke your tokens. Resubscribing restores access to the tokens you already hold, so there is nothing to re-create.
See the plan

Authentication

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_..."

The question shape

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" }
  }
]

GET /api/v1/me

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"
}
  • The cheapest way to tell a live token from a dead one. It touches no survey data.
  • kind is pat for a token you created yourself, or oauth for one an application holds on your behalf.
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.

GET /api/v1/surveys

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"
    }
  ]
}
  • include_admin=1 adds admin_token and admin_url to every row. It is off by default so a routine listing does not hand back one credential per survey.
  • include_closed=0 restricts the list to open surveys. Archived surveys are never returned.
  • status here is the stored column. Expiry is folded in by GET /api/v1/surveys/{id}, one call away.
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.

POST /api/v1/surveys

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"
}
  • This is the only response that always returns admin_token. Store it: the admin link is how you watch results live, and the results endpoint will not give it back.
  • A survey created here does not expire. Anonymous surveys made in the browser do.
  • Titles are capped at 200 characters, and an account can own 500 surveys.
  • An optional slug field claims a custom link. It spends a separate 30-per-hour budget, because that limit is about name grabs rather than call volume.
  • An optional theme field colors the respondent page: one of default, sunset, ocean, forest, rose or slate, or your own {"primary", "background"} pair of hex colors. The response reports the palette the survey actually got.
  • Leave theme out and the survey takes the palette your account last saved in the browser, the same one a one-click template launch uses. Send "default" to override that and get the house colors.
  • Setting a theme here never changes the palette your browser builder opens on. That is yours to set by hand.
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.

GET /api/v1/surveys/{id}

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]
    }
  ]
}
  • Never returns admin_token, on any query. Read it from the create response or from the list endpoint with include_admin=1.
  • theme is the palette the respondent page is currently wearing, reported as "default" when it has none. It is the same spelling PATCH accepts, so you can read it, change one thing, and send it straight back.
  • status folds in expiry here, so it has a third value the list endpoint never returns: expired, for a survey still marked open whose expiry has passed. One closed by hand reads closed whatever its expiry says.
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.

PATCH /api/v1/surveys/{id}

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" }
  • Send any of status, theme, questions and archived. At least one, in any combination. The response carries the id, the status, and each field you changed.
  • Everything you send is validated before anything is written, so a request that is half wrong changes nothing at all.
  • questions replaces the whole list. Once a survey has answers you may add questions, but not remove, reorder or retype one, and not rename or drop a multiple-choice option: answers are keyed by position and matched to options by their exact text, so any of those would orphan real data. Refused with 400.
  • archived only hides a survey from your own listing. Its public link, its status and its admin dashboard are untouched, and archiving something already archived is not an error.
  • Retitling is the one thing missing, and it is missing everywhere — no part of rifts.to renames a live survey.
  • Reopening a survey whose expiry has already passed is refused rather than done quietly. Flipping the column back would not make it accept answers again, so the API says so instead of reporting a success that changes nothing. Clear the expiry from the admin dashboard first.
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.

GET /api/v1/templates

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"
    }
  ]
}
  • A template is a reusable set of questions. It has no link, no admin token and no answers of its own until you launch it.
  • Questions come back in full rather than as a count, so choosing between two similarly named templates does not cost a call each.
  • Newest edit first. Another account's templates are never listed.
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.

POST /api/v1/templates

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"
}
  • Questions are validated exactly as they are on create, so a template can never hold a shape a launch would then refuse.
  • Names are capped at 200 characters, and an account can hold 200 templates.
  • Editing and deleting a template stay in your account pages. An edit that quietly rewrites something a weekly poll depends on is not a thing to do from a chat window.
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.

POST /api/v1/templates/{id}/launch

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"
}
  • The body is optional. Send nothing and you get the template's questions, your remembered palette, and a random three-word link.
  • The new survey copies the template's questions at that moment. Editing the template afterwards never touches a survey already launched from it.
  • slug and theme work exactly as they do on create, including the separate 30-per-hour budget for slugs.
  • Every launch is its own survey with its own answers, which is what keeps last week's numbers out of this week's.
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.

Limits

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 token600 per hour
Custom slug attempts per account30 per hour
Surveys per account500
Templates per account200
Tokens per account10
Title length200 characters

What a response contains

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.

When something goes wrong

Every response carries an X-Request-Id header. Include it in a support email and the exact request can be looked up.

OpenAPI

The whole surface is described in an OpenAPI 3.1 document, so you can generate a client rather than write one.

/openapi.json

Using this from an AI client

If 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
Back to home
rifts.to API: create surveys and read answers programmatically | rifts.to