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

# Server Client (EdpireClient)

> The typed Node.js API client — zero browser dependencies, native fetch (Node 18+).

`@edpire/sdk/client` exports `EdpireClient`, a typed wrapper over the Edpire REST API. It runs **server-side only** (your API key must never reach the browser), has **zero browser dependencies**, and uses native `fetch`.

It is not a rendering pattern — it's the backend utility used alongside the [Embedded Player](/developer/sdk/embedded-player) and [Custom Flow](/developer/sdk/custom-flow).

## Setup

Create the client **once at module level** and reuse it — don't instantiate it per request.

```typescript theme={null}
import { EdpireClient } from "@edpire/sdk/client"

// module level — one instance, reused across all requests
const client = new EdpireClient({
  apiKey: process.env.EDPIRE_API_KEY!,   // starts with edp_live_
  baseUrl: "https://edpire.com",         // optional, this is the default
})
```

### `EdpireClientOptions`

| Option    | Type           | Required | Description                                                            |
| --------- | -------------- | -------- | ---------------------------------------------------------------------- |
| `apiKey`  | `string`       | **Yes**  | Your Edpire API key (`edp_live_…`). Server-side only.                  |
| `baseUrl` | `string`       | No       | API base URL. Defaults to `https://edpire.com`.                        |
| `fetch`   | `typeof fetch` | No       | Custom fetch — for testing or edge runtimes (e.g. Cloudflare Workers). |

## Method reference

All methods are `async`, hit `/api/v1` on `baseUrl` with `Authorization: Bearer <apiKey>`, and throw [`EdpireError`](#error-handling) on non-2xx.

### Assessments

| Method                    | Returns                                | Notes                                                                                 |
| ------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------- |
| `getAssessments(params?)` | `PaginatedResponse<AssessmentSummary>` | `params`: `status`, `ids` (bulk, max 50), `page`, `limit`. No exercises in summaries. |
| `getAssessment(id)`       | `Assessment`                           | Full content with `exercises[].questions[].content_ast`. Never includes answer keys.  |

```typescript theme={null}
const { items, total } = await client.getAssessments({ status: "published", page: 1, limit: 20 })

// Bulk fetch up to 50 IDs in one call (passing more than 50 throws a 400)
const { items: batch } = await client.getAssessments({ ids: ["id1", "id2", "id3"] })

// Single assessment with exercises + questions
const assessment = await client.getAssessment("assessment-uuid")
```

### Submit & check

| Method                                 | Returns       | Notes                                                                                                        |
| -------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------ |
| `submit(assessmentId, options)`        | `GradeResult` | Records + grades a full attempt. Fires `submission.graded`. Accepts flat `StoredAnswer[]` or nested answers. |
| `checkQuestion(assessmentId, options)` | `CheckResult` | Grades one question. Stateless (no record). Rate-limited per `(session_id, question_id)`.                    |

```typescript theme={null}
// Full submission — answers can be a flat StoredAnswer[] (auto-nested) ...
const result = await client.submit("assessment-uuid", {
  learner_ref: "user-123",
  answers: stored,
})

// ... or the nested format directly:
const result2 = await client.submit("assessment-uuid", {
  learner_ref: "user-123",
  answers: {
    exerciseAnswers: [
      { exerciseId: "ex-1", questionAnswers: [
        { questionId: "q-1", answers: [{ nodeId: "n1", type: "choiceSet", value: ["opt_a"] }] },
      ]},
    ],
  },
})

// Single-question check (Custom Flow)
const check = await client.checkQuestion("assessment-uuid", {
  exercise_id: "ex-1",
  question_id: "q-1",
  answers: [{ nodeId: "n1", type: "choiceSet", value: ["opt_a"] }],
  learner_ref: "user-123",
  session_id: "attempt-uuid",      // generate once per attempt
  include_correct_answers: true,   // reveal correct answers (set server-side)
})
```

<Info>`SubmitOptions` also accepts `allow_draft` (submit against draft assessments for testing) and `metadata` (consumer-side data, not stored by Edpire).</Info>

### Submissions & learner history

| Method                                   | Returns                         | Notes                                                     |
| ---------------------------------------- | ------------------------------- | --------------------------------------------------------- |
| `getSubmission(id)`                      | `Submission`                    | Full submission with per-question results.                |
| `getLearnerResults(learnerRef, params?)` | `PaginatedResponse<Submission>` | All submissions for a learner. `params`: `page`, `limit`. |

```typescript theme={null}
const submission = await client.getSubmission("submission-uuid")
const { items } = await client.getLearnerResults("user-123", { page: 1, limit: 50 })
```

### Collections

| Method                              | Returns                                                | Notes                                                   |
| ----------------------------------- | ------------------------------------------------------ | ------------------------------------------------------- |
| `getCollections(params?)`           | `PaginatedResponse<Collection>`                        | List collections (with `item_count`).                   |
| `getCollection(id)`                 | `CollectionDetail`                                     | Includes ordered `items[].assessment`.                  |
| `getCollectionResults(id, params?)` | `PaginatedResponse<Submission & { assessment_title }>` | Flat results across every assessment in the collection. |

```typescript theme={null}
const { items: collections } = await client.getCollections()
const detail = await client.getCollection("collection-uuid")   // detail.items[].assessment
const { items: results } = await client.getCollectionResults("collection-uuid")
```

### Embed tokens

| Method                                     | Returns      | Notes                                                                                    |
| ------------------------------------------ | ------------ | ---------------------------------------------------------------------------------------- |
| `mintEmbedToken(assessmentId, learnerRef)` | `EmbedToken` | Single-use token (expires 1h) for the [Embedded Player](/developer/sdk/embedded-player). |

```typescript theme={null}
const { token, expires_at } = await client.mintEmbedToken("assessment-uuid", "user-123")
```

### Webhooks

| Method                         | Returns             | Notes                                                                 |
| ------------------------------ | ------------------- | --------------------------------------------------------------------- |
| `registerWebhook(url, events)` | `WebhookWithSecret` | **Secret shown once** — store it. `http://localhost` allowed for dev. |
| `listWebhooks()`               | `Webhook[]`         | Secrets not included.                                                 |
| `deleteWebhook(id)`            | `void`              | —                                                                     |

```typescript theme={null}
const webhook = await client.registerWebhook(
  "https://yourplatform.com/webhooks/edpire",
  ["submission.graded", "assessment.published", "assessment.content_updated"],
)
console.log(webhook.secret)   // store securely — shown once
```

See [Webhooks](/developer/webhooks) for the full event list and signature verification.

## Error handling

Every method throws `EdpireError` on a non-2xx response:

```typescript theme={null}
import { EdpireError } from "@edpire/sdk/client"

try {
  await client.getAssessment("bad-id")
} catch (err) {
  if (err instanceof EdpireError) {
    console.log(err.status)   // e.g. 404
    console.log(err.message)  // e.g. "Assessment not found"
  }
}
```

Common status codes: `400` bad request · `401` invalid API key · `403` forbidden / origin not allowed · `404` not found · `409` max attempts reached · `422` grading failed · `429` rate limit exceeded.

## Custom fetch (testing / edge runtimes)

```typescript theme={null}
const client = new EdpireClient({
  apiKey: "test-key",
  baseUrl: "http://localhost:3001",
  fetch: customFetch,   // injectable for tests or Cloudflare Workers
})
```

## Type reference

All response and option types (`Assessment`, `GradeResult`, `CheckResult`, `Submission`, `Collection`, etc.) are exported from `@edpire/sdk/client`. See the [type catalog](/developer/sdk/types#server-client-types).
