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

# MemberSpot plugin

> Membership and course platform access with MemberSpot - read users, offers, courses, exams and course progress, and grant or revoke offers, course access and custom properties.

Use **MemberSpot** through Corsair: one client, typed API calls.

Create the key in your school under Settings, API keys ([https://app.memberspot.de/settings/apikeys](https://app.memberspot.de/settings/apikeys)). It is per school, so one key covers every member.

Things to know before you call:

* **The API is documented as OpenAPI 3.0.** The readable reference is [https://api.memberspot.de/api](https://api.memberspot.de/api) and the machine-readable spec is [https://api.memberspot.de/api-json](https://api.memberspot.de/api-json), so the plugin's schemas were built from the spec rather than from prose.
* **Auth is an `X-API-KEY` header.** A missing key answers 400, not 401; only an invalid key answers 401. The plugin treats both as auth failures and never retries them.
* **`auth.check` returns `schoolId`, not `id`.** The spec documents that route with no response body, so its shape was taken from the live API. It is the cheapest way to confirm a key works.
* **`name` means last name.** MemberSpot splits a member into `firstname` and `name`, and this plugin keeps those names exactly. `created` and the other dates are ISO date-times.
* **Paging uses two different names.** A page request takes `lastLoadedId` and the response returns `nextPage`; the course-progress list calls the inbound cursor `lastLoadedCourseId`. Pass `nextPage` straight back as `lastLoadedId`.
* **Many operations come in email and uid pairs.** Most writes that target a member exist twice, once keyed by email and once by uid; prefer the uid form when you already have the uid. `users.setOfferExpires`, `users.setCourseStartDate`, `users.loginToken` and `users.offers` take a uid only, so look the uid up with `users.findByEmail` first.
* **Deletes are capped at 10 members per call.** The schema enforces the cap locally, because the API rejects an 11th rather than batching it.
* **Members are deleted, not deactivated.** `users.delete` and `users.deleteByUid` remove the account and its progress. To keep the record, revoke offers instead. `users.setOfferState` with `active: false` revokes one offer, and the member keeps any other offer. To remove all access, list the grants with `users.offers` and revoke each one.
* **Grant-state writes need `active`.** MemberSpot deletes the grant when `active` is missing, so the offer-state and order-state writes require it. Pass `true` to grant, `false` to revoke, or `null` to delete the grant. `users.setOrderStateByOrderId` changes every member holding that order id.
* **`users.loginToken` mints a credential.** It returns a signed token and URL that sign in as the member, valid for an hour. It is marked as a write, so a read-only caller cannot create one, but treat the result as a secret and never log it.
* **A member that does not exist is not `null`.** `users.findByEmail` answers 400 `USER_NOT_FOUND`, and `users.findByUid` answers 200 with an empty body that fails the output schema. Catch the error rather than testing for a null result.
* **Custom properties are keyed by id.** `users.setCustomProperties` takes a map of property id (the `id` from `customProperties.list`) to value and sends it as the `[{ id, value }]` array the API expects.
* **Chapter access needs the course.** `chapters.enableAccess` takes the `courseId` as well as the `chapterId`.
* **Rate limits.** Several write routes document a 429 with no `Retry-After`. The plugin retries twice, after the header when present and after one minute otherwise. Every other 4xx is a rejected request and is not retried.
* **No webhooks.** MemberSpot sends none, so the plugin declares no webhook surface.

**What you get:**

* 32 typed API operations

## Setup

**1. Install**

```bash npm theme={null}
npm install corsair @corsair-dev/memberspot better-sqlite3
npm install --save-dev @types/better-sqlite3
```

```bash yarn theme={null}
yarn add corsair @corsair-dev/memberspot better-sqlite3
yarn add --dev @types/better-sqlite3
```

```bash pnpm theme={null}
pnpm install corsair @corsair-dev/memberspot better-sqlite3
pnpm install --save-dev @types/better-sqlite3
```

```bash bun theme={null}
bun add corsair @corsair-dev/memberspot better-sqlite3
bun add --dev @types/better-sqlite3
```

**2. Add the plugin**

```ts corsair.ts theme={null}
import Database from 'better-sqlite3';
import { createCorsair } from 'corsair';
import { memberspot } from '@corsair-dev/memberspot';

export const corsair = createCorsair({
	plugins: [
		memberspot(),
	],
	database: new Database('corsair.db'),
	kek: process.env.CORSAIR_KEK!,
	multiTenancy: true,
	hub: {
		projectApiKey: process.env.CORSAIR_API_KEY!,
		signingSecret: process.env.CORSAIR_SIGNING_SECRET!,
	},
});
```

With `multiTenancy: true`, scope calls with `corsair.withTenant(id)`. See [Quick start](https://docs.corsair.dev/quick-start.md) for KEK + Hub keys, and [Multi-tenancy](https://docs.corsair.dev/concepts/multi-tenancy.md) for account isolation.

**3. Choose authentication**

> [tab] API Key (Recommended)
>
> No setup required yet. When you make your first request as a tenant, Corsair prompts for the API key.
>
> ```ts theme={null}
> memberspot()
> ```
>
> More: [API Key](https://docs.corsair.dev/concepts/api-key)

**4. Connect a tenant**

Mint a connect link and send the tenant to it. Hub hosts the page and delivers the result to your app — see [Connect / OAuth](https://docs.corsair.dev/management/connect).

```ts theme={null}
const { connectUrl } = await corsair.manage.connect.createLink({
	plugin: 'memberspot',
	tenantId: 'acme',
});
// redirect the user's browser to connectUrl
```

## Example API calls

**List a page of members**

```ts theme={null}
const tenant = corsair.withTenant('acme');
await tenant.memberspot.api.users.list({ pageLength: 25 });
```

**Revoke an offer without deleting the member**

```ts theme={null}
const tenant = corsair.withTenant('acme');
await tenant.memberspot.api.users.setOfferState({ email: 'jordan@acme.example', offerId: 'offer-1', active: false });
```

See the full list on the [API](https://docs.corsair.dev/plugins/memberspot/api.md) page.

## What's next

> [card] [API reference](https://docs.corsair.dev/plugins/memberspot/api.md)
>
> Every `memberspot.api.*` operation with input and output types.

> [card] [Connect / OAuth](https://docs.corsair.dev/management/connect)
>
> createLink, Hub delivery, and tenant connect flows.

> [card] [Use with agents](https://docs.corsair.dev/mcp-adapters/mcp-adapters)
>
> Expose this plugin's operations as MCP tools.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.