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

  • Without an active subscription you cannot create a token. Minting is blocked in your account settings and on the OAuth consent screen.
  • If a subscription lapses, 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 plans

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.

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

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

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"
}
  • 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.
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.

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",
  "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.
  • 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 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.

PATCH /api/v1/surveys/{id}

Close a survey, or reopen a closed one.

Request

{ "status": "closed" }

200 response

{ "id": "fuzzy-sleepy-tornado", "status": "closed" }
  • status is the only field you can change. Retitling and editing questions stay in the browser.
  • 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": "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.

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

Back to home
rifts.to API: create surveys and read answers programmatically | rifts.to