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

# Server Data API

> Trusted, server-side access to your own submissions, analytics, and files.

A separate, secret-key-authenticated surface for your **backend** to read data it owns — not for accepting submissions (that's [Submit a Form](/api-reference/submit-form)) and never called from a browser. The workspace is derived entirely from the key; you never pass a workspace ID.

```bash theme={null}
npm install @raykoi/sdk
```

```javascript theme={null}
import { createAdminClient } from '@raykoi/sdk/admin';

const admin = createAdminClient({ apiKey: process.env.RAYKOI_SECRET_KEY });
```

<Warning>
  Requires the Starter plan or above, and never runs in a browser — `createAdminClient` warns loudly to the console if it detects a `window` global. This is a deliberately separate entry point from the main SDK (`@raykoi/sdk/admin`, not `@raykoi/sdk`) specifically so a bundler can't accidentally pull it into browser/embed code.
</Warning>

## Authentication & scopes

`Authorization: Bearer rk_live_...`, same key format as everywhere else. Every route additionally requires a scope on that specific key:

| Scope   | Grants                                                      |
| ------- | ----------------------------------------------------------- |
| `read`  | List/get forms, submissions, analytics, file download URLs. |
| `admin` | Everything `read` grants, plus deleting submissions.        |

A key created with only `submit` scope (the default for a key meant to authenticate submissions) is rejected by every Server Data API route — read access has to be explicitly granted when you create the key.

## Endpoints

### List forms

```javascript theme={null}
const { forms, pagination } = await admin.forms.list({ limit: 50 });
```

`GET /api/v1/server/forms` — supports `page`, `limit`, `search`, `sortBy`.

### Get a form

```javascript theme={null}
const form = await admin.forms.get('YOUR_PUBLIC_ID');
```

`GET /api/v1/server/forms/:public_id`

### List submissions

```javascript theme={null}
const { submissions, pagination } = await admin.submissions.list('YOUR_PUBLIC_ID', {
  limit: 50,
  status: 'completed', // or 'spam' | 'suspicious'
});
```

`GET /api/v1/server/forms/:public_id/submissions` — supports `page`, `limit`, `status`, `search`, `range` (`all` | `7d` | `30d` | `90d`).

<ResponseField name="fields" type="object">
  Submitted field values, keyed the same way they were submitted.
</ResponseField>

<ResponseField name="status" type="string">
  `completed`, `spam`, or `suspicious`, decided server-side by Raykoi's spam-scoring pipeline (honeypot, CAPTCHA, rate limiting, and repeated-failure signals).
</ResponseField>

Internal database primary keys and idempotency bookkeeping are never exposed here — `id` is always the submission's public ID.

### Get a submission

```javascript theme={null}
const submission = await admin.submissions.get('YOUR_PUBLIC_ID', submissionId);
```

`GET /api/v1/server/forms/:public_id/submissions/:submission_id`

### Delete submissions

```javascript theme={null}
await admin.submissions.delete('YOUR_PUBLIC_ID', [submissionId1, submissionId2]);
```

`DELETE /api/v1/server/forms/:public_id/submissions` — requires `admin` scope. Up to 500 IDs per call.

### Get analytics

```javascript theme={null}
const stats = await admin.analytics.get('YOUR_PUBLIC_ID');
```

`GET /api/v1/server/forms/:public_id/analytics` — views, starts, submissions, and completion rate, honoring your plan's analytics retention window.

### Get a file's download URL

```javascript theme={null}
const url = await admin.files.getDownloadUrl('YOUR_PUBLIC_ID', filePath);
```

`GET /api/v1/server/forms/:public_id/files/download` — returns a short-lived signed URL, same model as the upload side. See [File Uploads](/essentials/file-uploads).

## Rate limiting

Keyed per workspace (not per IP) — several backends legitimately calling from behind the same office IP, or one customer's infrastructure calling from multiple regions, doesn't cause them to compete for the same limit.
