{"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. The project is automatically derived from the key.\n\n**`/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`.\n\n**Every other endpoint requires a secret key (`sk_*`)** and rejects public keys with `401`.\n\nSo the two key types are not interchangeable in either direction: pick the key that matches the endpoint you are calling."}},"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"},"description":{"type":"string","nullable":true},"subject":{"type":"string"},"body":{"type":"string","description":"HTML content with `{{variable}}` placeholders."},"from":{"type":"string","format":"email","description":"Sender address. Must belong to a verified domain."},"fromName":{"type":"string","nullable":true},"replyTo":{"type":"string","format":"email","nullable":true},"type":{"type":"string","enum":["TRANSACTIONAL","MARKETING","HEADLESS"]},"projectId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Campaign":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"subject":{"type":"string"},"body":{"type":"string"},"from":{"type":"string","format":"email"},"fromName":{"type":"string","nullable":true},"replyTo":{"type":"string","format":"email","nullable":true},"type":{"type":"string","enum":["TRANSACTIONAL","MARKETING","HEADLESS"],"description":"Content type of the campaign email. Not to be confused with `audienceType`."},"status":{"type":"string","enum":["DRAFT","SCHEDULED","SENDING","SENT","CANCELLED"]},"audienceType":{"type":"string","enum":["ALL","SEGMENT","FILTERED"]},"audienceCondition":{"allOf":[{"$ref":"#/components/schemas/FilterCondition"}],"nullable":true,"description":"Set when `audienceType` is `FILTERED`."},"segmentId":{"type":"string","nullable":true,"description":"Set when `audienceType` is `SEGMENT`."},"scheduledFor":{"type":"string","format":"date-time","nullable":true,"description":"When the campaign is scheduled to send. Null for immediate or unsent campaigns."},"totalRecipients":{"type":"integer"},"sentCount":{"type":"integer"},"deliveredCount":{"type":"integer"},"openedCount":{"type":"integer"},"clickedCount":{"type":"integer"},"bouncedCount":{"type":"integer"},"projectId":{"type":"string"},"sentAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Segment":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","enum":["DYNAMIC","STATIC"],"description":"`DYNAMIC` segments are evaluated from `condition`. `STATIC` segments hold a manually managed member list."},"condition":{"allOf":[{"$ref":"#/components/schemas/FilterCondition"}],"nullable":true,"description":"Filter condition for `DYNAMIC` segments. Null for `STATIC` segments."},"trackMembership":{"type":"boolean","description":"When true, contacts entering or leaving the segment emit `segment.<slug>.entry` / `segment.<slug>.exit` events."},"memberCount":{"type":"integer","description":"Cached member count, refreshed by a background job rather than computed per request."},"projectId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Filter":{"type":"object","required":["field","operator"],"properties":{"field":{"type":"string","description":"Contact field to test. Custom fields are addressed via the `data.` prefix (e.g. `data.plan`); standard fields are `email`, `subscribed`, `createdAt`. Event operators take an event name instead."},"operator":{"type":"string","enum":["equals","notEquals","contains","notContains","greaterThan","lessThan","greaterThanOrEqual","lessThanOrEqual","exists","notExists","within","olderThan","triggered","triggeredWithin","triggeredOlderThan","notTriggered","notTriggeredWithin","memberOfSegment","notMemberOfSegment"]},"value":{"description":"Comparison value. Omitted for `exists` / `notExists` / `triggered` / `notTriggered`. For time-window operators this is the number of `unit`s."},"unit":{"type":"string","enum":["days","hours","minutes"],"description":"Time unit for window operators (`within`, `olderThan`, `triggeredWithin`, `triggeredOlderThan`, `notTriggeredWithin`)."}}},"FilterGroup":{"type":"object","required":["filters"],"properties":{"filters":{"type":"array","items":{"$ref":"#/components/schemas/Filter"},"description":"Filters within a group are combined with AND."},"conditions":{"$ref":"#/components/schemas/FilterCondition","description":"Optional nested condition, allowing arbitrarily deep AND/OR trees."}}},"FilterCondition":{"type":"object","required":["logic","groups"],"description":"A boolean tree over contact filters. `logic` combines the `groups`; the filters inside each group are always ANDed together.","properties":{"logic":{"type":"string","enum":["AND","OR"]},"groups":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/FilterGroup"}}}},"LegacyError":{"type":"object","description":"Flat error shape returned by the hand-validated create/update endpoints (`POST /contacts`, `POST /templates`, `POST /segments`) for missing required fields. Unlike the rest of the API these return `400` with a bare `error` string rather than the standard envelope. Errors raised deeper in those same endpoints (404, 409, domain verification) still use the standard `Error` envelope.","properties":{"error":{"type":"string"}}},"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":{"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum items per page. Values above 100 are clamped to 100."},"Cursor":{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor. Pass the `cursor` returned by the previous page."},"Page":{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},"PageSize":{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"default":20},"description":"Items per page."},"SortDirection":{"name":"dir","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"description":"Sort direction. Unrecognised values fall back to the default."},"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."}},"responses":{"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"}}}},"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"}}}},"UnverifiedDomain":{"description":"The sender domain is not registered to this project, or has not completed DNS verification. Add and verify the domain in your project settings first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":{"code":"INTERNAL_SERVER_ERROR","message":"Domain \"example.com\" is not verified. Please complete the DNS verification process in your domain settings.","statusCode":403,"requestId":"8f14e45f-ceea-467a-9575-1f0f38e0b1c2"},"timestamp":"2025-01-15T10:30:00.000Z"}}}},"NotFound":{"description":"The requested resource does not exist in this project.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"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"}}}}}},"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","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:\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":"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.\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**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"}}}},"/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."},{"name":"subscribed","in":"query","schema":{"type":"boolean"},"description":"Filter by subscription state. Omit to return both subscribed and unsubscribed contacts."},{"name":"sort","in":"query","schema":{"type":"string","enum":["email","createdAt"],"default":"createdAt"},"description":"Column to sort by. Unrecognised values fall back to `createdAt`."},{"$ref":"#/components/parameters/SortDirection"}],"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."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"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"}}}}}]}}}},"400":{"description":"`email` was not supplied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegacyError"},"example":{"error":"Email is required"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/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"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"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"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"An existing contact already uses the email you're trying to set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"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)"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/templates":{"get":{"tags":["Templates"],"summary":"List templates","description":"Get a paginated list of email templates.\n\nThis endpoint uses **offset pagination** (`page` / `pageSize`), not the cursor pagination used by `GET /contacts`.","operationId":"listTemplates","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"name":"type","in":"query","schema":{"type":"string","enum":["TRANSACTIONAL","MARKETING","HEADLESS"]},"description":"Filter by template type."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Case-insensitive match on name, description, or subject."},{"name":"sort","in":"query","schema":{"type":"string","enum":["name","createdAt","updatedAt"],"default":"createdAt"},"description":"Column to sort by. Unrecognised values fall back to `createdAt`."},{"$ref":"#/components/parameters/SortDirection"}],"responses":{"200":{"description":"List of templates","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Template"}},"total":{"type":"integer","description":"Total templates matching the filters, across all pages."},"page":{"type":"integer"},"pageSize":{"type":"integer"},"totalPages":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Templates"],"summary":"Create template","description":"Create a new email template.\n\n`from` is required and its domain must already be verified for this project, otherwise the request is rejected.","operationId":"createTemplate","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","subject","body","from"],"properties":{"name":{"type":"string","description":"Internal name for the template."},"description":{"type":"string","description":"Optional internal note. Not sent to recipients."},"subject":{"type":"string"},"body":{"type":"string","description":"HTML body. Use `{{variable}}` placeholders to interpolate contact data at send time."},"from":{"type":"string","format":"email","description":"Sender address. Must belong to a domain verified for this project."},"fromName":{"type":"string","description":"Sender display name."},"replyTo":{"type":"string","format":"email"},"type":{"type":"string","enum":["TRANSACTIONAL","MARKETING","HEADLESS"],"default":"MARKETING","description":"Optional. Defaults to `MARKETING` when omitted."}}},"example":{"name":"Password reset","subject":"Reset your password","body":"<h1>Hi {{firstName}}</h1><p>Use this code: {{resetCode}}</p>","from":"hello@example.com","fromName":"My App","type":"TRANSACTIONAL"}}}},"responses":{"201":{"description":"Template created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"400":{"description":"A required field (`name`, `subject`, `body`, `from`) is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegacyError"},"example":{"error":"From address is required"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/UnverifiedDomain"}}}},"/campaigns":{"get":{"tags":["Campaigns"],"summary":"List campaigns","description":"Get a paginated list of email campaigns.\n\nThis endpoint uses **offset pagination** (`page` / `pageSize`), not the cursor pagination used by `GET /contacts`.","operationId":"listCampaigns","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","SCHEDULED","SENDING","SENT","CANCELLED"]},"description":"Filter by campaign status. An unrecognised value returns `400`."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Case-insensitive match on name, subject, or sender address."},{"name":"sort","in":"query","schema":{"type":"string","enum":["name","createdAt","updatedAt"],"default":"createdAt"},"description":"Column to sort by. Unrecognised values fall back to `createdAt`."},{"$ref":"#/components/parameters/SortDirection"}],"responses":{"200":{"description":"List of campaigns","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Campaign"}},"page":{"type":"integer"},"pageSize":{"type":"integer"},"total":{"type":"integer","description":"Total campaigns matching the filters, across all pages."},"totalPages":{"type":"integer"}}}}}},"400":{"description":"Invalid `status` value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Campaigns"],"summary":"Create campaign","description":"Create a new email campaign. Campaigns are created as `DRAFT`; use `POST /campaigns/{id}/send` to send or schedule one.\n\nThe sender domain must already be verified for this project.\n\n**Audience:** `audienceType` decides which companion field is required — `SEGMENT` requires `segmentId`, `FILTERED` requires `audienceCondition`, and `ALL` requires neither.","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"},"type":{"type":"string","enum":["TRANSACTIONAL","MARKETING","HEADLESS"],"default":"MARKETING","description":"Content type of the campaign email. Defaults to `MARKETING`."},"audienceType":{"type":"string","enum":["ALL","SEGMENT","FILTERED"],"description":"Target audience type"},"segmentId":{"type":"string","format":"uuid","description":"Segment ID. Required when `audienceType` is `SEGMENT`."},"audienceCondition":{"allOf":[{"$ref":"#/components/schemas/FilterCondition"}],"description":"Audience filter tree. Required when `audienceType` is `FILTERED`. Same structure as a segment's `condition`."}}},"examples":{"allContacts":{"summary":"Send to every contact","value":{"name":"March newsletter","subject":"What shipped in March","body":"<h1>Hello {{firstName}}</h1>","from":"news@example.com","fromName":"Example","audienceType":"ALL"}},"segment":{"summary":"Send to a saved segment","value":{"name":"Premium announcement","subject":"A new perk for premium users","body":"<h1>Hello {{firstName}}</h1>","from":"news@example.com","audienceType":"SEGMENT","segmentId":"9c4d5e1f-2a3b-4c5d-8e9f-0a1b2c3d4e5f"}},"filtered":{"summary":"Send to an ad-hoc filter","description":"Premium users who signed up in the last 30 days.","value":{"name":"New premium onboarding","subject":"Getting started","body":"<h1>Welcome {{firstName}}</h1>","from":"news@example.com","audienceType":"FILTERED","audienceCondition":{"logic":"AND","groups":[{"filters":[{"field":"data.plan","operator":"equals","value":"premium"},{"field":"createdAt","operator":"within","value":30,"unit":"days"}]}]}}}}}}},"responses":{"201":{"description":"Campaign created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Campaign"}}}}}},"400":{"description":"`segmentId` missing for a `SEGMENT` campaign, or `audienceCondition` missing for a `FILTERED` campaign.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/UnverifiedDomain"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/campaigns/{id}/send":{"post":{"tags":["Campaigns"],"summary":"Send or schedule campaign","description":"Send a campaign immediately or schedule it for future delivery.","operationId":"sendCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"scheduledFor":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp for when to send the campaign. Omit or set to null for immediate send. Any value `new Date()` can parse is accepted; an unparseable one returns `400`."}}},"examples":{"immediate":{"summary":"Send now","value":{}},"scheduled":{"summary":"Schedule for later","value":{"scheduledFor":"2025-02-01T09:00:00.000Z"}}}}}},"responses":{"200":{"description":"Campaign scheduled or sending","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Campaign"},"message":{"type":"string","description":"Human-readable confirmation, e.g. `Campaign is being sent`."}}},"example":{"success":true,"data":{"id":"9c4d5e1f-2a3b-4c5d-8e9f-0a1b2c3d4e5f","name":"March newsletter","status":"SCHEDULED","scheduledFor":"2025-02-01T09:00:00.000Z"},"message":"Campaign scheduled for 2025-02-01T09:00:00.000Z"}}}},"400":{"description":"Unparseable `scheduledFor`, or the campaign is not in a sendable state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The campaign `id` in the path is not a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/segments":{"get":{"tags":["Segments"],"summary":"List segments","description":"Get all audience segments for the project, newest first. This endpoint is not paginated — it returns a bare array of every segment.","operationId":"listSegments","responses":{"200":{"description":"List of segments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Segment"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Segments"],"summary":"Create segment","description":"Create a new audience segment.\n\n**`DYNAMIC` segments** (the default) are defined by a `condition` — a boolean tree that is re-evaluated against your contacts, so membership changes on its own as contact data changes. `condition` is required for these.\n\n**`STATIC` segments** hold a manually managed member list; `condition` is ignored. Add members with `POST /segments/{id}/members`.","operationId":"createSegment","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":500},"type":{"type":"string","enum":["DYNAMIC","STATIC"],"default":"DYNAMIC","description":"Defaults to `DYNAMIC` when omitted."},"condition":{"allOf":[{"$ref":"#/components/schemas/FilterCondition"}],"description":"Required for `DYNAMIC` segments; ignored for `STATIC` ones."},"trackMembership":{"type":"boolean","default":false,"description":"Emit `segment.<slug>.entry` / `segment.<slug>.exit` events as contacts join and leave. These can be used as workflow triggers."}}},"examples":{"simple":{"summary":"Premium users","value":{"name":"Premium Users","condition":{"logic":"AND","groups":[{"filters":[{"field":"data.plan","operator":"equals","value":"premium"}]}]},"trackMembership":true}},"orLogic":{"summary":"Either of two groups (OR)","description":"Filters inside a group are ANDed; `logic` combines the groups. This matches premium users OR anyone who signed up in the last 7 days.","value":{"name":"Premium or new","description":"Targets for the onboarding push","condition":{"logic":"OR","groups":[{"filters":[{"field":"data.plan","operator":"equals","value":"premium"},{"field":"subscribed","operator":"equals","value":true}]},{"filters":[{"field":"createdAt","operator":"within","value":7,"unit":"days"}]}]}}},"eventBased":{"summary":"Based on a tracked event","description":"Contacts who triggered `purchase` in the last 30 days.","value":{"name":"Recent purchasers","condition":{"logic":"AND","groups":[{"filters":[{"field":"purchase","operator":"triggeredWithin","value":30,"unit":"days"}]}]}}},"static":{"summary":"Static segment","value":{"name":"Beta testers","type":"STATIC"}}}}}},"responses":{"201":{"description":"Segment created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Segment"}}}},"400":{"description":"`name` is missing, or `condition` is missing on a `DYNAMIC` segment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegacyError"},"example":{"error":"Condition is required and must be an object for DYNAMIC segments"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}},"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":{}}