Importing answers
Bring older answers in by CSV or through the API, checked like answers from the widget.
If you already have answers from an older survey or another tool, you can bring them into a form so Results counts them with the new ones. Import from a CSV file in OneStar, or send rows from your server through the API. Importing is part of Max.
Every imported row is checked against the form's live questions exactly like an answer from the widget. Rows go into the form's live version, so publish the form first.
What an import does#
- Each row becomes one finished answer, on the channel
import. - It counts in Results like any other answer, and toward your monthly answers. When the month reaches the most your plan stores, the import stops and says how many rows were left out.
- It is never quotable: nobody who wrote it agreed to be quoted on your form's public page.
- It has no respondent, so it counts as anonymous, and it carries no contact.
- It sends no answer events to webhooks and posts nothing to Slack, so a large import never floods them. The month's allowance warnings still go out, as they would for answers from the widget.
- It is imported once. Sending the same rows again adds nothing; they come back as skipped.
Checks on every row#
A row is refused, with a reason, when:
- an answer is for a question that is not on the live version, or is not in that question's shape;
- an answer is for a follow up question that the row's other answers would not have shown;
- the row has no answers;
- its date is in the future, before 2000, or older than your plan keeps answers.
Required questions are not enforced, since an older survey may not have asked them. Answers expire after the time your plan keeps them, counted from their own date, so a row older than that is refused rather than deleted the same night.
Import a CSV in OneStar#
On the form's Results, choose Import answers.
- Pick a CSV file with a header row, up to 10 MB and 20,000 rows.
- For each rating, NPS or text question of the live form, pick the column that fills it, or Skip. Choice and ranking questions cannot be filled from a CSV; use the API for them.
- Under When it was answered, pick the column with each row's date, or Today. Dates must be ISO 8601, such as
2026-03-14or2026-03-14T09:30:00Z. A date in any other format is read as today. - Choose Import. OneStar shows how many answers came in, how many were already there, and the first rows it could not read.
Columns whose names look like personal data, such as email, name, user id, phone or address, are dropped when the file is uploaded and are never offered. Rating and NPS cells are read as numbers. Sending the same file again adds nothing; an edited file is a new file, so its rows import again.
Import through the API#
Send rows to POST /v1/owner/forms/{id}/answers with a secret key that has the import scope (see REST API for keys and the form id). Up to 500 rows a call, and a body under 1 MB.
curl https://api.onestar.sh/v1/owner/forms/9c2e41b07d5a4f31a8e6b2c9d0f71e54/answers \
-H "Authorization: Bearer os_sk_..." \
-H "Content-Type: application/json" \
-d @rows.json{
"rows": [
{
"answers": { "b3f9a12c": 2, "b7d01e55": "The boss fight lags on older phones.", "b52c8e0a": "deck" },
"created_at": "2026-04-02T17:20:00Z",
"external_id": "survey-2026-8812",
"platform": "ios",
"app_version": "4.1.3"
},
{
"answers": { "b3f9a12c": 7 },
"created_at": "2026-04-03T08:05:12Z",
"external_id": "survey-2026-8813"
}
]
}| Field | Required | What it is |
|---|---|---|
answers | Yes | Answers keyed by question id, in each question's shape (below). |
created_at | No | When it was answered, ISO 8601. A date with no time zone is read as UTC. Without it, the row is dated the moment it is imported. |
external_id | No | Your own id for the row, up to 100 characters. A row with an external_id OneStar already took is skipped. Without one, a row is skipped when its answers and date match a row already taken, so a row with neither can import twice. |
platform | No | Up to 40 characters, filterable in Results like the widget's context. |
app_version | No | Up to 40 characters, the same. |
Any other field refuses the call. Answers take the same shapes as answers from your own UI:
| Question | Answer |
|---|---|
| Rating | A whole number: stars 1 to 5, thumbs 1 (down) or 5 (up), faces 1 to 3 or 1 to 5, a scale 1 to its points, a slider 0 to 100. |
| NPS | A whole number from 0 to 10. |
| Choice | An option id, a list of option ids, or { "choices": ["other"], "other_text": "..." } for Other. |
| Ranking | Every option id, in order. |
| Text | A string. |
Question ids and option ids come from GET /v1/owner/forms/{id}.
The response counts what happened. Row numbers start at 1, and up to 20 problems are listed:
{
"imported": 1,
"skipped": 0,
"refused": 0,
"invalid": 1,
"errors": [
{ "row": 2, "message": "Some answers need another look.", "fields": { "b3f9a12c": "Pick one of the answers." } }
]
}imported: rows stored.skipped: rows already imported before.refused: rows left out because the month reached the most your plan stores. Send them again next month.invalid: rows that failed a check.errorsgives the row, a sentence, and a message per question id infields.
A call is refused as a whole with 409 and the code not_published when the form has never been published. Imports have their own limit on how often they run; at it you get 429 with a Retry-After header. Split a large history into calls of up to 500 rows and send them one after another.