--- 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": "
Your message here
" }' ``` ### 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)