docs: expand documentation with new sections on importing contacts, unsubscribe pages, and API key management

This commit is contained in:
Dries Augustyns
2026-05-06 21:37:00 +02:00
parent 88be252a29
commit 4ddafdc041
27 changed files with 1943 additions and 483 deletions
+44 -36
View File
@@ -27,12 +27,18 @@ Invalid request format or parameters. Check the error details for specific issue
### 401 Unauthorized
Authentication failed or missing. Verify your API key.
### 402 Payment Required
A billing limit has been reached or the operation requires a plan upgrade. Returned with `BILLING_LIMIT_EXCEEDED` and `UPGRADE_REQUIRED`.
### 403 Forbidden
Not authorized to access this resource. Check permissions or project status.
Not authorized to access this resource. Check permissions or project status. Also returned when email verification is required (`EMAIL_VERIFICATION_REQUIRED`) or the project has been disabled (`PROJECT_DISABLED`).
### 404 Not Found
The requested resource does not exist. Verify the resource ID.
### 409 Conflict
The request conflicts with the current state of a resource — for example, creating a contact with an email that already exists, or updating to an email that's already taken. Returned with `CONFLICT`.
### 422 Unprocessable Entity
Request validation failed. Check the `errors` array for field-level details.
@@ -96,6 +102,7 @@ All errors include a machine-readable `code` field for programmatic handling. He
| `FORBIDDEN` | 403 | Not allowed to perform this action |
| `PROJECT_ACCESS_DENIED` | 403 | No access to this project |
| `PROJECT_DISABLED` | 403 | Project has been disabled |
| `EMAIL_VERIFICATION_REQUIRED` | 403 | The user's email address must be verified before this action |
### Validation & Input Errors
@@ -442,55 +449,56 @@ if (!data.success) {
### Common Issues and Solutions
#### Authentication Issues (401)
import {Accordion, Accordions} from 'fumadocs-ui/components/accordion';
**Problem**: `INVALID_API_KEY` or `MISSING_AUTH`
<Accordions type="single">
**Solutions**:
- Verify your API key is copied correctly (no extra spaces)
- Check you're using the right key type (`sk_` for secret, `pk_` for public)
- Ensure the `Authorization` header uses Bearer token format
- Verify the key hasn't been revoked or regenerated
<Accordion title="Authentication Issues (401) — INVALID_API_KEY or MISSING_AUTH">
#### Validation Issues (422)
- Verify your API key is copied correctly (no extra spaces).
- Check you're using the right key type (`sk_` for secret, `pk_` for public).
- Ensure the `Authorization` header uses Bearer token format.
- Verify the key hasn't been revoked or regenerated.
**Problem**: `VALIDATION_ERROR` with field errors
</Accordion>
**Solutions**:
- Check the `errors` array for specific field issues
- Verify all required fields are included
- Ensure field types match (strings quoted, numbers unquoted)
- Review the API reference for correct request format
<Accordion title="Validation Issues (422) — VALIDATION_ERROR">
#### Not Found Issues (404)
- Check the `errors` array for specific field issues.
- Verify all required fields are included.
- Ensure field types match (strings quoted, numbers unquoted).
- Review the API reference for the correct request format.
**Problem**: `TEMPLATE_NOT_FOUND`, `CONTACT_NOT_FOUND`, etc.
</Accordion>
**Solutions**:
- Verify the resource ID is correct
- Check the resource belongs to your project
- Ensure the resource hasn't been deleted
- List available resources via the API to confirm IDs
<Accordion title="Not Found Issues (404) — TEMPLATE_NOT_FOUND, CONTACT_NOT_FOUND, etc.">
#### Rate Limit Issues (429)
- Verify the resource ID is correct.
- Check the resource belongs to your project.
- Ensure the resource hasn't been deleted.
- List available resources via the API to confirm IDs.
**Problem**: `RATE_LIMIT_EXCEEDED`
</Accordion>
**Solutions**:
- Implement exponential backoff (wait 1s, 2s, 4s, 8s between retries)
- Reduce request frequency
- Consider upgrading your plan for higher limits
- Batch operations when possible
<Accordion title="Rate Limit Issues (429) — RATE_LIMIT_EXCEEDED">
#### Server Errors (500)
- Implement exponential backoff (wait 1s, 2s, 4s, 8s between retries).
- Reduce request frequency.
- Consider upgrading your plan for higher limits.
- Batch operations when possible.
**Problem**: `INTERNAL_SERVER_ERROR`
</Accordion>
**Solutions**:
- Note the request ID from the error response
- Wait a moment and retry the request
- Check [status.useplunk.com](https://status.useplunk.com) for incidents
- Contact support with the request ID if the issue persists
<Accordion title="Server Errors (500) — INTERNAL_SERVER_ERROR">
- Note the request ID from the error response.
- Wait a moment and retry the request.
- Check [status.useplunk.com](https://status.useplunk.com) for incidents.
- Contact support with the request ID if the issue persists.
</Accordion>
</Accordions>
### Best Practices
+193 -74
View File
@@ -67,11 +67,9 @@ curl -X POST {{API_URL}}/contacts \
## Response format
All API responses follow a standardized format for easy parsing and error handling.
### Success responses
### Success response
Public API endpoints (`/v1/send`, `/v1/track`):
Public API endpoints (`/v1/send`, `/v1/track`, `/v1/verify`) return a wrapped envelope:
```json
{
@@ -84,30 +82,24 @@ Public API endpoints (`/v1/send`, `/v1/track`):
}
```
Dashboard API endpoints (contacts, templates, campaigns):
Dashboard endpoints (contacts, templates, campaigns, segments, workflows, etc.) return the resource directly — no `success`/`data` wrapper:
```json
{
"success": true,
"data": {
"id": "cnt_abc123",
"email": "user@example.com",
"createdAt": "2025-11-30T10:30:00.000Z"
}
"id": "cnt_abc123",
"email": "user@example.com",
"createdAt": "2025-11-30T10:30:00.000Z"
}
```
List endpoints with pagination:
List endpoints with cursor pagination return:
```json
{
"success": true,
"data": {
"items": [...],
"nextCursor": "abc123",
"hasMore": true,
"total": 1000
}
"data": [ /* items */ ],
"cursor": "def456",
"hasMore": true,
"total": 10000
}
```
@@ -148,35 +140,37 @@ See the [Error Codes documentation](/api-reference/errors) for complete details
## Pagination
List endpoints support cursor-based pagination:
Most list endpoints use **cursor-based** pagination:
```bash
GET /contacts?limit=100&cursor=abc123
```
**Parameters:**
- `limit` — Number of items per page (default: 20, max: 100)
- `cursor` — Pagination cursor from previous response
- `limit` — items per page (default: 20, max: 100)
- `cursor` — pagination cursor from the previous response's `cursor` field
**Response:**
```json
{
"items": [...],
"nextCursor": "def456",
"data": [ /* items */ ],
"cursor": "def456",
"hasMore": true,
"total": 10000
}
```
Use `nextCursor` for the next page. When `hasMore` is false, you've reached the end.
Pass the response's `cursor` value as the next request's `cursor` query parameter. When `hasMore` is `false`, you've reached the end. The `total` count is only included on the first page (when no `cursor` is supplied) — subsequent pages return `total: 0` to keep listing fast.
A few endpoints (e.g. `GET /segments/:id/contacts`) use **page-based** pagination instead, with `page` and `pageSize` parameters. Their responses include `total`, `page`, and `pageSize` fields.
## Rate limits
Plunk enforces reasonable rate limits to ensure service quality:
- **Email sending** — 14 emails/second (AWS SES default)
- **API requests** — 1000 requests/minute per project
- **Bulk operations** — Automatically queued for processing
- **Email sending** — throttled per project to protect deliverability.
- **API requests** — 1000 requests/minute per project.
- **Bulk operations** — automatically queued for asynchronous processing.
If you exceed limits, you'll receive a `429 Too Many Requests` response.
@@ -202,75 +196,200 @@ For a complete list of error codes and troubleshooting guidance, see the [Error
## API endpoints
### Public API (transactional)
A complete, grouped reference of every endpoint exposed by the Plunk API.
**POST /v1/send** — Send transactional email(s)
- Accepts single or multiple recipients
- Template or inline content
- Variable substitution
### Public API
**POST /v1/track** — Track event for contact
- Creates/updates contact
- Tracks custom event
- Can use public key
The `/v1/*` endpoints are designed for use from your applications and accept simple, denormalised payloads. `POST /v1/track` is the only endpoint callable with a public (`pk_*`) key.
| Method | Path | Description | Key |
| ------ | ------------ | -------------------------------------------------------------------------------------------- | -------- |
| POST | `/v1/send` | Send a transactional email. Single or multiple recipients, template or inline content, attachments, headers, custom data. | `sk_*` |
| POST | `/v1/track` | Track an event for a contact. Auto-creates or upserts the contact. Triggers workflows. | `pk_*` or `sk_*` |
| POST | `/v1/verify` | Validate an email address — format, MX records, disposable domains, typo detection. | `sk_*` |
### Contacts
**GET /contacts** — List all contacts
**POST /contacts** — Create new contact
**GET /contacts/:id** — Get contact details
**PATCH /contacts/:id** — Update contact
**DELETE /contacts/:id** — Delete contact
| Method | Path | Description |
| ------ | ------------------------------------- | -------------------------------------------------------------------------------------------- |
| GET | `/contacts` | List contacts. Supports `search` (by email substring), `limit`, `cursor`. |
| POST | `/contacts` | Create or upsert a contact by email. Returns `_meta.isNew` and `_meta.isUpdate`. |
| GET | `/contacts/:id` | Get a single contact. |
| PATCH | `/contacts/:id` | Update a contact's email, subscription state, or `data` fields. |
| DELETE | `/contacts/:id` | Delete a contact. |
| POST | `/contacts/lookup` | Bulk email-existence check (max 500 emails per call). |
| **Custom fields** | | |
| GET | `/contacts/fields` | List standard and custom fields with inferred types and coverage percentages. |
| GET | `/contacts/fields/:field/values` | Distinct values for a custom field — used by segment / workflow filter UIs. |
| GET | `/contacts/fields/:field/usage` | Where a custom field is referenced (segments, campaigns, workflows). |
| DELETE | `/contacts/fields/:field` | Delete a custom field across every contact in the project. |
| **CSV import** | | |
| POST | `/contacts/import` | Upload a CSV file (multipart, ≤ 5 MB). Queued — returns a `jobId`. |
| GET | `/contacts/import/:jobId` | Poll the status of a CSV import job. |
| **Bulk operations** | | |
| POST | `/contacts/bulk-subscribe` | Subscribe up to 1,000 contacts by ID. Queued — returns a `jobId`. |
| POST | `/contacts/bulk-unsubscribe` | Unsubscribe up to 1,000 contacts by ID. Queued. |
| POST | `/contacts/bulk-delete` | Delete up to 1,000 contacts by ID. Queued. |
| GET | `/contacts/bulk/:jobId` | Poll the status of a bulk job. |
### Templates
**GET /templates** — List all templates
**POST /templates** — Create new template
**GET /templates/:id** — Get template details
**PATCH /templates/:id** — Update template
**DELETE /templates/:id** — Delete template
| Method | Path | Description |
| ------ | ----------------------------- | -------------------------------------------------------------------------------------------- |
| GET | `/templates` | List all templates. |
| POST | `/templates` | Create a template. `from` must be on a verified domain. |
| GET | `/templates/:id` | Get a template. |
| PATCH | `/templates/:id` | Update a template. |
| DELETE | `/templates/:id` | Delete a template. |
| POST | `/templates/:id/duplicate` | Duplicate a template — returns the new template ID. |
| GET | `/templates/:id/usage` | List campaigns and workflow steps that reference this template. |
### Campaigns
**GET /campaigns** — List all campaigns
**POST /campaigns** — Create new campaign
**GET /campaigns/:id** — Get campaign details
**PATCH /campaigns/:id** — Update campaign
**POST /campaigns/:id/send** — Send or schedule campaign
**POST /campaigns/:id/cancel** — Cancel scheduled campaign
**POST /campaigns/:id/test** — Send test email
**GET /campaigns/:id/stats**Get campaign analytics
| Method | Path | Description |
| ------ | ----------------------------- | -------------------------------------------------------------------------------------------- |
| GET | `/campaigns` | List all campaigns. |
| POST | `/campaigns` | Create a campaign in `DRAFT`. `from` must be on a verified domain. |
| GET | `/campaigns/:id` | Get a campaign. |
| PUT | `/campaigns/:id` | Update a campaign (replace). |
| DELETE | `/campaigns/:id` | Delete a campaign. Returns 409 if it has active executions. |
| POST | `/campaigns/:id/duplicate` | Duplicate a campaignreturns the new campaign in `DRAFT`. |
| POST | `/campaigns/:id/send` | Send (or schedule) the campaign. Pass `scheduledFor` for delayed sends. |
| POST | `/campaigns/:id/cancel` | Cancel a `SCHEDULED` or `SENDING` campaign. |
| POST | `/campaigns/:id/test` | Send a test email to a single address (`{ email: "you@example.com" }`). |
| GET | `/campaigns/:id/stats` | Get current send / open / click / bounce counts. |
### Segments
**GET /segments** — List all segments
**POST /segments** — Create new segment (Dynamic or Static)
**GET /segments/:id** — Get segment details
**PATCH /segments/:id** — Update segment
**DELETE /segments/:id** — Delete segment
**GET /segments/:id/contacts** — List segment members
**POST /segments/:id/members** — Add contacts to a static segment (by email)
**DELETE /segments/:id/members** — Remove contacts from a static segment (by email)
| Method | Path | Description |
| ------ | ----------------------------- | -------------------------------------------------------------------------------------------- |
| GET | `/segments` | List all segments (no pagination — small list). |
| POST | `/segments` | Create a segment. `type: "DYNAMIC"` requires `condition`; `type: "STATIC"` rejects it. |
| GET | `/segments/:id` | Get a segment, including cached `memberCount`. |
| PATCH | `/segments/:id` | Update name, description, condition (dynamic only), or `trackMembership`. |
| DELETE | `/segments/:id` | Delete a segment. Returns 409 if used by an active campaign. |
| GET | `/segments/:id/contacts` | Page-based list of segment members. `page`, `pageSize` (max 100). Live for dynamic segments. |
| POST | `/segments/:id/members` | Add emails to a **static** segment. Body: `{ emails, createMissing?, subscribed? }`. |
| DELETE | `/segments/:id/members` | Remove emails from a **static** segment. Body: `{ emails }`. |
| POST | `/segments/:id/compute` | Recompute membership for a tracked dynamic segment — fires entry/exit events. |
| POST | `/segments/:id/refresh` | Cheap count refresh — no events, no membership writes. |
### Workflows
**GET /workflows** — List all workflows
**POST /workflows** — Create new workflow
**GET /workflows/:id** — Get workflow details
**PATCH /workflows/:id** — Update workflow
**DELETE /workflows/:id** — Delete workflow
**GET /workflows/:id/executions** — List workflow executions
Each workflow consists of a **workflow** record, a graph of **steps**, **transitions** between them, and per-contact **executions**. The API mirrors that structure.
| Method | Path | Description |
| ------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| GET | `/workflows` | List all workflows. |
| GET | `/workflows/fields` | Fields available to use in `CONDITION` step filters (contact + event fields). |
| POST | `/workflows` | Create a workflow. Always starts with `triggerType: EVENT` and `enabled: false`. |
| GET | `/workflows/:id` | Get a workflow with its steps and transitions. |
| PATCH | `/workflows/:id` | Update workflow metadata, trigger type / config, `enabled`, `allowReentry`. |
| DELETE | `/workflows/:id` | Delete a workflow. Active executions must be cancelled or completed first. |
| **Steps** | | |
| POST | `/workflows/:id/steps` | Add a step (`SEND_EMAIL`, `DELAY`, `WAIT_FOR_EVENT`, `CONDITION`, `WEBHOOK`, `UPDATE_CONTACT`, `EXIT`). |
| PATCH | `/workflows/:id/steps/:stepId` | Update a step's config. |
| DELETE | `/workflows/:id/steps/:stepId?splice=true` | Delete a step. Pass `splice=true` to auto-reconnect surrounding transitions. |
| **Transitions** | | |
| POST | `/workflows/:id/transitions` | Add a transition between two steps. For `CONDITION` steps, include `branch: "yes" \| "no"`. |
| DELETE | `/workflows/:id/transitions/:transitionId` | Delete a transition. |
| **Executions** | | |
| POST | `/workflows/:id/executions` | Manually start an execution for a contact. Optional `context` JSON for per-execution variables. |
| GET | `/workflows/:id/executions` | List executions, filterable by `status`. |
| GET | `/workflows/:id/executions/:executionId` | Get a single execution. |
| DELETE | `/workflows/:id/executions/:executionId` | Cancel a running or waiting execution. |
| POST | `/workflows/:id/executions/cancel-all` | Cancel every active execution at once. |
### Events
**GET /events** — List all events
**GET /events/names** — List unique event names
| Method | Path | Description |
| ------ | ----------------------------- | -------------------------------------------------------------------------------------------- |
| POST | `/events/track` | Internal alias for `/v1/track`, for dashboard use. Use `/v1/track` from your apps. |
| GET | `/events` | List recent tracked events for a project. |
| GET | `/events/stats` | Aggregated event statistics. |
| GET | `/events/contact/:contactId` | All events for a single contact. |
| GET | `/events/names` | All distinct event names tracked in this project. |
| GET | `/events/:eventName/usage` | Where an event name is referenced (segment filters, workflow triggers, conditions). |
| DELETE | `/events/:eventName` | Delete every event with the given name from the project. |
### Domains
**GET /domains** — List verified domains
**POST /domains** — Add domain for verification
**DELETE /domains/:id** — Remove domain
| Method | Path | Description |
| ------ | ----------------------------- | -------------------------------------------------------------------------------------------- |
| GET | `/domains/project/:projectId` | List domains configured for a project (verified and pending). |
| POST | `/domains` | Add a domain for verification. Returns the DNS records you need to add. |
| GET | `/domains/:id/verify` | Force a verification check now (otherwise checked every 5 minutes in the background). |
| DELETE | `/domains/:id` | Remove a domain. |
### Activity & analytics
| Method | Path | Description |
| ------ | ----------------------------- | -------------------------------------------------------------------------------------------- |
| GET | `/activity` | Cross-resource activity feed (sends, opens, clicks, bounces, complaints, inbound, etc.). |
| GET | `/activity/stats` | Aggregated counts for dashboard charts. |
| GET | `/activity/recent-count` | Recent event count for the dashboard's "live" indicator. |
| GET | `/activity/types` | Distinct activity types in the project. |
| GET | `/activity/upcoming` | Upcoming scheduled sends and active executions. |
| GET | `/analytics/timeseries` | Email send / open / click time-series. |
| GET | `/analytics/top-campaigns` | Top-performing campaigns by metric. |
| GET | `/analytics/campaign-stats` | Campaign-level breakdown. |
| GET | `/analytics/top-events` | Most frequent custom event names. |
### Uploads
| Method | Path | Description |
| ------ | ---------------- | ---------------------------------------------------------------------------------------------------- |
| POST | `/uploads/image` | Upload an image (multipart) for use inside template bodies. Returns a public URL. |
### Authentication & user management
The dashboard authenticates with JWT cookies; these endpoints mostly aren't useful from server-to-server integrations but are documented here for completeness.
| Method | Path | Description |
| ------ | ---------------------------------------------------------- | ------------------------------------------------------------ |
| POST | `/auth/login` | Email + password login. Sets a JWT cookie. |
| POST | `/auth/signup` | Sign up a new user (subject to `DISABLE_SIGNUPS`). |
| GET | `/auth/logout` | Clear the auth cookie. |
| GET | `/auth/oauth-config` | Which OAuth providers are configured. |
| POST | `/auth/verify-email` | Verify an email with a token from an email link. |
| POST | `/auth/request-verification` | Resend the verification email. |
| POST | `/auth/request-password-reset` | Send a password reset email. |
| POST | `/auth/reset-password` | Reset a password with a token. |
| GET | `/users/@me` | Get the current user. |
| GET | `/users/@me/projects` | List the user's projects. |
| POST | `/users/@me/projects` | Create a project. |
| PATCH | `/users/@me/projects/:id` | Update project settings. |
| POST | `/users/@me/projects/:id/regenerate-keys` | Rotate both API keys. |
| POST | `/users/@me/projects/:id/checkout` | Create a Stripe Checkout session. |
| POST | `/users/@me/projects/:id/billing-portal` | Open the Stripe billing portal. |
| GET | `/users/@me/projects/:id/billing-limits` | Read per-category billing caps. |
| PUT | `/users/@me/projects/:id/billing-limits` | Update per-category billing caps. |
| GET | `/users/@me/projects/:id/billing-consumption` | Current period consumption for the project. |
| GET | `/users/@me/projects/:id/billing-invoices` | Stripe invoices for the project. |
| GET | `/users/@me/projects/:id/security` | Security info — bounce/complaint rates, recent suspensions. |
| POST | `/users/@me/projects/:id/reset` | Wipe project data (irreversible). |
| DELETE | `/users/@me/projects/:id` | Delete the project entirely. |
| GET | `/projects/:id/setup-state` | Onboarding setup state. |
| GET | `/projects/:id/security` | Security state for a single project. |
| GET | `/projects/:id/members` | Project team members. |
| POST | `/projects/:id/members` | Invite a team member. |
| PATCH | `/projects/:id/members/:userId` | Change a member's role. |
| DELETE | `/projects/:id/members/:userId` | Remove a member. |
### Configuration
| Method | Path | Description |
| ------ | ---------- | -------------------------------------------------------------------------------------------------------- |
| GET | `/config` | Public, no-auth feature flags — which integrations are enabled (OAuth providers, billing, S3, SMTP, …). |
### Internal webhook endpoints
These endpoints receive events from the underlying email and billing infrastructure. You don't call them from your applications — they're listed for completeness for self-hosters.
| Method | Path |
| ------ | ----------------------------- |
| POST | `/webhooks/sns` |
| POST | `/webhooks/incoming/stripe` |
## Client libraries