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

# Retently plugin

> NPS, CSAT and CES surveys with Retently - list and upsert customers, send transactional surveys, import historical feedback, read campaign reports, and manage tags, topics and suppressions.

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

Create a key in Retently under Settings, API. The key decides what the plugin can do:

* **Read permission** allows GET only. **Write permission** adds POST and DELETE. A read-only key on a write route returns a `403` that the plugin reports as `isPermissionError`, which is a different problem from a `401` (a wrong or deleted key) and needs a new key rather than a retry.

Things to know before you send:

* **`survey.send` sends real email.** It queues a survey through a *transactional* campaign and returns only `status: 'queued'`; there is no survey ID to cancel. A non-transactional campaign is a `400`, an inactive one a `403`, and an unknown one a `404`. `delay` overrides the campaign's own scheduling, so omit it to respect the campaign's sending time frame.
* **`feedback.import` changes your scores.** Imported responses count toward NPS and trends, bucketed by `response_date`, so set it on a historical import or every response lands on the import day. They do not consume send quota and never fire webhooks. Only 200 responses go in one call, and a bad item fails alone: read `details.failed` for the reason instead of treating a `200` as all-or-nothing.
* **The main comment and additional questions collide.** A top-level `comment` fills the template's `isMain` question, so also answering that question in `additional_questions` fails the item as ambiguous. Get question UUIDs from `templates.get`.
* **RATING answers need their metric.** An `additional_questions` entry of type `RATING` is scored on a metric (`nps`, `csat`, `ces`, `star`, `thumbs`, `xsat` or `pmf`); the plugin rejects one that omits it before the request is sent.
* **Dates are not uniform.** `customers.list`, `feedback.list` and `outbox.list` accept `YYYY-MM-DD`, an ISO 8601 date-time, or **UNIX seconds** - milliseconds silently resolve to a far-future date. `reports.get` is the exception: it takes `YYYY-MM-DD` only and rejects a full timestamp. `feedback.import` dates are ISO 8601 only.
* **Paging stops at 1,000.** `limit` is capped there, and `data.pages` tells you how many pages exist, so walk them rather than asking for everything at once.
* **Unsubscribing and resubscribing are not symmetric.** `customers.resubscribe` is idempotent and reports a status per contact: only `resubscribed` means a change, `skipped` means Retently refused for compliance (self-unsubscribed or hard-bounced), and `suppressed: true` warns that an account-level suppression still blocks delivery.
* **Rate limit.** 150 requests a minute. No `Retry-After` is documented, so the plugin backs off exponentially on a `429`.

**What you get:**

* 18 typed API operations

## Setup

**1. Install**

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

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

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

```bash bun theme={null}
bun add corsair @corsair-dev/retently 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 { retently } from '@corsair-dev/retently';

export const corsair = createCorsair({
	plugins: [
		retently(),
	],
	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}
> retently()
> ```
>
> 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: 'retently',
	tenantId: 'acme',
});
// redirect the user's browser to connectUrl
```

## Example API calls

**Read the latest NPS responses**

```ts theme={null}
const tenant = corsair.withTenant('acme');
await tenant.retently.api.feedback.list({ limit: 25, sort: '-createdDate' });
```

**Send a transactional survey**

```ts theme={null}
const tenant = corsair.withTenant('acme');
await tenant.retently.api.survey.send({
	campaign: '5a9d595701c85b37224ab2d0',
	subscribers: [{ email: 'john.smith@example.com', first_name: 'John', company: 'ACME' }],
});
```

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

## What's next

> [card] [API reference](https://docs.corsair.dev/plugins/retently/api.md)
>
> Every `retently.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.