API Reference
Complete Plunk API documentation
Base URL
https://next-api.useplunk.comAll API requests use this base URL.
Authentication
Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY- Secret Key (sk_*) — Required for all endpoints except
/v1/track - Public Key (pk_*) — Only works with
/v1/trackfor client-side event tracking
Making requests
Send transactional email
curl -X POST https://next-api.useplunk.com/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
curl -X POST https://next-api.useplunk.com/v1/track \
-H "Authorization: Bearer pk_your_public_key" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"event": "signed_up"
}'Create contact
curl -X POST https://next-api.useplunk.com/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:
{
"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:
{
"id": "cnt_abc123",
"email": "user@example.com",
"createdAt": "2025-11-30T10:30:00.000Z"
}List endpoints with cursor pagination return:
{
"data": [ /* items */ ],
"cursor": "def456",
"hasMore": true,
"total": 10000
}Error response
All errors include detailed information to help you debug issues:
{
"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 handlingmessage— Human-readable descriptionstatusCode— HTTP status coderequestId— 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 for complete details and examples.
Pagination
Most list endpoints use cursor-based pagination:
GET /contacts?limit=100&cursor=abc123Parameters:
limit— items per page (default: 20, max: 100)cursor— pagination cursor from the previous response'scursorfield
Response:
{
"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 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
const PLUNK_SECRET_KEY = process.env.PLUNK_SECRET_KEY;
async function sendEmail(to, subject, body) {
const response = await fetch('https://next-api.useplunk.com/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
import os
import requests
PLUNK_SECRET_KEY = os.environ['PLUNK_SECRET_KEY']
def send_email(to, subject, body):
response = requests.post(
'https://next-api.useplunk.com/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
curl -X POST https://next-api.useplunk.com/v1/send \
-H "Authorization: Bearer $PLUNK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "user@example.com", "subject": "Hello", "body": "Message"}'