Plunk Public API
Public API endpoints for sending emails and tracking events
Public API endpoints for sending emails and tracking events
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/plunk-public-api-api"
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Plunk Public API
description: Open-source email platform API for transactional emails, campaigns, and marketing automation
version: 1.0.0
contact:
name: Plunk Support
url: https://www.useplunk.com
servers:
- url: https://next-api.useplunk.com
description: Production server
security:
- ApiKeyAuth: []
tags:
- name: Public API
description: Public API endpoints for sending emails and tracking events
paths:
/v1/send:
post:
tags:
- Public API
summary: Send transactional email
description: 'Send a transactional email via the public API. Automatically creates or updates the recipient contact.
**Required content:** either a `template` ID, **or** both `subject` and `body`. Template fields can be overridden by explicit request fields.
**Sender:** `from` is required unless using a template that already has a `from` configured. The sender''s domain must be verified.
**Multiple recipients:** when `to` is an array, each recipient is processed sequentially with its own contact upsert and rendered email — there is no batch-send semantics. Sending is always immediate; for scheduled sends, use a Campaign.
**Attachments:** up to 10 attachments per email and 10 MB total by default. The total message size cannot exceed 40 MB.'
operationId: sendEmail
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- to
properties:
to:
oneOf:
- type: string
format: email
description: Simple email address
- type: object
required:
- email
properties:
name:
type: string
description: Recipient display name
email:
type: string
format: email
description: Recipient email address
description: Recipient with name and email
- type: array
items:
oneOf:
- type: string
format: email
- type: object
required:
- email
properties:
name:
type: string
description: Recipient display name
email:
type: string
format: email
description: Recipient email address
description: Array of recipients (strings or objects)
description: Recipient email(s). Can be a string, an object with {name, email}, or an array of either.
subject:
type: string
minLength: 1
maxLength: 998
description: Email subject. Required if no `template` is provided. Cannot contain newline characters.
body:
type: string
minLength: 1
description: Email body (HTML). Required if no `template` is provided.
template:
type: string
format: uuid
description: Template ID to use for this email. When provided, uses the template's subject, body, from, and reply-to settings. You can override these by explicitly providing subject, body, from, or reply fields in the request. Template variables are populated from the data field.
from:
oneOf:
- type: string
format: email
description: Simple email address
- type: object
required:
- email
properties:
name:
type: string
description: Sender display name
email:
type: string
format: email
description: Sender email address
description: Sender with name and email
description: 'Sender email address (requires verified domain). Required unless using a template that has a ''from'' address configured. Can be a string (e.g., ''hello@example.com'') or an object with {name, email} (e.g., {name: ''My App'', email: ''hello@example.com''}).'
name:
type: string
description: '**Deprecated.** Sender display name. Prefer `from: { name, email }`. Used only as a fallback when `from` is a string and no name is set there.'
subscribed:
type: boolean
description: Subscription state to apply to the recipient. For **new** contacts, defaults to `false` on `/v1/send`. For **existing** contacts, omitting this preserves their current state — pass `true` or `false` to explicitly change it. A change emits `contact.subscribed` or `contact.unsubscribed`.
data:
type: object
additionalProperties: true
description: 'Variables for template rendering and contact data updates. Each value can be:
- A primitive (string, number, boolean) — saved on the contact and available as a template variable.
- `null` — deletes the field from the contact.
- An empty string — skipped (does not overwrite existing data).
- An object `{ value, persistent: false }` — used for this send only, not stored on the contact (good for one-shot password reset codes, magic links).
Reserved keys (`id`, `plunk_id`, `plunk_email`, `email`, `unsubscribeUrl`, `subscribeUrl`, `manageUrl`) are silently filtered out.'
headers:
type: object
additionalProperties:
type: string
description: Custom email headers. Header names cannot contain `\r\n`. Header values are limited to 998 characters and cannot contain `\r\n` (header injection is rejected).
reply:
type: string
format: email
description: Reply-to address.
attachments:
type: array
description: 'Email attachments. Default cap: 10 attachments and 10 MB total. The full message size cannot exceed 40 MB.'
maxItems: 10
items:
type: object
required:
- filename
- content
- contentType
properties:
filename:
type: string
maxLength: 255
description: Attachment filename. Cannot contain newline or quote characters.
content:
type: string
description: Base64-encoded file content.
contentType:
type: string
maxLength: 255
description: MIME type (e.g., `application/pdf`, `image/png`).
contentId:
type: string
description: Content-ID for inline images. Required when `disposition` is `inline`. Reference the image in the email body via `<img src="cid:yourContentId">`.
disposition:
type: string
enum:
- attachment
- inline
default: attachment
description: Use `inline` together with `contentId` to embed images in the body. Use `attachment` (the default) for downloadable files.
examples:
simple:
summary: Simple transactional email
value:
to: user@example.com
subject: Password Reset Request
body: '<h1>Reset Your Password</h1><p>Click the link to reset: {{resetLink}}</p>'
data:
resetLink: https://example.com/reset/abc123
withNames:
summary: Email with recipient and sender names
value:
to:
name: Jane Doe
email: jane@example.com
from:
name: My Company
email: hello@mycompany.com
subject: Welcome to Our Service
body: <h1>Welcome {{name}}!</h1><p>We're glad to have you.</p>
data:
name: Jane
multipleRecipients:
summary: Multiple recipients with names
value:
to:
- name: Jane Doe
email: jane@example.com
- name: John Smith
email: john@example.com
from:
name: Newsletter
email: news@mycompany.com
subject: Monthly Update
body: <h1>Hello {{name}}!</h1>
withTemplate:
summary: Using a template
description: Send email using a template. Provide the template ID and any data for template variables. The template's subject, body, from address, and reply-to will be used automatically.
value:
to: user@example.com
template: 9c4d5e1f-2a3b-4c5d-8e9f-0a1b2c3d4e5f
data:
firstName: John
lastName: Doe
resetCode:
value: ABC123
persistent: false
withTemplateOverride:
summary: Using template with overrides
description: You can override template values by providing subject, body, from, or reply fields. This example overrides the template's subject line.
value:
to: user@example.com
template: 9c4d5e1f-2a3b-4c5d-8e9f-0a1b2c3d4e5f
subject: Custom Subject Override
data:
firstName: Jane
marketingEmail:
summary: 'Marketing email (set subscribed: true)'
value:
to: user@example.com
subject: Weekly Newsletter
body: <h1>This Week's Updates</h1>
subscribed: true
withAttachment:
summary: Email with PDF attachment
value:
to: user@example.com
subject: Your Invoice
body: <h1>Invoice Attached</h1><p>Please find your invoice attached.</p>
attachments:
- filename: invoice.pdf
content: JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL...
contentType: application/pdf
responses:
'200':
description: Email queued successfully
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
emails:
type: array
items:
type: object
properties:
contact:
type: object
properties:
id:
type: string
email:
type: string
email:
type: string
description: Plunk email record ID. Use this to correlate webhook events (which include this ID as 'emailId' in the event data) with your send requests.
timestamp:
type: string
format: date-time
example:
success: true
data:
emails:
- contact:
id: cnt_abc123
email: user@example.com
email: ac32f08e-c6b9-45d3-9824-a73dff1e3bbf
timestamp: '2025-01-15T10:30:00.000Z'
'400':
description: Malformed JSON body, or an invalid `Idempotency-Key` header.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: The project is disabled and cannot send.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: The `template` ID does not exist in this project.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
$ref: '#/components/responses/IdempotencyConflict'
'422':
$ref: '#/components/responses/ValidationError'
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
/v1/track:
post:
tags:
- Public API
summary: Track event
description: 'Track an event for a contact. Automatically creates or upserts the contact, then records the event. Tracked events can be used as workflow triggers, segment filters, and audience filters.
**Reserved event names** (rejected with `VALIDATION_ERROR` and code `reserved_event`): anything matching `email.*`, `contact.subscribed`, `contact.unsubscribed`, `segment.<slug>.entry`, `segment.<slug>.exit`. These are emitted by Plunk itself.
**Idempotency**: re-tracking the same event creates a new event record. Send an `Idempotency-Key` header to have a repeated request refused with `409` instead.'
operationId: trackEvent
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
- event
properties:
email:
type: string
format: email
description: Contact email. The contact is auto-created if it doesn't exist.
event:
type: string
description: Event name. Cannot match the reserved patterns above.
subscribed:
type: boolean
description: Subscription state to apply to the contact. **New** contacts default to subscribed (`true`). **Existing** contacts keep their current state unless you pass an explicit value here. Pass `false` to track an event without resubscribing an unsubscribed contact.
data:
type: object
additionalProperties: true
description: 'Contact data and one-off event variables. Persistent values (primitives, plain objects) are saved on the contact and become available as template variables. Pass `{ value, persistent: false }` for one-shot variables that should not be stored on the contact (e.g. order IDs, transaction details). `null` deletes a field. Empty strings are ignored. Reserved keys are filtered out — see the contacts concept page.'
example:
email: user@example.com
event: purchase
data:
product: Premium Plan
amount: 99
responses:
'200':
description: Event tracked successfully
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
contact:
type: string
description: Contact ID
event:
type: string
description: Event ID
timestamp:
type: string
format: date-time
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/IdempotencyConflict'
'422':
$ref: '#/components/responses/ValidationError'
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
/v1/verify:
post:
tags:
- Public API
summary: Verify email address
description: Verify an email address for validity, check if it's from a disposable domain or personal email provider, verify MX records, and detect potential typos with suggestions.
operationId: verifyEmail
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
properties:
email:
type: string
format: email
description: Email address to verify
examples:
validEmail:
summary: Valid email address
value:
email: user@gmail.com
typoEmail:
summary: Email with potential typo
value:
email: user@gmial.com
disposableEmail:
summary: Disposable email address
value:
email: user@tempmail.com
responses:
'200':
description: Email verification completed successfully
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
description: Always true for successful requests
data:
type: object
properties:
email:
type: string
format: email
description: Email address that was verified
valid:
type: boolean
description: Whether the email appears to be valid overall
isDisposable:
type: boolean
description: Whether the email is from a disposable/temporary email domain
isAlias:
type: boolean
description: Whether the email is from a forwarding/alias service
isTypo:
type: boolean
description: Whether a potential typo was detected in the email address
isPlusAddressed:
type: boolean
description: Whether the email uses plus addressing (contains a + in the local part)
isPersonalEmail:
type: boolean
description: Whether the email is from a personal/free email provider (Gmail, Hotmail, Yahoo, etc.)
domainExists:
type: boolean
description: Whether the domain exists in DNS (has NS records)
hasWebsite:
type: boolean
description: Whether the domain has a website (has DNS A or AAAA records) - informational only
hasMxRecords:
type: boolean
description: Whether the domain has MX records configured for email delivery
suggestedEmail:
type: string
format: email
description: Suggested correction if a typo was detected (optional)
nullable: true
reasons:
type: array
items:
type: string
description: Array of human-readable reasons describing the verification results
required:
- email
- valid
- isDisposable
- isAlias
- isTypo
- isPlusAddressed
- isPersonalEmail
- domainExists
- hasWebsite
- hasMxRecords
- reasons
examples:
validEmail:
summary: Valid email
value:
success: true
data:
email: user@gmail.com
valid: true
isDisposable: false
isAlias: false
isTypo: false
isPlusAddressed: false
isPersonalEmail: true
domainExists: true
hasWebsite: true
hasMxRecords: true
reasons:
- Email appears to be valid
typoDetected:
summary: Email with typo detected
value:
success: true
data:
email: user@gmial.com
valid: false
isDisposable: false
isAlias: false
isTypo: true
isPlusAddressed: false
isPersonalEmail: false
domainExists: false
hasWebsite: false
hasMxRecords: false
suggestedEmail: user@gmail.com
reasons:
- Possible typo detected, did you mean gmail.com?
- Domain does not exist (no nameservers found)
disposableEmail:
summary: Disposable email detected
value:
success: true
data:
email: user@tempmail.com
valid: true
isDisposable: true
isAlias: false
isTypo: false
isPlusAddressed: false
isPersonalEmail: false
domainExists: true
hasWebsite: true
hasMxRecords: true
reasons:
- Email appears to be valid
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/ValidationError'
components:
responses:
Unauthorized:
description: Missing or invalid API key.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
success: false
error:
code: INVALID_API_KEY
message: Invalid secret API key. This endpoint requires a secret key (sk_*), not a public key.
statusCode: 401
requestId: 8f14e45f-ceea-467a-9575-1f0f38e0b1c2
timestamp: '2025-01-15T10:30:00.000Z'
IdempotencyConflict:
description: Idempotency-Key already used. The request was refused, not performed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
success: false
error:
code: IDEMPOTENCY_KEY_REUSED
message: Idempotency-Key "order-1234-receipt" has already been used
statusCode: 409
requestId: 8f14e45f-ceea-467a-9575-1f0f38e0b1c2
details:
key: order-1234-receipt
originalRequest: POST /v1/send
originalRequestAt: '2025-01-15T10:30:00.000Z'
originalStatusCode: 200
suggestion: This Idempotency-Key was already used, so the request was refused rather than performed twice. Generate a new key for a genuinely new request.
timestamp: '2025-01-15T10:31:00.000Z'
ValidationError:
description: Request body failed schema validation. `error.errors` lists the offending fields.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
success: false
error:
code: VALIDATION_ERROR
message: Request validation failed
statusCode: 422
requestId: 8f14e45f-ceea-467a-9575-1f0f38e0b1c2
errors:
- field: to
message: Invalid email
code: invalid_string
suggestion: Please check the API documentation for the correct request format.
timestamp: '2025-01-15T10:30:00.000Z'
schemas:
FieldError:
type: object
properties:
field:
type: string
description: Dot-path of the offending field, e.g. `attachments.0.filename`.
message:
type: string
code:
type: string
description: Validation issue code, e.g. `invalid_type`, `too_big`, `reserved_event`.
received:
description: The value that was received, when available.
Error:
type: object
properties:
success:
type: boolean
enum:
- false
error:
type: object
properties:
code:
type: string
description: Machine-readable error code, e.g. `VALIDATION_ERROR`, `INVALID_API_KEY`, `IDEMPOTENCY_KEY_REUSED`.
message:
type: string
statusCode:
type: integer
requestId:
type: string
description: Correlation ID for this request. Include it when contacting support.
errors:
type: array
items:
$ref: '#/components/schemas/FieldError'
description: Field-level detail, present on validation failures.
details:
type: object
additionalProperties: true
description: Additional error context.
suggestion:
type: string
description: Hint for fixing the request.
timestamp:
type: string
format: date-time
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
schema:
type: string
maxLength: 255
description: Optional key that guarantees this request runs at most once. If the key was already used by your project, the request is refused with `409` instead of being performed a second time. Keys are scoped to your project, expire after 24 hours (configurable when self-hosting), and must be 1-255 printable ASCII characters.
securitySchemes:
ApiKeyAuth:
type: http
scheme: bearer
bearerFormat: API Key
description: 'API Key authentication. The project is automatically derived from the key.
**`/v1/track` requires a public key (`pk_*`)** — it is the one endpoint intended for client-side use, and a secret key is rejected there with `401`.
**Every other endpoint requires a secret key (`sk_*`)** and rejects public keys with `401`.
So the two key types are not interchangeable in either direction: pick the key that matches the endpoint you are calling.'
x-ext-urls: {}