---
title: API Reference
description: Complete Plunk API documentation
---

## Base URL

```
{{API_URL}}
```

All API requests use this base URL.

## Authentication

Include your API key in the `Authorization` header:

```bash
Authorization: Bearer YOUR_API_KEY
```

- **Secret Key (sk_*)** — Required for all endpoints except `/v1/track`
- **Public Key (pk_*)** — Only works with `/v1/track` for client-side event tracking

## Making requests

### Send transactional email

```bash
curl -X POST {{API_URL}}/v1/send \
  -H "Authorization: Bearer sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "subject": "Hello",
    "body": "<p>Your message here</p>"
  }'
```

### Track event

```bash
curl -X POST {{API_URL}}/v1/track \
  -H "Authorization: Bearer pk_your_public_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "event": "signed_up"
  }'
```

### Create contact

```bash
curl -X POST {{API_URL}}/contacts \
  -H "Authorization: Bearer sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "subscribed": true,
    "data": {
      "firstName": "John",
      "plan": "pro"
    }
  }'
```

## Response format

### Success responses

Public API endpoints (`/v1/send`, `/v1/track`, `/v1/verify`) return a wrapped envelope:

```json
{
  "success": true,
  "data": {
    "contact": "cnt_abc123",
    "event": "evt_xyz789",
    "timestamp": "2025-11-30T10:30:00.000Z"
  }
}
```

Dashboard endpoints (contacts, templates, campaigns, segments, workflows, etc.) return the resource directly — no `success`/`data` wrapper:

```json
{
  "id": "cnt_abc123",
  "email": "user@example.com",
  "createdAt": "2025-11-30T10:30:00.000Z"
}
```

List endpoints with cursor pagination return:

```json
{
  "data": [ /* items */ ],
  "cursor": "def456",
  "hasMore": true,
  "total": 10000
}
```

### Error response

All errors include detailed information to help you debug issues:

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "statusCode": 422,
    "requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "errors": [
      {
        "field": "email",
        "message": "Invalid email",
        "code": "invalid_string"
      }
    ],
    "suggestion": "One or more fields have incorrect types. Check that strings are quoted, numbers are unquoted, and booleans are true/false."
  },
  "timestamp": "2025-11-30T10:30:00.000Z"
}
```

**Error fields:**
- `code` — Machine-readable error code for programmatic handling
- `message` — Human-readable description
- `statusCode` — HTTP status code
- `requestId` — Unique ID for debugging (include when contacting support)
- `errors` — Field-level validation details (when applicable)
- `suggestion` — Helpful guidance for fixing the error

See the [Error Codes documentation](/api-reference/errors) for complete details and examples.

## Pagination

Most list endpoints use **cursor-based** pagination:

```bash
GET /contacts?limit=100&cursor=abc123
```

**Parameters:**
- `limit` — items per page (default: 20, max: 100)
- `cursor` — pagination cursor from the previous response's `cursor` field

**Response:**
```json
{
  "data": [ /* items */ ],
  "cursor": "def456",
  "hasMore": true,
  "total": 10000
}
```

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** — 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.

## Error codes

The API uses standard HTTP status codes along with machine-readable error codes:

**400 Bad Request** — Invalid request parameters or malformed request body

**401 Unauthorized** — Missing or invalid API key

**403 Forbidden** — Not authorized to access this resource or project disabled

**404 Not Found** — Resource doesn't exist

**422 Unprocessable Entity** — Request validation failed (see `errors` array for details)

**429 Too Many Requests** — Rate limit exceeded

**500 Internal Server Error** — An unexpected error occurred (contact support with request ID)

For a complete list of error codes and troubleshooting guidance, see the [Error Codes documentation](/api-reference/errors).

## API endpoints

A complete, grouped reference of every endpoint exposed by the Plunk API.

### Public API

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

| 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

| 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

| 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 campaign — returns 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

| 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

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

| 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

| 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

### Node.js

```javascript
const PLUNK_SECRET_KEY = process.env.PLUNK_SECRET_KEY;

async function sendEmail(to, subject, body) {
  const response = await fetch('{{API_URL}}/v1/send', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${PLUNK_SECRET_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ to, subject, body })
  });

  const data = await response.json();

  if (!data.success) {
    throw new Error(`[${data.error.code}] ${data.error.message}`);
  }

  return data.data;
}
```

### Python

```python
import os
import requests

PLUNK_SECRET_KEY = os.environ['PLUNK_SECRET_KEY']

def send_email(to, subject, body):
    response = requests.post(
        '{{API_URL}}/v1/send',
        headers={
            'Authorization': f'Bearer {PLUNK_SECRET_KEY}',
            'Content-Type': 'application/json'
        },
        json={'to': to, 'subject': subject, 'body': body}
    )

    data = response.json()

    if not data.get('success'):
        error = data['error']
        raise Exception(f"[{error['code']}] {error['message']}")

    return data['data']
```

### cURL

```bash
curl -X POST {{API_URL}}/v1/send \
  -H "Authorization: Bearer $PLUNK_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "user@example.com", "subject": "Hello", "body": "Message"}'
```

## What's next

- [Send a transactional email](/api-reference/public-api/sendEmail)
- [Track an event](/api-reference/public-api/trackEvent)
- [Error codes reference](/api-reference/errors)
