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

# SDK endpoints

> REST endpoints the React Native SDK uses — call them directly to integrate from any other platform.

The SDK wraps these endpoints into idiomatic mobile code.
If you're integrating from a platform we don't have an SDK for yet
(Flutter, Swift, Kotlin, web), call these directly.

Base URL: `https://api.insito.app/v1/sdk`. See
[Authentication](/api/authentication) for the `Bearer proj_xxx`
header.

***

## `POST /identify`

Idempotent upsert of a respondent. Call once when your user signs in
or as early as you have a stable `userId`.

### Request

```json theme={null}
{
  "userId": "user-123",        // required, your stable identifier
  "platform": "ios",            // optional
  "appVersion": "2.7.0",        // optional
  "metadata": { "cohort": "beta-q2" }, // optional device metadata
  "properties": {                // optional custom user properties
    "plan": "premium",           // string | number | boolean values only
    "transactionsThisMonth": 14,
    "hasLinkedCard": true
  },
  "events": [                    // optional, declare trigger events
    { "name": "checkout_completed", "description": "User finished a purchase" }
  ],
  "screens": [                   // optional, declare screens
    { "name": "Checkout" }
  ]
}
```

### Response

```json theme={null}
{
  "respondentId": "550e8400-e29b-41d4-a716-446655440000"
}
```

Idempotent on `(project_id, userId)` — calling again with the same
userId updates `platform`, `appVersion`, `metadata`, `properties`, and
`last_seen_at` but doesn't create a new respondent.

`properties` are custom, business-level user attributes (only `string` /
`number` / `boolean` values are kept). They're stored on the respondent and
discovered into the project's [User Properties registry](/dashboard/user-properties),
where they become type-aware audience filters. The API stores exactly the
`metadata` object you send. Our SDKs merge device metadata (`locale`,
`timezone`, `osVersion`, `sessionCount`, `daysSinceInstall`) into `metadata`
before calling this endpoint; if you call it directly, send those keys yourself
to use them in audience filters.

`events` and `screens` (each up to 200 items, `name` 1–256 chars,
optional `description` ≤ 500 chars) register entries in the project's
[App Variable Registry](/dashboard/variables) as **declared** (source
`sdk_init`). They flip to **verified** once the SDK actually fires
them. Auto-discovered events/screens are registered automatically by
`POST /event` and `POST /screen-map` — you only need these fields to
pre-declare variables before they're first seen.

***

## `POST /event`

Trigger evaluation. Decides whether to show a survey.

### Request

```json theme={null}
{
  "event": "checkout_completed",  // required string, 1–128 chars (snake_case by convention)
  "userId": "user-123"             // required
}
```

### Response (no survey)

```json theme={null}
{ "survey": null }
```

Reasons: user not yet identified, no active survey for this trigger,
user is within the throttle window, plan is exhausted, or response
limit reached on the survey.

### Response (survey shown)

```json theme={null}
{
  "survey": {
    "surveyId": "uuid",
    "logic": {},
    "showBranding": true,
    "showQuestionNumbers": true,
    "allowBack": true,
    "autosaveProgress": true,
    "delaySeconds": 3,
    "questions": [
      {
        "id": "uuid",
        "type": "nps",
        "text": "How likely are you to recommend us?",
        "options": null,
        "settings": null,
        "isRequired": true,
        "position": 0
      }
    ]
  },
  "brandConfig": {
    "themeMode": "auto",
    "colors": { "primary": "#FF6B35", "background": null, "surface": null, "textPrimary": null, "textSecondary": null, "border": null },
    "colorsDark": { "primary": "#FF8A5C", "background": null, "surface": null, "textPrimary": null, "textSecondary": null, "border": null },
    "typography": { "fontFamily": "Inter", "questionSize": "medium", "bodySize": "medium", "questionWeight": "bold" },
    "shape": { "borderRadius": 12 },
    "spacing": "default",
    "branding": { "showPoweredBy": true, "logoUrl": null }
  },
  "labels": {
    "next": "Continue",
    "npsLowLabel": "Not likely",
    "npsHighLabel": "Very likely"
  }
}
```

Survey-level flags control the modal UX:

| Field                 | Default | Notes                                                    |
| --------------------- | ------- | -------------------------------------------------------- |
| `showBranding`        | `true`  | Footer "Powered by Insito". Forced on Free/Starter.      |
| `showQuestionNumbers` | `true`  | Progress labels like `Question 1 of 3`.                  |
| `allowBack`           | `true`  | Back button in the footer when not on the first step.    |
| `autosaveProgress`    | `true`  | When `true`, partial answers are saved via `/responses`. |

The survey object also carries `logic` (branching rules between questions,
`{}` when none) and, when the trigger defines a delay, `delaySeconds` (wait this
long after qualifying before showing). Each question includes a `settings`
object with type-specific configuration (scale bounds, screen copy, etc.), or
`null`.

**`brandConfig`** (INS-103) is the project's [Brand Kit](/dashboard/brand-kit). It is
**only present when the project has saved a Brand Kit** — projects with no
Brand Kit omit the key entirely, and the SDK keeps the theme from `init()`.
Returned by both `/event` and `/evaluate`. `null` colours / `borderRadius` /
`fontFamily` mean "inherit the SDK preset". `branding.showPoweredBy` is forced
to `true` below the Growth plan. The SDK applies it on top of the base preset
but below any `init()` overrides (code wins over the dashboard).

**`themeMode`** (INS-104) is `"light"`, `"dark"`, or `"auto"`. `auto` follows
the device appearance (`Appearance.getColorScheme()`) and switches live while
the modal is open. **`colorsDark`** is the dark-scheme palette, applied only
when dark is resolved; `null` tokens inherit the SDK `dark` preset.

**`labels`** (INS-135) are the project's [SDK UI label](/dashboard/labels)
overrides. Like `brandConfig`, the key is **only present when the project has
overridden at least one label** — and only the overridden keys are included.
Returned by both `/event` and `/evaluate`. The SDK merges them over its built-in
defaults and below any `init({ labels })` overrides (code wins over the
dashboard). See [SDK labels](/sdk/react-native/labels) for the full key list.

`options` shape per question type:

* `nps`: `null` — score is always 0–10
* `rating`: `{ "max": 5 }`
* `multiple_choice`: `{ "choices": ["A", "B", "C"], "multiSelect": false }`
* `open_text`: `{ "maxLength": 500 }`

***

## `POST /evaluate`

Auto-evaluated trigger check. Unlike `/event` (named events), the SDK calls this
for lifecycle triggers — **app open**, **session start**, and **screen visit** —
reporting its local per-user counters so the server can match thresholds,
audience filters, and frequency caps.

### Request

```json theme={null}
{
  "userId": "user-123",              // required
  "currentScreen": "/checkout",       // optional; required for screen-visit triggers
  "contextType": "screen_visit",      // optional: "app_open" | "session_start" | "screen_visit"
  "coldStart": false,                 // optional; lets cold-start-only session triggers qualify
  "counters": {                       // optional local counters
    "appOpenCount": 12,
    "sessionCount": 8,
    "screenVisits": { "/checkout": 3 }
  },
  "properties": { "plan": "premium" } // optional
}
```

### Response

Same shape as [`POST /event`](#post-event) — `{ "survey": null }` or a survey
object (optionally with `brandConfig` and `labels`). The SDK follows a matched
survey with `POST /impression` just like the named-event flow.

***

## `POST /impression`

Records that a survey was shown to a user. Sets the per-user
throttle window in Redis. The SDK fires this right after rendering
the modal — don't call it from a `wait, did the user see it?`
position.

### Request

```json theme={null}
{
  "surveyId": "uuid",  // from the previous /event call
  "userId": "user-123"
}
```

### Response

```json theme={null}
{ "ok": true }
```

Internal note: Redis failures here are non-fatal (the data row is
written either way). The throttle key just has a slightly delayed
TTL when Redis is unhealthy.

***

## `POST /responses`

Create or upsert a **partial** response while autosave is enabled. The SDK
calls this as the user advances through questions.

### Request

```json theme={null}
{
  "surveyId": "uuid",
  "userId": "user-123",
  "answers": [
    { "questionId": "uuid", "type": "nps", "value": 9 }
  ],
  "platform": "ios",
  "appVersion": "2.7.0"
}
```

### Response

```json theme={null}
{ "responseId": "550e8400-...", "status": "partial" }
```

Returns `201` when it creates the partial row (which counts against the app's
monthly quota) and `200` when it updates the existing one — it's idempotent per
`(survey, respondent)` while status is `partial`. Returns `400 autosave_disabled`
if the survey has autosave turned off, `404` if the survey or respondent is
unknown, and `402 response_limit_reached` if the app is over its cap.

***

## `PATCH /responses/:id`

Replace the answers on an existing partial response by its id.

### Request

```json theme={null}
{
  "surveyId": "uuid",
  "userId": "user-123",
  "answers": [
    { "questionId": "uuid", "type": "open_text", "value": "Great app!" }
  ],
  "platform": "ios",
  "appVersion": "2.7.0"
}
```

### Response

```json theme={null}
{ "responseId": "550e8400-...", "status": "partial" }
```

***

## `POST /response`

Submit the user's answers. Counts against your plan. Completes a partial
response when `responseId` is supplied.

### Request

```json theme={null}
{
  "surveyId": "uuid",
  "userId": "user-123",
  "responseId": "550e8400-...",
  "answers": [
    { "questionId": "uuid", "type": "nps", "value": 9 },
    { "questionId": "uuid", "type": "open_text", "value": "Love it!" }
  ],
  "platform": "ios",
  "appVersion": "2.7.0"
}
```

Omit `responseId` for a one-shot submit (no prior partial row).

### Response (success)

```json theme={null}
{ "responseId": "550e8400-..." }
```

### Response (plan exhausted)

```json theme={null}
{
  "error": "response_limit_reached",
  "upgradeUrl": "https://app.insito.io/settings/billing",
  "projectId": "550e8400-..."
}
```

Status `402 Payment Required` (`upgradeUrl` and `projectId` are top-level, not
nested). The SDK surfaces this as a `response_failed` event with
`reason: "response_limit_reached"`.

### Answer `value` shapes

| Question type              | `value`                                  |
| -------------------------- | ---------------------------------------- |
| `nps`                      | Integer 0..10                            |
| `rating`                   | Integer 1..5                             |
| `multiple_choice` (single) | String matching one of `options.choices` |
| `multiple_choice` (multi)  | Array of strings                         |
| `open_text`                | String, ≤ `options.maxLength`            |

***

## `POST /screen-map`

Batch upsert of screen visits. The SDK aggregates locally for \~60s
then flushes here. Send `null` or omit if you don't use screen
tracking.

### Request

```json theme={null}
{
  "screens": {
    "/home": 12,
    "/settings": 3,
    "/checkout/success": 1
  }
}
```

Up to 200 paths per call.

### Response

```json theme={null}
{ "ok": true }
```

***

## Cross-cutting

All SDK endpoints (`GET /me`, `POST /identify`, `POST /event`, `POST /evaluate`,
`POST /impression`, `POST /responses`, `PATCH /responses/:id`, `POST /response`,
`POST /screen-map`):

* Authenticate with `Authorization: Bearer proj_xxx`.
* Share the 120 req/min per-key rate limit (see [Authentication](/api/authentication#rate-limits)).
* Take a JSON body (`Content-Type: application/json`).
* Validate with [zod](https://zod.dev) — a 400 returns
  `{ "error": "bad_request", "issues": [{ "path": "...", "message": "..." }] }`.
* Return `{ "error": "internal_error" }` with status `500` on database failures.

## OpenAPI

We're publishing a full OpenAPI 3.1 spec — track \[INS-…] for the
schedule. Right now this page is the canonical reference.
