> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omneo.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Submitting questionnaires and routing answers

> Submitting a questionnaire fill, addressing each answer to its slot, and understanding how Omneo routes answers to profiles, ratings, and other destinations.

To record a customer's feedback, you submit a fill of a questionnaire: the profile who answered, what the answers are about, and the answers themselves. Omneo saves the submission, then routes each answer to the destination declared on its question. This guide covers submitting a fill and what happens next. For the model behind it, see [Questions and questionnaires](/concepts/ratings/questionnaires).

All endpoints are bearer authenticated and live under `https://api.[tenant].getomneo.com/api/v3`.

## Submitting a fill

Post the fill to the questionnaire's submissions endpoint:

```shell theme={null}
curl -X POST "https://api.[tenant].getomneo.com/api/v3/questionnaires/{questionnaire}/submissions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02",
    "answerable": { "type": "profile", "id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02" },
    "answers": [
      { "question_handle": "nps-score", "value": 9 },
      { "question_handle": "visit-comment", "value": "Staff were helpful." }
    ]
  }'
```

A successful submission returns `201`.

| Field          | Type   | Notes                                                                                                                                                          |
| -------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `profile_id`\* | uuid   | The Profile who filled the form.                                                                                                                               |
| `answerable`\* | object | What the answers are about. See below.                                                                                                                         |
| `answers`\*    | array  | One entry per answer. See [Addressing each answer](#addressing-each-answer).                                                                                   |
| `context`      | object | Optional resolution hints used when routing needs to find a target, for example `transaction_id`, `transaction_item_id`, `appointment_id`, or `connection_id`. |

### The answerable

`answerable` is the subject of the fill: `{ "type": ..., "id": ... }`. The `type` names what the answers describe and `id` identifies the specific record. Common types are `profile`, `transaction`, `product_variant`, and `appointment`, and many other resource types are accepted.

```json theme={null}
{ "type": "product_variant", "id": 441200 }
```

The answerable frames the whole submission. Individual answers can still route to different destinations based on each question's link configuration.

## Addressing each answer

Each entry in `answers` carries a `value` and identifies which question slot it belongs to. You can identify the slot three ways, in order of specificity:

| Identifier                  | Use when                                                                    |
| --------------------------- | --------------------------------------------------------------------------- |
| `questionnaire_question_id` | You have the exact slot id from the questionnaire. Most precise.            |
| `question_id`               | You have the question id but not the slot id.                               |
| `question_handle`           | You are working from stable handles, for example a hand-authored front end. |

A full submission addressed by handle:

```json theme={null}
{
  "profile_id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02",
  "answerable": { "type": "profile", "id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02" },
  "context": {
    "transaction_id": 987654
  },
  "answers": [
    { "question_handle": "csat-score", "value": 5 },
    { "question_handle": "open-feedback", "value": "Quick and friendly." }
  ]
}
```

Here the fill is about the Profile, and `context.transaction_id` tells any transaction-routed answer which transaction to write to. Supply the context key a question needs to resolve its destination: `transaction_id`, `transaction_item_id`, `appointment_id`, or `connection_id`.

## What happens after submission

Routing runs after the submission is saved. Each answer is routed independently against the link configuration on its question, and one answer failing does not affect the others. An answer is saved as `pending`, then routing moves it to one of three terminal states:

| State     | Meaning                                                                                                    |
| --------- | ---------------------------------------------------------------------------------------------------------- |
| `pending` | Saved but not yet routed. Re-read the submission or answer to see the terminal state once routing has run. |
| `mapped`  | The destination was written.                                                                               |
| `skipped` | Nothing to write, for example a capture-only question (`link_type` of `none`).                             |
| `failed`  | The write could not be applied, for example the target could not be resolved.                              |

Because routing is per answer, a fill can partly succeed: a score routes to a Rating while a free-text comment is captured only. For where each `link_type` writes and the profile write policies, see [Routing answers](/concepts/ratings/questionnaires#routing-answers).

## The submission response

The `201` response is the observable result of the fill. Its `answers[]` is not an echo of what you sent: each entry is the routing outcome for one answer, carrying the resolved destination and a `mapping_status`. This example fills the post-visit form with a score that routes to a Rating and a comment that is captured only:

```json theme={null}
{
  "data": {
    "submission_id": 5012,
    "questionnaire_id": 7,
    "submitted_count": 2,
    "answerable": { "type": "profile", "id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02" },
    "context": { "transaction_id": 987654 },
    "submitted_at": "2026-07-14T23:15:09Z",
    "answers": [
      {
        "id": 88001,
        "questionnaire_question_id": 51,
        "question_id": 42,
        "question_handle": "nps-score",
        "question_version_id": 108,
        "profile_id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02",
        "answerable_type": "profile",
        "answerable_id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02",
        "mapping_key": "nps",
        "link_type": "rating",
        "link_target": "nps",
        "link_write_policy": null,
        "question_label": "How likely are you to recommend us to a friend or colleague?",
        "question_type": "slider",
        "question_required": true,
        "answer_type": "integer",
        "value": 9,
        "mappable_type": "rating",
        "mappable_id": 33120,
        "mapped_field": "nps",
        "mapping_status": "mapped",
        "mapping_error": null,
        "mapped_at": "2026-07-14T23:15:09Z",
        "meta": {},
        "created_at": "2026-07-14T23:15:09Z",
        "updated_at": "2026-07-14T23:15:09Z"
      },
      {
        "id": 88002,
        "questionnaire_question_id": 52,
        "question_id": 55,
        "question_handle": "visit-comment",
        "question_version_id": 141,
        "profile_id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02",
        "answerable_type": "profile",
        "answerable_id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02",
        "mapping_key": "comment",
        "link_type": "none",
        "link_target": null,
        "link_write_policy": null,
        "question_label": "Anything else you would like to share?",
        "question_type": "textarea",
        "question_required": false,
        "answer_type": "string",
        "value": "Staff were helpful.",
        "mappable_type": null,
        "mappable_id": null,
        "mapped_field": null,
        "mapping_status": "skipped",
        "mapping_error": null,
        "mapped_at": null,
        "meta": {},
        "created_at": "2026-07-14T23:15:09Z",
        "updated_at": "2026-07-14T23:15:09Z"
      }
    ]
  }
}
```

### Top-level fields

| Field              | Notes                                         |
| ------------------ | --------------------------------------------- |
| `submission_id`    | Id of the saved submission.                   |
| `questionnaire_id` | The questionnaire that was filled.            |
| `submitted_count`  | Number of answers accepted.                   |
| `answerable`       | The subject of the fill, as `{ type, id }`.   |
| `context`          | The resolution hints supplied on the request. |
| `submitted_at`     | When the fill was recorded.                   |
| `answers`          | One routing outcome per answer. See below.    |

### Answer outcome fields

Each entry in `answers[]` is a routing outcome:

| Field                       | Notes                                                                               |
| --------------------------- | ----------------------------------------------------------------------------------- |
| `id`                        | The stored answer id.                                                               |
| `questionnaire_question_id` | The slot the answer was given for.                                                  |
| `question_id`               | The question id.                                                                    |
| `question_handle`           | The question handle.                                                                |
| `question_version_id`       | The question version the answer is pinned to.                                       |
| `profile_id`                | The Profile who answered.                                                           |
| `answerable_type`           | Type of the submission subject.                                                     |
| `answerable_id`             | Id of the submission subject.                                                       |
| `mapping_key`               | The slot's key within the form.                                                     |
| `link_type`                 | Routing destination kind, copied from the question.                                 |
| `link_target`               | Destination within the `link_type`.                                                 |
| `link_write_policy`         | Overwrite policy applied.                                                           |
| `question_label`            | The label shown at submission time.                                                 |
| `question_type`             | The input type.                                                                     |
| `question_required`         | Whether the slot was required.                                                      |
| `answer_type`               | Data type of the stored value.                                                      |
| `value`                     | The submitted value.                                                                |
| `mappable_type`             | Type of the record written. Populated when `mapping_status` is `mapped`.            |
| `mappable_id`               | Id of the record written. Populated when `mapping_status` is `mapped`.              |
| `mapped_field`              | Field written on the destination. Populated when `mapping_status` is `mapped`.      |
| `mapping_status`            | `mapped`, `skipped`, or `failed`. See below.                                        |
| `mapping_error`             | Reason the write could not be applied. Populated when `mapping_status` is `failed`. |
| `mapped_at`                 | When the write happened. Populated when `mapping_status` is `mapped`.               |
| `meta`                      | Free-form metadata.                                                                 |
| `created_at`                | When the answer was stored.                                                         |
| `updated_at`                | When the answer last changed.                                                       |

The values of `mapping_status`:

| `mapping_status` | Meaning                                                                        | Populated fields                                             |
| ---------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------ |
| `pending`        | The answer is saved but has not been routed yet.                               | None yet.                                                    |
| `mapped`         | The destination was written.                                                   | `mappable_type`, `mappable_id`, `mapped_field`, `mapped_at`. |
| `skipped`        | Nothing to write, for example a capture-only question (`link_type` of `none`). | None of the mapping fields.                                  |
| `failed`         | The write could not be applied.                                                | `mapping_error`.                                             |

<Note>
  A saved submission returns `201` even when some answers end in `failed`. Routing outcomes are reported per answer inside `answers[]`, not as an HTTP error. Read each `mapping_status` to confirm the writes you expected actually landed.
</Note>

## Routing to a Rating

An answer whose question has `link_type` of `rating` always inserts a new Rating rather than updating an earlier one. Re-submitting a score for the same subject creates a second Rating, so the store accumulates every score over time.

When you need a single current value, such as a product's latest NPS from one customer, read the Ratings for that subject and aggregate on the most recent. Do not assume one Rating per subject per customer.

## End-to-end example

This walks a single post-visit form from authoring to inspecting its routing outcomes. Authoring detail lives in [Building questionnaires](/dev-guides/feedback/building-questionnaires); the commands here are condensed to show the whole flow.

<Steps>
  <Step title="Create the questions">
    Author a score that routes to a Rating and a comment that is captured only.

    ```shell theme={null}
    curl -X POST "https://api.[tenant].getomneo.com/api/v3/questions" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{
        "handle": "nps-score",
        "label": "How likely are you to recommend us to a friend or colleague?",
        "type": "slider",
        "validation": { "min": 0, "max": 10 },
        "link_type": "rating",
        "link_target": "nps"
      }'

    curl -X POST "https://api.[tenant].getomneo.com/api/v3/questions" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{
        "handle": "visit-comment",
        "label": "Anything else you would like to share?",
        "type": "textarea",
        "link_type": "none"
      }'
    ```
  </Step>

  <Step title="Build the questionnaire with a page">
    Create the form, add a page, then place both slots on that page.

    ```shell theme={null}
    curl -X POST "https://api.[tenant].getomneo.com/api/v3/questionnaires" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Post-visit feedback",
        "handle": "post-visit-feedback",
        "purpose": "post_visit"
      }'

    curl -X POST "https://api.[tenant].getomneo.com/api/v3/questionnaires/{questionnaire}/pages" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{ "title": "Your visit", "section_header": "Experience", "sort_order": 1 }'

    curl -X POST "https://api.[tenant].getomneo.com/api/v3/questionnaires/{questionnaire}/questions" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{ "question_handle": "nps-score", "mapping_key": "nps", "questionnaire_page_id": 21, "sort_order": 1, "is_required": true }'

    curl -X POST "https://api.[tenant].getomneo.com/api/v3/questionnaires/{questionnaire}/questions" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{ "question_handle": "visit-comment", "mapping_key": "comment", "questionnaire_page_id": 21, "sort_order": 2, "is_required": false }'
    ```
  </Step>

  <Step title="Submit a fill">
    Post the two answers for the Profile who filled the form.

    ```shell theme={null}
    curl -X POST "https://api.[tenant].getomneo.com/api/v3/questionnaires/{questionnaire}/submissions" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{
        "profile_id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02",
        "answerable": { "type": "profile", "id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02" },
        "answers": [
          { "question_handle": "nps-score", "value": 9 },
          { "question_handle": "visit-comment", "value": "Staff were helpful." }
        ]
      }'
    ```
  </Step>

  <Step title="Inspect the routing outcomes">
    Read `answers[]` in the `201` response. The `nps-score` answer comes back with `mapping_status` of `mapped` and a `mappable_type` of `rating`, so a new Rating was written. The `visit-comment` answer comes back `skipped` because its `link_type` is `none`, so it was captured without a destination. This is the response shown in [The submission response](#the-submission-response).
  </Step>
</Steps>

## Edge cases and gotchas

* **The answerable frames the fill, answers still route independently.** `answerable` names the overall subject, but each answer routes to the destination on its own question, which can differ from the answerable. A profile fill can still write a Rating and a Transaction answer at once.
* **Version pinning is permanent.** Each stored answer records `question_version_id`. Editing the question later creates a new version and never rewrites answers already captured, so historical responses stay exact.
* **Reads nest the rendering fields.** When you read a question to render a form, its `label`, `type`, `options`, and `validation` come back under `current_version`, not at the top level. See [The question response shape](/dev-guides/feedback/building-questionnaires#the-question-response-shape).
* **Ratings always insert.** A `rating` answer creates a new Rating every time. Re-submitting a score for the same subject accumulates Ratings rather than updating one, so aggregate on the most recent when you need a single current value.
* **A `failed` answer does not fail the request.** The submission is still saved and returns `201`. The failure is reported on that answer, with `mapping_status` of `failed` and a reason in `mapping_error`:

```json theme={null}
{
  "questionnaire_question_id": 71,
  "question_handle": "purchase-rating",
  "link_type": "transaction",
  "value": 4,
  "mapping_status": "failed",
  "mapping_error": "The target could not be resolved.",
  "mappable_type": null,
  "mappable_id": null,
  "mapped_field": null
}
```

A common cause is a missing resolution hint: supply the `context` key the question needs (`transaction_id`, `transaction_item_id`, `appointment_id`, or `connection_id`) so the destination can be found.

## The booking case

Appointment booking questions are a questionnaire attached to the appointment definition, but their answers are not submitted through the submissions endpoint. Instead, send them inside the appointment create payload, on the `answers` array, addressed by `questionnaire_question_id`:

```shell theme={null}
curl -X POST "https://api.[tenant].getomneo.com/api/v3/appointments" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "appointment_definition_id": 1,
    "profile_id": "a1b460b2-953f-4587-b4eb-fb0f29b55e02",
    "location_id": 13,
    "scheduled_start_at": "2026-05-11 10:00:00",
    "scheduled_end_at": "2026-05-11 10:30:00",
    "timezone": "Australia/Melbourne",
    "answers": [
      { "questionnaire_question_id": 1, "value": "consultation" }
    ]
  }'
```

See [Working with appointments](/dev-guides/visits/appointments) for the full booking flow. Post-visit feedback and standalone surveys use the submissions endpoint on this page instead, so booking answers stay with the booking and later feedback is captured as its own submission.

## Errors

| Status | When                                                                                |
| ------ | ----------------------------------------------------------------------------------- |
| `401`  | The bearer token is missing or invalid.                                             |
| `403`  | The token is valid but lacks the scope or permission to submit.                     |
| `404`  | The questionnaire does not exist, or a referenced slot is not part of it.           |
| `422`  | Validation failed. A submission requires `profile_id`, `answerable`, and `answers`. |

A `failed` routing outcome is not an HTTP error. The submission still returns `201`; inspect each answer's `mapping_status` to confirm the writes landed.

## Related

* [Questions and questionnaires concept](/concepts/ratings/questionnaires)
* [Building questionnaires](/dev-guides/feedback/building-questionnaires)
* [Working with appointments](/dev-guides/visits/appointments)
* [Submit questionnaire API](/api-reference/questionnaire-submission/submit-questionnaire)
* [Browse questionnaires API](/api-reference/questionnaire/browse-questionnaires)
* [Browse questions API](/api-reference/question/browse-questions)
