Sign in
Feedback docs

REST API

Secret keys and the /v1/owner/ routes: forms, results with filters, answers and import.

The REST API lets your own server read your forms, their results and their answers, and import older answers. It is part of Max. Every route lives under https://api.onestar.sh/v1/owner/ and opens only the workspace that made the key.

Secret keys#

Make a key in OneStar under Settings, Feedback API:

  • Give it a name, like Production server, and choose whether it Can import. Every key can read; a key that can import also has the import scope.
  • The key is shown once. Copy it into your server's configuration right away. It starts with os_sk_.
  • The list shows each key's first characters, its scopes and when it was last used.
  • Revoke stops a key at once. A workspace keeps up to 10 live keys.

A secret key reads every answer in your workspace. Keep it on your server: never put it in a web page, in browser code or in an app. The widget and the form pages never need one; they use only a form's public id.

Send the key as a bearer token on every request:

curl
curl https://api.onestar.sh/v1/owner/forms \
  -H "Authorization: Bearer os_sk_..."

Form ids#

These routes take a form's id: 32 lower case hex characters, such as 9c2e41b07d5a4f31a8e6b2c9d0f71e54. It is not the public id that starts with f_, which the widget uses. GET /v1/owner/forms lists both, and webhook events name forms by id in collector_id.

Routes#

RouteScopeReturns
GET /v1/owner/formsreadYour forms.
GET /v1/owner/forms/{id}readOne form with its live questions, their ids and option ids.
GET /v1/owner/forms/{id}/resultsreadEvery question counted, with filters, one split and the load funnel.
GET /v1/owner/forms/{id}/answersreadAnswers newest first, a page at a time, with filters.
POST /v1/owner/forms/{id}/answersimportImports older answers. See Importing answers.

List forms#

curl
curl https://api.onestar.sh/v1/owner/forms -H "Authorization: Bearer os_sk_..."
200 OK
{
  "forms": [
    {
      "id": "9c2e41b07d5a4f31a8e6b2c9d0f71e54",
      "public_id": "f_hemsz66yupbd76d2",
      "title": "How was your run?",
      "kind": "survey",
      "state": "live",
      "product": { "id": "4f1d0c9a7b2e4d3c8a6b5e1f0d9c2a74", "name": "Driftwood" },
      "live_version": { "n": 3, "published_at": "2026-10-28T14:02:11.204512+00:00" },
      "answers_30d": 412
    }
  ]
}

kind is reaction, nps, poll, survey or open. state is draft, live, paused or closed. live_version is null until the first publish. answers_30d counts answers of the last 30 days, test answers left out. Archived forms are not listed.

Get a form#

Returns the live version's questions in order: the question ids and option ids you use to send answers from your own UI and to import. It never returns the draft, the form's settings or its theme.

curl
curl https://api.onestar.sh/v1/owner/forms/9c2e41b07d5a4f31a8e6b2c9d0f71e54 -H "Authorization: Bearer os_sk_..."
200 OK
{
  "id": "9c2e41b07d5a4f31a8e6b2c9d0f71e54",
  "public_id": "f_hemsz66yupbd76d2",
  "title": "How was your run?",
  "kind": "survey",
  "state": "live",
  "product": { "id": "4f1d0c9a7b2e4d3c8a6b5e1f0d9c2a74", "name": "Driftwood" },
  "live_version": { "n": 3, "published_at": "2026-10-28T14:02:11.204512+00:00" },
  "blocks": [
    { "id": "b3f9a12c", "question": "How was your run?", "required": true, "type": "rating", "style": "stars", "points": 5 },
    { "id": "b7d01e55", "question": "What should we fix first?", "required": false, "show_if": { "block": "b3f9a12c", "op": "lt", "value": 4.0 }, "type": "text", "style": "long", "limit": 2000 },
    { "id": "b52c8e0a", "question": "Where do you play?", "required": false, "type": "choice", "options": [{ "id": "pc", "label": "PC" }, { "id": "deck", "label": "Steam Deck" }], "multiple": false, "other": false, "shuffle": false }
  ]
}

Each block has id, type, question and required, plus help and show_if when set, and the fields of its type: style and points (and any end labels) for a rating, options with multiple, max_choices, other and shuffle for a choice, options for a ranking, style, limit and placeholder for a text question. blocks is empty before the first publish.

Filters#

Results and answers take the same filters as Results in the app. Leave a filter out to include everything.

ParameterValueKeeps
versionA version numberOnly that version. By default every version with the same questions as the live one is counted together.
placementA placement id (32 hex characters)Answers from one placement. Ids come from funnel.rows and from segments split by placement.
channelembed, hosted or importAnswers from the widget on your site, from the form's own page, or imported.
levelanonymous, contact, signed_in or onestarWho answered: no contact, a typed contact, a signed in user of your product, or a OneStar account.
platform, app_version, plan, cohortA value of up to 40 charactersAnswers whose page stated that value with onestar.set. See Context and placements.
trait with trait_valueA trait key and its valueSigned in users with that trait. Both are needed; trait alone is ignored.
days1 to 3650Answers from the last that many days.

Values match exactly. Test answers are never included, and neither are answers removed by the person who sent them.

Results#

curl
curl "https://api.onestar.sh/v1/owner/forms/9c2e41b07d5a4f31a8e6b2c9d0f71e54/results?days=30&dimension=platform" \
  -H "Authorization: Bearer os_sk_..."

dimension picks the one split returned in segments: platform (the default), app_version, placement, level, channel, plan or cohort. The response below is trimmed to the rating question.

200 OK
{
  "form": { "id": "9c2e41b07d5a4f31a8e6b2c9d0f71e54", "title": "How was your run?", "kind": "survey", "state": "live" },
  "versions": [
    { "n": 2, "published_at": "2026-10-02T09:15:40.118220+00:00", "counted": true, "same_questions": true },
    { "n": 3, "published_at": "2026-10-28T14:02:11.204512+00:00", "counted": true, "same_questions": true }
  ],
  "shown_version": 3,
  "n": 120,
  "capped": false,
  "completed": { "k": 118, "share": 0.9833, "ci": [0.9413, 0.9954], "small": false },
  "wrote": { "k": 29, "share": 0.2417, "ci": [0.1739, 0.3255], "small": false },
  "identity": [
    { "level": "anonymous", "label": "Anonymous", "k": 89, "share": 0.7417, "ci": [0.6567, 0.8116], "small": false },
    { "level": "contact", "label": "Left a contact", "k": 0, "share": 0.0, "ci": [0.0, 0.031], "small": false },
    { "level": "signed_in", "label": "Signed in user", "k": 31, "share": 0.2583, "ci": [0.1884, 0.3433], "small": false },
    { "level": "onestar", "label": "OneStar account", "k": 0, "share": 0.0, "ci": [0.0, 0.031], "small": false }
  ],
  "blocks": [
    {
      "id": "b3f9a12c",
      "type": "rating",
      "question": "How was your run?",
      "count": {
        "type": "rating",
        "style": "stars",
        "n": 120,
        "distribution": [
          { "value": 1, "k": 14, "share": 0.1167, "ci": [0.0708, 0.1863], "small": false },
          { "value": 2, "k": 9, "share": 0.075, "ci": [0.04, 0.1364], "small": false },
          { "value": 3, "k": 14, "share": 0.1167, "ci": [0.0708, 0.1863], "small": false },
          { "value": 4, "k": 31, "share": 0.2583, "ci": [0.1884, 0.3433], "small": false },
          { "value": 5, "k": 52, "share": 0.4333, "ci": [0.3481, 0.5227], "small": false }
        ],
        "mean": { "mean": 3.817, "ci": [3.571, 4.062], "n": 120, "small": false },
        "top": { "from": 4, "k": 83, "share": 0.6917, "ci": [0.6042, 0.7673], "small": false },
        "bottom": { "to": 2, "k": 23, "share": 0.1917, "ci": [0.1313, 0.2712], "small": false }
      },
      "completion": { "k": 120, "share": 1.0, "ci": [0.969, 1.0], "small": false, "shown": 120 },
      "trend": [
        { "week": "2026-10-19", "value": 0.7213, "ci": [0.5983, 0.8181], "n": 61, "small": false, "unit": "share" },
        { "week": "2026-10-26", "value": 0.661, "ci": [0.5337, 0.7686], "n": 59, "small": false, "unit": "share" }
      ]
    }
  ],
  "segments": {
    "dimension": "platform",
    "trait": null,
    "block": "b3f9a12c",
    "rows": [
      { "key": "ios", "n": 62, "headline": { "value": 0.6935, "ci": [0.5703, 0.7942], "n": 62, "small": false, "unit": "share" } },
      { "key": "android", "n": 58, "headline": { "value": 0.6897, "ci": [0.562, 0.7938], "n": 58, "small": false, "unit": "share" } }
    ],
    "more": 0
  },
  "funnel": {
    "since": "2026-10-02",
    "loads": 1840,
    "started": 120,
    "finished": 118,
    "rows": [
      { "placement_id": "e81b5c3d20a94f7f9c6d1a2b3c4d5e6f", "channel": "embed", "mode": "inline", "origin": "https://driftwood.games", "name": "checkout", "loads": 1840, "started": 120, "finished": 118 }
    ],
    "more": 0
  },
  "filters": { "days": 30 }
}

What the fields mean:

  • Shares come as { k, share, ci, small }: the count, the share from 0 to 1, its 95% range, and small when too few answers stand behind it to lean on.
  • n is the number of answers counted. capped is true when a form has more answers than one result counts; the newest are counted.
  • versions lists every version; counted marks the ones in these figures, and shown_version is the version whose wording is shown.
  • completed is the share of answers that were finished, and wrote the share with at least one written answer.
  • identity splits answers by who answered.
  • Each block has its count (its shape depends on the question type), its completion among the people it was shown to (shown), and for a rating or NPS question a weekly trend of its headline figure: the share at the happy end of a rating, or the NPS score.
  • segments splits answers by dimension, largest first, with the headline of the first rating or NPS question (block). key is null for answers without that value, and more counts the groups not listed.
  • funnel counts how often the form loaded, and how many answers were started and finished, per placement. Only days applies to it, never the other filters. It is null until a load has been counted. Headless and imported answers have no loads and stay out of it.
  • filters echoes the filters that were applied.

Answers#

Answers come newest first, 50 a page. Pass the response's next as before to get the next page; next is null on the last page. Add wrote=true to list only answers with words in them. Every filter above applies.

curl
curl "https://api.onestar.sh/v1/owner/forms/9c2e41b07d5a4f31a8e6b2c9d0f71e54/answers?wrote=true&level=signed_in" \
  -H "Authorization: Bearer os_sk_..."
200 OK
{
  "items": [
    {
      "id": "5c0e7d2a9b8f4e61a3d2c4b7e9f01a6d",
      "created_at": "2026-10-30T18:44:02.551930+00:00",
      "version": 3,
      "channel": "embed",
      "mode": "inline",
      "origin": "https://driftwood.games",
      "level": "signed_in",
      "has_contact": true,
      "display_name": null,
      "completed": true,
      "platform": "ios",
      "app_version": "4.2.0",
      "locale": "en-US",
      "path": "/runs/summary",
      "quotable": false,
      "answers": [
        { "block": "b3f9a12c", "type": "rating", "question": "How was your run?", "value": 2.0, "choices": null, "text": null, "removed": false, "label": "2", "featured": false, "hidden": null },
        { "block": "b7d01e55", "type": "text", "question": "What should we fix first?", "value": null, "choices": null, "text": "The boss fight lags on older phones.", "removed": false, "label": "The boss fight lags on older phones.", "featured": false, "hidden": null }
      ]
    }
  ],
  "next": "0a7f3e9c2b1d4c58b6e2f1a0d9c8b7e6"
}
  • label is the answer as a person reads it: the score, the option labels, or the text.
  • A choice answer lists option ids in choices; a ranking lists them in order.
  • A written answer longer than 280 characters is cut to 280 and ends with .... Read it in full in OneStar.
  • removed is true when a written answer was removed; its text is then null.
  • has_contact says whether the person left a contact. The contact itself never appears in the API; open the answer in OneStar to see it.
  • featured and hidden reflect what you chose to show on the form's public page.

Errors#

Errors are JSON with a plain sentence in detail, and most carry a code:

403 Forbidden
{ "detail": "This key does not have the import scope.", "code": "insufficient_scope" }
StatuscodeWhen
401auth_requiredNo key, a malformed key, or a key that was revoked or never existed.
402upgrade_requiredThe workspace is not on Max. The body also names the feature.
403insufficient_scopeThe key does not have the scope the route needs.
404noneNo such form in this workspace. A form of another workspace answers the same way.
409not_publishedAn import into a form that was never published.
413noneThe request body is larger than 1 MB.
422noneA parameter or body the API cannot read. detail is then a list of problems, each naming where it is.
429noneToo many requests. Wait the number of seconds in the Retry-After header, then try again.

A 5xx answer means something failed on OneStar's side; try again later with a growing wait.