> ## 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.

# Dashboard endpoints

> Programmatic access to surveys, projects, responses, analytics, integrations, and billing.

The dashboard endpoints back everything at admin.insito.app. They're
public and stable — use them to build internal tooling, custom
dashboards, or to migrate data out of Insito.

Base URL: `https://api.insito.app/v1/dashboard`. See
[Authentication](/api/authentication) for the Supabase JWT bearer
flow.

***

## Projects

### `GET /projects`

List projects for the authenticated user's org. The response is wrapped in a
`projects` array and uses the snake\_case column names from the database.

```json theme={null}
{
  "projects": [
    {
      "id": "uuid",
      "short_id": "a1b2c3d4",
      "name": "Acme iOS",
      "platform": "react_native",
      "api_key": "proj_xxx",
      "created_at": "2026-05-01T10:00:00Z"
    }
  ]
}
```

### `POST /projects`

Create a project. Triggers the welcome email on org's first project
(see [Welcome email runbook](https://github.com/VJiri/insito-api/blob/main/docs/runbook/INS-welcome-email.md)).

```json theme={null}
{ "name": "Acme Android", "platform": "react_native" }
```

Platform is one of `react_native` (default), `ios`, `android`, `flutter`.

### `GET /projects/:id`

Single project plus a `respondent_count` (number of identified users).

### `PATCH /projects/:id`

Update an existing project. All fields optional; send only what changed.

```json theme={null}
{
  "name": "Acme Android",
  "brandConfig": {
    "themeMode": "auto",
    "colors": { "primary": "#FF6B35" },
    "colorsDark": { "primary": "#FF8A5C" },
    "typography": { "questionWeight": "bold", "fontFamily": "Inter" },
    "shape": { "borderRadius": 12 },
    "spacing": "default",
    "branding": { "showPoweredBy": false }
  },
  "labels": {
    "next": "Continue",
    "skip": null
  },
  "autoDiscoverEvents": false,
  "autoDiscoverScreens": true
}
```

`brandConfig` is the project [Brand Kit](/dashboard/brand-kit). The server
merges it onto canonical defaults and **clamps it to the org's plan** —
`branding.showPoweredBy` is forced to `true` below the Growth plan. It is then
returned to the SDK in the survey response (see
[`POST /event`](/api/sdk-endpoints)).

`themeMode` (`"light"` / `"dark"` / `"auto"`) selects the survey appearance and
`colorsDark` holds the dark-scheme palette (INS-104). Both fields are optional;
omitted fields keep their stored value (or the canonical default — `themeMode`
defaults to `"light"`).

`labels` (INS-135) are the project's [SDK UI label](/dashboard/labels)
overrides. Send only the keys that changed: a **string** sets the override, and
**`null`** clears it back to the SDK default. Other keys keep their stored value.
Empty / whitespace-only strings are dropped (never stored). See
[SDK labels](/sdk/react-native/labels) for the full key list.

`autoDiscoverEvents` and `autoDiscoverScreens` (INS-160) are independent
booleans (default `true`) controlling whether the SDK ingestion endpoints
auto-register new events / screens into the variable registry. When `false` for
a type, new `sdk_auto` entries of that type stop being created but "last seen"
keeps updating on existing ones; declared and manual variables are unaffected.

### `DELETE /projects/:id`

Soft-delete — sets `is_active = false` and returns `{ "ok": true }`. It does
**not** cascade-delete surveys, respondents, or responses. Use the dashboard's
confirmation flow rather than this endpoint directly.

***

## Surveys

### `GET /projects/:projectId/surveys`

List surveys for a project, ordered by `created_at` desc.

### `POST /projects/:projectId/surveys`

Create a survey. Body matches the [creating-surveys](/dashboard/creating-surveys)
shape — name, triggerKey, throttleDays, audience, plus a `questions`
array (1–20 items total, including welcome and end screens).

### `PATCH /surveys/:id`

Partial update. Builder edits write to the survey **draft** JSON column only
— live SDK content is unchanged until you call **Publish**.

Status-only patches (`{ "status": "active" }` with no other fields) update
live columns immediately (used from the survey list).

Draft PATCH response:

```json theme={null}
{
  "survey": {
    "id": "uuid",
    "name": "Post-checkout NPS",
    "has_unpublished_changes": true,
    "unpublished_change_count": 3,
    "showBranding": true,
    "showQuestionNumbers": true,
    "allowBack": true,
    "autosaveProgress": true
  },
  "draft_saved_at": "2026-06-09T14:22:00Z"
}
```

Behavior fields (`showBranding`, `showQuestionNumbers`, `allowBack`,
`autosaveProgress`) are clamped to the org's plan on save.

### `POST /surveys/:id/publish`

Copy draft → live columns and set `published_at`. Optional body:

```json theme={null}
{ "status": "active" }
```

Use `{ "status": "active" }` when activating a never-published draft.
For active/paused surveys, omit `status` to publish content without
changing eligibility.

Response:

```json theme={null}
{
  "survey": { "...": "..." },
  "published_at": "2026-06-09T14:25:00Z"
}
```

### `DELETE /surveys/:id`

**Hard delete** — removes the survey row (and its responses). Returns
`{ "ok": true }`. To deactivate without deleting, PATCH `{ "status": "paused" }`.

### `GET /surveys/:id/analytics`

NPS analytics for the survey's first NPS question, powering the dashboard cards
and trend chart. Response is wrapped in `analytics`:

```json theme={null}
{
  "analytics": {
    "score": 32,
    "total": 247,
    "promoters": 118,
    "passives": 74,
    "detractors": 55,
    "promoterPct": 48,
    "passivePct": 30,
    "detractorPct": 22,
    "trend": [
      { "date": "2026-05-15", "total": 12, "score": 28 }
    ]
  }
}
```

The `trend` array holds daily UTC buckets for the last 30 days. If the survey has
no NPS question, the counts come back as zeros.

***

## Responses

### `GET /projects/:projectId/responses`

Offset-paginated response list across the project.

Query params:

| Param      | Type     | Notes                                       |
| ---------- | -------- | ------------------------------------------- |
| `surveyId` | uuid     | Filter to one survey                        |
| `from`     | ISO date | Inclusive lower bound                       |
| `to`       | ISO date | Inclusive upper bound                       |
| `platform` | string   | `react_native`, `ios`, `android`, `flutter` |
| `limit`    | int      | 1–100, default 20                           |
| `offset`   | int      | Default 0                                   |

Response is `{ responses, total, limit, offset }`.

### `GET /surveys/:id/responses`

Cursor-paginated responses for a single survey, newest first. Powers
the Survey Builder **Responses** tab. Includes both `completed` and
`partial` responses.

Query params:

| Param    | Type   | Notes                                                                                |
| -------- | ------ | ------------------------------------------------------------------------------------ |
| `cursor` | string | Opaque keyset cursor from the previous page's `nextCursor`. Omit for the first page. |
| `limit`  | int    | 1–100, default 50                                                                    |

```json theme={null}
{
  "responses": [
    {
      "id": "f0b1…",
      "submittedAt": "2026-06-18T09:24:11.512+00:00",
      "status": "completed",
      "platform": "ios",
      "appVersion": "3.4.0",
      "respondentId": "a91c…",
      "answers": {
        "q_1f2e…": 9,
        "q_88ac…": ["Pricing", "Onboarding"],
        "q_4d10…": "Loved the new dashboard"
      }
    }
  ],
  "nextCursor": "MjAyNi0wNi0xOFQwOToyNDoxMS41MTIrMDA6MDB8ZjBiMQ",
  "totalCount": 1432
}
```

* `answers` is a **map keyed by question id** (not an array). A
  question that's absent from the map was skipped or never reached
  (branching) — render it as empty / an em dash.
* `nextCursor` is `null` on the last page. Pass it back as `cursor`
  to fetch the next page. Pagination is keyset-based on
  `(submitted_at, id)`, so it stays stable as new responses arrive.
* `totalCount` is the exact total for the survey; it's only returned
  on the first page (`null` on subsequent pages).

### `GET /surveys/:id/responses/export`

Streams the survey's full response set as `text/csv` (no pagination,
no plan gating). Returns:

* `Content-Type: text/csv; charset=utf-8`
* `Content-Disposition: attachment; filename="{survey-slug}-responses-{YYYY-MM-DD}.csv"`

The first row is a header built from the response metadata (`Submitted
At, Status, Platform, App Version, Respondent ID`) followed by one
column per (non-screen) question, using the question text. Each
subsequent row is one response. The `Respondent ID` column contains the SDK
`userId` (`external_user_id`), not the internal UUID. Fields are RFC-4180
escaped (values containing commas, quotes, or newlines are wrapped in double
quotes with interior quotes doubled). Multiple-choice answers are joined with
`; `, and skipped questions are empty cells. A UTF-8 BOM is prepended so
spreadsheet apps detect the encoding.

***

## Integrations

Integrations are **project-scoped**. Each project stores at most one row per
type (`slack`, `webhook`, `email_digest`).

### `GET /projects/:projectId/integrations`

Returns the project's configured integrations.

### `POST /projects/:projectId/integrations`

Upserts an integration. The body is a discriminated union on `type`:

```json theme={null}
{ "type": "webhook", "url": "https://your-domain.com/insito-webhooks" }
```

```json theme={null}
{ "type": "email_digest", "recipients": ["team@acme.com"] }
```

### `DELETE /projects/:projectId/integrations/:type`

Removes the integration of the given type.

### `POST /projects/:projectId/integrations/slack/connect`

Kicks off the Slack OAuth flow. Returns `{ "url": "https://slack.com/oauth/..." }`
to send the browser to; the callback handles the rest.

***

## Variables

The [App Variable Registry](/dashboard/variables) — the project's events and
screens. Auto-discovered (`sdk_auto`) and SDK-declared (`sdk_init`) entries are
created by the [SDK endpoints](/api/sdk-endpoints); the routes below manage
manual entries and labels/descriptions.

### `GET /projects/:projectId/variables`

Lists variables for the project, each with `type`, `name`, `display_label`,
`description`, `source`, `verified`, `approved`, `first_seen_at`, `last_seen_at`,
and `created_at`. Optional query params: `type` (`event` | `screen`) and
`approved` (`true` | `false`) to filter the Available vs Auto-discovered lists.

### `POST /projects/:projectId/variables`

Creates a manual variable. Returns `409` if `(type, name)` already exists.

```json theme={null}
{
  "type": "event",                 // "event" | "screen"
  "name": "checkout_completed",    // required, 1–256 chars
  "displayLabel": "Checkout done", // optional
  "description": "User paid"       // optional, ≤ 500 chars
}
```

Manual variables (`POST`) are created with `approved: true`; auto-discovered
(`sdk_auto`) entries start `approved: false` and only appear in the survey
builder's trigger picker once approved.

### `PATCH /projects/:projectId/variables/:varId`

Updates `name`, `displayLabel`, and/or `description`. Send at least one field.
Renaming `name` cascades the change into every survey that references the key
(live and draft configurations) via the `rename_app_variable` RPC.

### `PATCH /projects/:projectId/variables/:varId/approve`

Approves a single auto-discovered variable so it becomes selectable in the
builder.

### `POST /projects/:projectId/variables/approve-all`

Approves all currently-unapproved variables. Optional `type` query param scopes
it to `event` or `screen`.

### `GET /projects/:projectId/variables/:varId/usage`

Returns the surveys that reference the variable, so the UI can warn before a
rename or delete.

### `DELETE /projects/:projectId/variables/:varId`

Deletes any variable, regardless of `source`. SDK-reported names may reappear in
the Auto-discovered list on the next ingestion.

***

## User properties

The [User Properties registry](/dashboard/user-properties) — the custom,
business-level properties discovered from
[`identify({ properties })`](/api/sdk-endpoints). Properties are discovery-only:
they're created and typed by the SDK, so there's no create or delete route.

### `GET /projects/:projectId/user-properties`

Lists every discovered property, each with `name`, `type`
(`string` | `number` | `boolean`), `display_label`, `description`,
`first_seen_at`, `last_seen_at`, and an approximate `user_count`.

### `PATCH /projects/:projectId/user-properties/:name`

Updates `displayLabel` and/or `description` only. Send at least one field. The
`:name` path segment is the property key (URL-encoded).

```json theme={null}
{
  "displayLabel": "Subscription plan",
  "description": "The user's current billing tier"
}
```

***

## Billing

### `GET /billing/plans`

Returns the plan catalog (`free`, `starter`, `growth`, `studio`) with prices,
response limits, app limits, and features.

### `POST /billing/checkout`

Creates a Stripe Checkout session for a plan upgrade. Returns `{ "url": "..." }`
— redirect the user there.

```json theme={null}
{ "planId": "growth" }
```

### `POST /billing/portal`

Creates a Stripe customer portal session. Returns `{ url }` for downgrades,
cancellation, and card management.

### `GET /billing/invoices`

Lists the org's Stripe invoices.

Additional add-on routes exist for purchasable extra apps:
`POST /billing/checkout-addon` and `POST /billing/cancel-addon`.

### Usage

There is no `/billing/usage` route. Use:

* `GET /organization/usage` → `{ "usage": { "plan", "year", "month", "responseCount", "responseLimit", "percent" } }` (org aggregate).
* `GET /projects/:id/usage` → per-app usage (the value the SDK cap is checked against).

***

## Organization

### `GET /organization`

Current org row, wrapped in `organization`: `id`, `name`, `slug`, `plan`,
`plan_app_limit`, `plan_response_limit`, `usage_context`, `goals`,
`stripe_customer_id`, `stripe_subscription_id`, `created_at`, `updated_at`. It
does not embed a member list.

### `PATCH /organization`

Edit org-level fields. Optional body: `name`, `usageContext` (`work` |
`personal` | `null`), and `goals` (array, max 5). Members manage their own
profile via `PATCH /members/me`.

***

## Conventions

* Every endpoint returns JSON, even errors.
* Pagination varies: the project-level response list uses `limit`/`offset`; the
  per-survey response list uses cursor (`cursor`, `limit`, `nextCursor`).
* Timestamps are ISO 8601 in UTC.
* Primary keys are UUIDv4. Projects and surveys also accept their `short_id` in
  URL path params. API keys are `proj_<32 hex>`.
* All routes 404 on cross-org access — no 403, so org existence
  isn't leaked.

## Status & uptime

We publish realtime status at [status.insito.app](https://status.insito.app)
and incidents at
[status.insito.app/incidents](https://status.insito.app/incidents). Webhooks
update both within seconds of an incident open/close.
