{"openapi":"3.1.0","info":{"title":"Plunk 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":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API Key authentication. **Secret keys (sk_*)** are required for all endpoints except `/v1/track`. **Public keys (pk_*)** only work with the `/v1/track` endpoint for client-side event tracking. The project is automatically derived from the key."}},"schemas":{"Contact":{"type":"object","properties":{"id":{"type":"string","description":"Unique contact identifier"},"email":{"type":"string","format":"email","description":"Contact email address"},"subscribed":{"type":"boolean","description":"Subscription status"},"data":{"type":"object","description":"Custom contact data fields","additionalProperties":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Template":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"type":{"type":"string","enum":["TRANSACTIONAL","MARKETING"]},"createdAt":{"type":"string","format":"date-time"}}},"Campaign":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"type":{"type":"string","enum":["ALL","SEGMENT","FILTERED"]},"status":{"type":"string","enum":["DRAFT","SCHEDULED","SENDING","SENT"]},"scheduledAt":{"type":"string","format":"date-time","nullable":true}}},"Segment":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"filters":{"type":"object"},"trackMembership":{"type":"boolean"},"memberCount":{"type":"integer"}}},"Error":{"type":"object","properties":{"code":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"time":{"type":"integer"}}}},"parameters":{"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum items per page"},"Cursor":{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor"}}},"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.\n\n**Required content:** either a `template` ID, **or** both `subject` and `body`. Template fields can be overridden by explicit request fields.\n\n**Sender:** `from` is required unless using a template that already has a `from` configured. The sender's domain must be verified.\n\n**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.\n\n**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","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:\n- A primitive (string, number, boolean) — saved on the contact and available as a template variable.\n- `null` — deletes the field from the contact.\n- An empty string — skipped (does not overwrite existing data).\n- An object `{ value, persistent: false }` — used for this send only, not stored on the contact (good for one-shot password reset codes, magic links).\n\nReserved 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":"clx123abc456","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":"clx123abc456","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":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/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.\n\n**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.\n\n**No idempotency**: re-tracking the same event creates a new event record.","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"}}}}}}}}}}},"/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"]}}}}}}},"400":{"description":"Bad request - invalid email format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized - invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/contacts":{"get":{"tags":["Contacts"],"summary":"List contacts","description":"Get a paginated list of contacts with cursor-based pagination.","operationId":"listContacts","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"search","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match on email."}],"responses":{"200":{"description":"List of contacts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"cursor":{"type":"string","nullable":true,"description":"Cursor for the next page. Pass this back as the `cursor` query parameter to fetch the next page."},"hasMore":{"type":"boolean"},"total":{"type":"integer","description":"Total count. Only populated on the first page (when no `cursor` is supplied); subsequent pages return `0` to avoid the recount cost."}}}}}}}},"post":{"tags":["Contacts"],"summary":"Create or update contact","description":"Create a new contact or update existing (upsert by email)","operationId":"createContact","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"subscribed":{"type":"boolean","default":true},"data":{"type":"object","additionalProperties":true}}},"example":{"email":"user@example.com","subscribed":true,"data":{"firstName":"John","lastName":"Doe","plan":"premium"}}}}},"responses":{"200":{"description":"Contact updated","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Contact"},{"type":"object","properties":{"_meta":{"type":"object","properties":{"isNew":{"type":"boolean"},"isUpdate":{"type":"boolean"}}}}}]}}}},"201":{"description":"Contact created","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Contact"},{"type":"object","properties":{"_meta":{"type":"object","properties":{"isNew":{"type":"boolean"},"isUpdate":{"type":"boolean"}}}}}]}}}}}}},"/contacts/{id}":{"get":{"tags":["Contacts"],"summary":"Get contact","description":"Get a single contact by ID","operationId":"getContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Contact details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"404":{"description":"Contact not found"}}},"patch":{"tags":["Contacts"],"summary":"Update contact","description":"Update an existing contact's email, subscription state, or `data` fields.","operationId":"updateContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Change the contact's email address. Returns 409 if another contact in the project already uses this email."},"subscribed":{"type":"boolean","description":"Update subscription state. Flipping this fires `contact.subscribed` or `contact.unsubscribed`."},"data":{"type":"object","additionalProperties":true,"description":"Patch contact data. `null` deletes a key, empty strings are ignored, primitives are stored. Reserved keys are silently filtered out."}}}}}},"responses":{"200":{"description":"Contact updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"409":{"description":"An existing contact already uses the email you're trying to set."}}},"delete":{"tags":["Contacts"],"summary":"Delete contact","description":"Permanently delete a contact","operationId":"deleteContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Contact deleted successfully (no content returned)"},"404":{"description":"Contact not found"}}}},"/templates":{"get":{"tags":["Templates"],"summary":"List templates","description":"Get a paginated list of email templates","operationId":"listTemplates","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"type","in":"query","schema":{"type":"string","enum":["TRANSACTIONAL","MARKETING"]}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"List of templates","content":{"application/json":{"schema":{"type":"object","properties":{"templates":{"type":"array","items":{"$ref":"#/components/schemas/Template"}},"total":{"type":"integer"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"totalPages":{"type":"integer"}}}}}}}},"post":{"tags":["Templates"],"summary":"Create template","description":"Create a new email template","operationId":"createTemplate","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","subject","body","type"],"properties":{"name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"type":{"type":"string","enum":["TRANSACTIONAL","MARKETING"]}}}}}},"responses":{"201":{"description":"Template created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}}}}},"/campaigns":{"get":{"tags":["Campaigns"],"summary":"List campaigns","description":"Get a paginated list of email campaigns","operationId":"listCampaigns","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","SCHEDULED","SENDING","SENT"]}}],"responses":{"200":{"description":"List of campaigns","content":{"application/json":{"schema":{"type":"object","properties":{"campaigns":{"type":"array","items":{"$ref":"#/components/schemas/Campaign"}},"page":{"type":"integer"},"pageSize":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}}}}}}}},"post":{"tags":["Campaigns"],"summary":"Create campaign","description":"Create a new email campaign","operationId":"createCampaign","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","subject","body","from","audienceType"],"properties":{"name":{"type":"string","description":"Campaign name"},"description":{"type":"string","description":"Campaign description"},"subject":{"type":"string","description":"Email subject line"},"body":{"type":"string","description":"HTML email body"},"from":{"type":"string","format":"email","description":"Sender email address (must be from verified domain)"},"fromName":{"type":"string","description":"Sender name"},"replyTo":{"type":"string","format":"email","description":"Reply-to email address"},"audienceType":{"type":"string","enum":["ALL","SEGMENT","FILTERED"],"description":"Target audience type"},"segmentId":{"type":"string","description":"Segment ID (required if audienceType is SEGMENT)"},"audienceFilter":{"type":"object","description":"Filter conditions (required if audienceType is FILTERED)"}}}}}},"responses":{"200":{"description":"Campaign created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Campaign"}}}}}}}}},"/campaigns/{id}/send":{"post":{"tags":["Campaigns"],"summary":"Send or schedule campaign","description":"Send a campaign immediately or schedule for future delivery","operationId":"sendCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"scheduledFor":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 timestamp for when to send the campaign. Omit or set to null for immediate send."}}}}}},"responses":{"200":{"description":"Campaign scheduled/sending"}}}},"/segments":{"get":{"tags":["Segments"],"summary":"List segments","description":"Get all audience segments","operationId":"listSegments","responses":{"200":{"description":"List of segments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Segment"}}}}}}},"post":{"tags":["Segments"],"summary":"Create segment","description":"Create a new audience segment","operationId":"createSegment","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","filters"],"properties":{"name":{"type":"string"},"filters":{"type":"object","description":"Filter conditions"},"trackMembership":{"type":"boolean","default":false}}},"example":{"name":"Premium Users","filters":{"operator":"AND","conditions":[{"field":"data.plan","operator":"equals","value":"premium"}]},"trackMembership":true}}}},"responses":{"201":{"description":"Segment created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Segment"}}}}}}}},"tags":[{"name":"Public API","description":"Public API endpoints for sending emails and tracking events"},{"name":"Contacts","description":"Contact management operations"},{"name":"Templates","description":"Email template management"},{"name":"Campaigns","description":"Email campaign management"},{"name":"Segments","description":"Audience segmentation"}],"x-ext-urls":{}}