# Add a question to a section of a draft

Append a question to a section of a draft.

`POST /supplier-assessment-templates/{templateId}/questions`

Scope `supplier_template:write` | Plan **Growth+**

Append a question to a section of a draft. `sectionId`, `answerType` and `text` (`en` required) are required; `questionId` and the order are assigned by the server. A choice question (`single_select`, `multi_select`) must be created with 2 to 12 `options`, every other answer type with none. `weight` is 1 to 10, `domain` one of the listed domains (or empty), `controlIds` and `modes` are free tokens of at most 64 characters. The draft's `revision` is optional here; when sent (in the body or as `If-Match`) a stale value fails with 409 STALE_REVISION. A section holds 50 questions and a questionnaire 300 (409 QUESTION_LIMIT_REACHED), and the document may not exceed 1 MiB (409 TEMPLATE_SIZE_LIMIT_REACHED). The question is added to the draft only; it is scored after a person publishes the draft. Structural write: 30 per token and hour. Requires the supplier_template:write scope (or a signed-in Owner, Manager or Contributor session).

## Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `templateId` | string | yes | Questionnaire document id (`satpl_...`), or `satpl_builtin_supplier_security_privacy_v1` for the built-in one. |

## Header parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `If-Match` | string | no | The draft revision as `W/"<revision>"`, from the ETag header. Wins over a `revision` in the body. A write on an older revision fails with 409 STALE_REVISION. pattern `^W/"[0-9]+"$` |

## Request body

- `sectionId`: string minLength 1, maxLength 64
- `text`: object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
  - `en`: string minLength 1, maxLength 500
  - `ru`: optional string maxLength 500
  - `he`: optional string maxLength 500
- `help`: optional object Text per locale, at most 2000 characters per locale; HTML is rejected. All locales are optional.
  - `en`: optional string maxLength 2000
  - `ru`: optional string maxLength 2000
  - `he`: optional string maxLength 2000
  - No other fields.
- `evidenceHint`: optional object Text per locale, at most 2000 characters per locale; HTML is rejected. All locales are optional.
  - `en`: optional string maxLength 2000
  - `ru`: optional string maxLength 2000
  - `he`: optional string maxLength 2000
  - No other fields.
- `answerType`: string
  - `text`
  - `email`
  - `phone`
  - `textarea`
  - `single_select`
  - `multi_select`
- `required`: optional boolean
- `modes`: optional array of string
- `domain`: optional string
  - ``
  - `governance`
  - `privacy`
  - `access`
  - `operations`
  - `resilience`
  - `supply_chain`
  - `contractual`
- `weight`: optional integer Scoring weight. minimum 1, maximum 10
- `controlIds`: optional array of string
- `allowNotApplicable`: optional boolean
- `options`: optional array of object Required (2 to 12) for single_select and multi_select, empty or absent for every other answer type. maxItems 12
  - `label`: object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
    - `en`: string minLength 1, maxLength 500
    - `ru`: optional string maxLength 500
    - `he`: optional string maxLength 500
  - `riskValue`: integer Scoring: 0 (no risk) to 4. minimum 0, maximum 4
  - `findingSeverity`: optional string
    - ``
    - `info`
    - `low`
    - `medium`
    - `high`
    - `critical`
  - `finding`: optional object Text per locale, at most 2000 characters per locale; HTML is rejected. All locales are optional.
    - `en`: optional string maxLength 2000
    - `ru`: optional string maxLength 2000
    - `he`: optional string maxLength 2000
    - No other fields.
  - `notApplicable`: optional boolean
  - No other fields.
- `revision`: optional integer The draft revision, honoured when sent; or send it as If-Match. minimum 0
- No other fields.

## Returns

The `data` field of the standard `{ data, error, meta }` envelope:

- `templateId`: string
- `templateKey`: string Shared by all versions of the questionnaire.
- `version`: integer 0 for an open draft.
- `titleKey`: string
- `descriptionKey`: optional string
- `title`: optional object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
  - `en`: string minLength 1, maxLength 500
  - `ru`: optional string maxLength 500
  - `he`: optional string maxLength 500
- `description`: optional object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
  - `en`: string minLength 1, maxLength 500
  - `ru`: optional string maxLength 500
  - `he`: optional string maxLength 500
- `modes`: array of string
- `source`: optional string
  - `builtin`
  - `authored`
- `status`: optional string draft and submitted are open (editable or awaiting approval); published is the live version; archived is retired.
  - `draft`
  - `submitted`
  - `published`
  - `archived`
- `active`: boolean
- `isDefault`: optional boolean
- `questionCount`: optional integer
- `revision`: optional integer Send with an edit; changes on every structural edit.
- `contentHash`: optional string
- `clonedFromKey`: optional string
- `clonedFromVersion`: optional integer
- `parentVersion`: optional integer
- `publishedAt`: optional string (date-time)
- `publishedBy`: optional string
- `createdAt`: string (date-time)
- `updatedAt`: string (date-time)
- `createdBy`: optional string
- `updatedBy`: optional string
- `lastEditedBy`: optional string
- `lastEditedAt`: optional string (date-time)
- `submittedBy`: optional string
- `submittedAt`: optional string (date-time)
- `submitterNote`: optional string
- `rejectedBy`: optional string
- `rejectedAt`: optional string (date-time)
- `rejectionReason`: optional string
- `sections`: array of object
  - `sectionId`: string
  - `titleKey`: string
  - `title`: optional object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
    - `en`: string minLength 1, maxLength 500
    - `ru`: optional string maxLength 500
    - `he`: optional string maxLength 500
  - `order`: integer
  - `questions`: array of object
    - `questionId`: string
    - `textKey`: string Built-in text key; authored questions carry `text` instead.
    - `helpKey`: optional string
    - `answerType`: string
      - `text`
      - `email`
      - `phone`
      - `textarea`
      - `single_select`
      - `multi_select`
    - `required`: boolean
    - `modes`: array of string
    - `domain`: string
      - ``
      - `governance`
      - `privacy`
      - `access`
      - `operations`
      - `resilience`
      - `supply_chain`
      - `contractual`
    - `syncTarget`: optional string
    - `evidenceHintKey`: optional string
    - `options`: optional array of object
      - `optionId`: string
      - `labelKey`: string Built-in text key; authored options carry `label` instead.
      - `notApplicable`: optional boolean
      - `label`: optional object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
        - `en`: string minLength 1, maxLength 500
        - `ru`: optional string maxLength 500
        - `he`: optional string maxLength 500
      - `riskValue`: optional integer Scoring: 0 (no risk) to 4. minimum 0, maximum 4
      - `findingSeverity`: optional string
        - `info`
        - `low`
        - `medium`
        - `high`
        - `critical`
      - `findingKey`: optional string
      - `finding`: optional object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
        - `en`: string minLength 1, maxLength 500
        - `ru`: optional string maxLength 500
        - `he`: optional string maxLength 500
    - `requiresVendorTypes`: optional array of string
    - `allowNotApplicable`: optional boolean
    - `requiresResponses`: optional array of object
      - `questionId`: string
      - `anyOption`: array of string
    - `text`: optional object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
      - `en`: string minLength 1, maxLength 500
      - `ru`: optional string maxLength 500
      - `he`: optional string maxLength 500
    - `help`: optional object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
      - `en`: string minLength 1, maxLength 500
      - `ru`: optional string maxLength 500
      - `he`: optional string maxLength 500
    - `evidenceHint`: optional object Text per locale. `en` is required; `ru` and `he` fall back to `en` when blank. At most 500 characters per locale (2000 for a description); HTML is rejected.
      - `en`: string minLength 1, maxLength 500
      - `ru`: optional string maxLength 500
      - `he`: optional string maxLength 500
    - `authored`: optional boolean
    - `weight`: optional integer Scoring weight. minimum 1, maximum 10
    - `controlIds`: optional array of string
- `limits`: object The structural caps the server enforces.
  - `maxSections`: integer maximum 20
  - `maxQuestionsPerSection`: integer maximum 50
  - `maxQuestionsPerTemplate`: integer maximum 300
  - `maxOptionsPerQuestion`: integer maximum 12
- `permissions`: optional object What the caller may do with this version right now.
  - `canEdit`: boolean
  - `canSubmit`: boolean
  - `canWithdraw`: boolean
  - `canReject`: boolean
  - `canPublish`: boolean or null
  - `canSetDefault`: boolean
  - `canArchive`: boolean
  - `canDelete`: boolean
  - `publishBlockedReason`: string or null

## Errors

| Status | Code | Retry | Meaning |
|---|---|---|---|
| 400 | `VALIDATION_FAILED` | no | The JSON body does not match the request schema, or the revision is missing where required. code VALIDATION_FAILED. |
| 401 | `INVALID_TOKEN` | no | Missing (UNAUTHORIZED) or invalid, expired or revoked (INVALID_TOKEN) token. |
| 402 | `FEATURE_GATED` | no | The tenant plan does not include supply chain security. code FEATURE_GATED; args.feature, args.minTier, args.minTierName. |
| 403 | `INSUFFICIENT_SCOPE` | no | The token lacks supplier_template:write (INSUFFICIENT_SCOPE), has no accountable person (FORBIDDEN: this principal cannot edit questionnaires), or the target is the built-in questionnaire (BUILTIN_IMMUTABLE). |
| 404 | `SECTION_NOT_FOUND` | no | Unknown questionnaire (NOT_FOUND) or unknown section in it (SECTION_NOT_FOUND). |
| 409 | `QUESTION_LIMIT_REACHED` | no | Everything in EditConflict, plus QUESTION_LIMIT_REACHED (50 questions in the section or 300 in the questionnaire). |
| 413 | `PAYLOAD_TOO_LARGE` | no | The JSON body exceeds the size limit. code PAYLOAD_TOO_LARGE. |
| 429 | `RATE_LIMITED` | yes | Too many failed authentication attempts from this address, or the token used its hourly write budget (30 structural, 120 field edits). code RATE_LIMITED; wait Retry-After seconds, also in error.args.retryAfterSeconds. |
| 500 | `DB_ERROR` | no | Unexpected server error. code DB_ERROR; quote error.id to support. |
| 503 | `BILLING_UNAVAILABLE` | yes | Billing is temporarily unavailable, so the plan could not be checked. code BILLING_UNAVAILABLE. Retry later. |

## Example

### curl

```bash
curl -X POST "https://api.autociso.io/api/v1/supplier-assessment-templates/<templateId>/questions" \
  -H "Authorization: Bearer <your-api-token>" \
  -H "Content-Type: application/json" \
  -d '{"sectionId":"sect_1hgyun41rx4a","answerType":"single_select","text":{"en":"Is data encrypted at rest?"},"required":true,"modes":[],"domain":"operations","weight":5,"options":[{"label":{"en":"Yes, all stores"},"riskValue":0},{"label":{"en":"No"},"riskValue":4,"findingSeverity":"high","finding":{"en":"Data at rest is not encrypted."}}],"revision":3}'
```

### Python

```python
import requests

response = requests.post(
    "https://api.autociso.io/api/v1/supplier-assessment-templates/<templateId>/questions",
    headers={"Authorization": "Bearer <your-api-token>", "Content-Type": "application/json"},
    json={
        "sectionId": "sect_1hgyun41rx4a",
        "answerType": "single_select",
        "text": {
            "en": "Is data encrypted at rest?",
        },
        "required": True,
        "modes": [],
        "domain": "operations",
        "weight": 5,
        "options": [
            {
                "label": {
                    "en": "Yes, all stores",
                },
                "riskValue": 0,
            },
            {
                "label": {
                    "en": "No",
                },
                "riskValue": 4,
                "findingSeverity": "high",
                "finding": {
                    "en": "Data at rest is not encrypted.",
                },
            },
        ],
        "revision": 3,
    },
)
print(response.json())
```

### JavaScript

```javascript
const response = await fetch("https://api.autociso.io/api/v1/supplier-assessment-templates/<templateId>/questions", {
  method: "POST",
  headers: { "Authorization": "Bearer <your-api-token>", "Content-Type": "application/json" },
  body: JSON.stringify({
    "sectionId": "sect_1hgyun41rx4a",
    "answerType": "single_select",
    "text": {
      "en": "Is data encrypted at rest?"
    },
    "required": true,
    "modes": [],
    "domain": "operations",
    "weight": 5,
    "options": [
      {
        "label": {
          "en": "Yes, all stores"
        },
        "riskValue": 0
      },
      {
        "label": {
          "en": "No"
        },
        "riskValue": 4,
        "findingSeverity": "high",
        "finding": {
          "en": "Data at rest is not encrypted."
        }
      }
    ],
    "revision": 3
  }),
});
console.log(await response.json());
```

## Response (201)

```json
{
  "data": {
    "templateId": "satpl_1hgyun41rx48",
    "templateKey": "tpl_1hgyun41rx49",
    "version": 0,
    "titleKey": "",
    "title": {
      "en": "Example vendor security review"
    },
    "modes": [],
    "source": "authored",
    "status": "draft",
    "active": false,
    "questionCount": 1,
    "revision": 4,
    "createdAt": "2026-10-09T08:15:00Z",
    "updatedAt": "2026-10-09T08:20:00Z",
    "sections": [
      {
        "sectionId": "sect_1hgyun41rx4a",
        "titleKey": "",
        "title": {
          "en": "Governance"
        },
        "order": 1,
        "questions": [
          {
            "questionId": "saq_1hgyun41rx4d",
            "textKey": "",
            "text": {
              "en": "Is data encrypted at rest?"
            },
            "answerType": "single_select",
            "required": true,
            "modes": [],
            "domain": "operations",
            "weight": 8,
            "authored": true,
            "options": [
              {
                "optionId": "saqo_1hgyun41rx4e",
                "labelKey": "",
                "label": {
                  "en": "Yes, all stores"
                },
                "riskValue": 0
              },
              {
                "optionId": "saqo_1hgyun41rx4f",
                "labelKey": "",
                "label": {
                  "en": "No"
                },
                "riskValue": 4,
                "findingSeverity": "high"
              }
            ]
          }
        ]
      }
    ],
    "limits": {
      "maxSections": 20,
      "maxQuestionsPerSection": 50,
      "maxQuestionsPerTemplate": 300,
      "maxOptionsPerQuestion": 12
    },
    "permissions": {
      "canEdit": true,
      "canSubmit": true,
      "canWithdraw": false,
      "canReject": false,
      "canPublish": false,
      "canSetDefault": false,
      "canArchive": false,
      "canDelete": true,
      "publishBlockedReason": null
    }
  }
}
```
