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 ``. 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: '

Reset Your Password

Click the link to reset: {{resetLink}}

' 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:

Welcome {{name}}!

We're glad to have you.

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:

Hello {{name}}!

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:

This Week's Updates

subscribed: true withAttachment: summary: Email with PDF attachment value: to: user@example.com subject: Your Invoice body:

Invoice Attached

Please find your invoice attached.

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..entry`, `segment..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: {}