PlunkPlunk
API Reference

API Reference

Complete Plunk API documentation

Base URL

https://next-api.useplunk.com

All 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/track for 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 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 for complete details and examples.

Pagination

Most list endpoints use cursor-based pagination:

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:

{
  "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.

MethodPathDescriptionKey
POST/v1/sendSend a transactional email. Single or multiple recipients, template or inline content, attachments, headers, custom data.sk_*
POST/v1/trackTrack an event for a contact. Auto-creates or upserts the contact. Triggers workflows.pk_* or sk_*
POST/v1/verifyValidate an email address — format, MX records, disposable domains, typo detection.sk_*

Contacts

MethodPathDescription
GET/contactsList contacts. Supports search (by email substring), limit, cursor.
POST/contactsCreate or upsert a contact by email. Returns _meta.isNew and _meta.isUpdate.
GET/contacts/:idGet a single contact.
PATCH/contacts/:idUpdate a contact's email, subscription state, or data fields.
DELETE/contacts/:idDelete a contact.
POST/contacts/lookupBulk email-existence check (max 500 emails per call).
Custom fields
GET/contacts/fieldsList standard and custom fields with inferred types and coverage percentages.
GET/contacts/fields/:field/valuesDistinct values for a custom field — used by segment / workflow filter UIs.
GET/contacts/fields/:field/usageWhere a custom field is referenced (segments, campaigns, workflows).
DELETE/contacts/fields/:fieldDelete a custom field across every contact in the project.
CSV import
POST/contacts/importUpload a CSV file (multipart, ≤ 5 MB). Queued — returns a jobId.
GET/contacts/import/:jobIdPoll the status of a CSV import job.
Bulk operations
POST/contacts/bulk-subscribeSubscribe up to 1,000 contacts by ID. Queued — returns a jobId.
POST/contacts/bulk-unsubscribeUnsubscribe up to 1,000 contacts by ID. Queued.
POST/contacts/bulk-deleteDelete up to 1,000 contacts by ID. Queued.
GET/contacts/bulk/:jobIdPoll the status of a bulk job.

Templates

MethodPathDescription
GET/templatesList all templates.
POST/templatesCreate a template. from must be on a verified domain.
GET/templates/:idGet a template.
PATCH/templates/:idUpdate a template.
DELETE/templates/:idDelete a template.
POST/templates/:id/duplicateDuplicate a template — returns the new template ID.
GET/templates/:id/usageList campaigns and workflow steps that reference this template.

Campaigns

MethodPathDescription
GET/campaignsList all campaigns.
POST/campaignsCreate a campaign in DRAFT. from must be on a verified domain.
GET/campaigns/:idGet a campaign.
PUT/campaigns/:idUpdate a campaign (replace).
DELETE/campaigns/:idDelete a campaign. Returns 409 if it has active executions.
POST/campaigns/:id/duplicateDuplicate a campaign — returns the new campaign in DRAFT.
POST/campaigns/:id/sendSend (or schedule) the campaign. Pass scheduledFor for delayed sends.
POST/campaigns/:id/cancelCancel a SCHEDULED or SENDING campaign.
POST/campaigns/:id/testSend a test email to a single address ({ email: "you@example.com" }).
GET/campaigns/:id/statsGet current send / open / click / bounce counts.

Segments

MethodPathDescription
GET/segmentsList all segments (no pagination — small list).
POST/segmentsCreate a segment. type: "DYNAMIC" requires condition; type: "STATIC" rejects it.
GET/segments/:idGet a segment, including cached memberCount.
PATCH/segments/:idUpdate name, description, condition (dynamic only), or trackMembership.
DELETE/segments/:idDelete a segment. Returns 409 if used by an active campaign.
GET/segments/:id/contactsPage-based list of segment members. page, pageSize (max 100). Live for dynamic segments.
POST/segments/:id/membersAdd emails to a static segment. Body: { emails, createMissing?, subscribed? }.
DELETE/segments/:id/membersRemove emails from a static segment. Body: { emails }.
POST/segments/:id/computeRecompute membership for a tracked dynamic segment — fires entry/exit events.
POST/segments/:id/refreshCheap 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.

MethodPathDescription
GET/workflowsList all workflows.
GET/workflows/fieldsFields available to use in CONDITION step filters (contact + event fields).
POST/workflowsCreate a workflow. Always starts with triggerType: EVENT and enabled: false.
GET/workflows/:idGet a workflow with its steps and transitions.
PATCH/workflows/:idUpdate workflow metadata, trigger type / config, enabled, allowReentry.
DELETE/workflows/:idDelete a workflow. Active executions must be cancelled or completed first.
Steps
POST/workflows/:id/stepsAdd a step (SEND_EMAIL, DELAY, WAIT_FOR_EVENT, CONDITION, WEBHOOK, UPDATE_CONTACT, EXIT).
PATCH/workflows/:id/steps/:stepIdUpdate a step's config.
DELETE/workflows/:id/steps/:stepId?splice=trueDelete a step. Pass splice=true to auto-reconnect surrounding transitions.
Transitions
POST/workflows/:id/transitionsAdd a transition between two steps. For CONDITION steps, include branch: "yes" | "no".
DELETE/workflows/:id/transitions/:transitionIdDelete a transition.
Executions
POST/workflows/:id/executionsManually start an execution for a contact. Optional context JSON for per-execution variables.
GET/workflows/:id/executionsList executions, filterable by status.
GET/workflows/:id/executions/:executionIdGet a single execution.
DELETE/workflows/:id/executions/:executionIdCancel a running or waiting execution.
POST/workflows/:id/executions/cancel-allCancel every active execution at once.

Events

MethodPathDescription
POST/events/trackInternal alias for /v1/track, for dashboard use. Use /v1/track from your apps.
GET/eventsList recent tracked events for a project.
GET/events/statsAggregated event statistics.
GET/events/contact/:contactIdAll events for a single contact.
GET/events/namesAll distinct event names tracked in this project.
GET/events/:eventName/usageWhere an event name is referenced (segment filters, workflow triggers, conditions).
DELETE/events/:eventNameDelete every event with the given name from the project.

Domains

MethodPathDescription
GET/domains/project/:projectIdList domains configured for a project (verified and pending).
POST/domainsAdd a domain for verification. Returns the DNS records you need to add.
GET/domains/:id/verifyForce a verification check now (otherwise checked every 5 minutes in the background).
DELETE/domains/:idRemove a domain.

Activity & analytics

MethodPathDescription
GET/activityCross-resource activity feed (sends, opens, clicks, bounces, complaints, inbound, etc.).
GET/activity/statsAggregated counts for dashboard charts.
GET/activity/recent-countRecent event count for the dashboard's "live" indicator.
GET/activity/typesDistinct activity types in the project.
GET/activity/upcomingUpcoming scheduled sends and active executions.
GET/analytics/timeseriesEmail send / open / click time-series.
GET/analytics/top-campaignsTop-performing campaigns by metric.
GET/analytics/campaign-statsCampaign-level breakdown.
GET/analytics/top-eventsMost frequent custom event names.

Uploads

MethodPathDescription
POST/uploads/imageUpload 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.

MethodPathDescription
POST/auth/loginEmail + password login. Sets a JWT cookie.
POST/auth/signupSign up a new user (subject to DISABLE_SIGNUPS).
GET/auth/logoutClear the auth cookie.
GET/auth/oauth-configWhich OAuth providers are configured.
POST/auth/verify-emailVerify an email with a token from an email link.
POST/auth/request-verificationResend the verification email.
POST/auth/request-password-resetSend a password reset email.
POST/auth/reset-passwordReset a password with a token.
GET/users/@meGet the current user.
GET/users/@me/projectsList the user's projects.
POST/users/@me/projectsCreate a project.
PATCH/users/@me/projects/:idUpdate project settings.
POST/users/@me/projects/:id/regenerate-keysRotate both API keys.
POST/users/@me/projects/:id/checkoutCreate a Stripe Checkout session.
POST/users/@me/projects/:id/billing-portalOpen the Stripe billing portal.
GET/users/@me/projects/:id/billing-limitsRead per-category billing caps.
PUT/users/@me/projects/:id/billing-limitsUpdate per-category billing caps.
GET/users/@me/projects/:id/billing-consumptionCurrent period consumption for the project.
GET/users/@me/projects/:id/billing-invoicesStripe invoices for the project.
GET/users/@me/projects/:id/securitySecurity info — bounce/complaint rates, recent suspensions.
POST/users/@me/projects/:id/resetWipe project data (irreversible).
DELETE/users/@me/projects/:idDelete the project entirely.
GET/projects/:id/setup-stateOnboarding setup state.
GET/projects/:id/securitySecurity state for a single project.
GET/projects/:id/membersProject team members.
POST/projects/:id/membersInvite a team member.
PATCH/projects/:id/members/:userIdChange a member's role.
DELETE/projects/:id/members/:userIdRemove a member.

Configuration

MethodPathDescription
GET/configPublic, 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.

MethodPath
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"}'

What's next