Auto-generated from Zod schemas. Run
npm run docs:apito regenerate.
Two auth methods are supported:
- Session cookie (browser): Managed by NextAuth with Google OAuth
- API key (machine-to-machine):
Authorization: Bearer pai_<key>— keys are workspace-scoped (tied to the workspace where they were created). The key inherits the creator's role; passX-Workspace-Idheader to specify workspace context per-request, same as session auth.
Auth levels:
- No auth: Public endpoint
- Session: Any authenticated user
- Admin: Requires admin role (returns 403 otherwise)
- Workspace Admin: Requires admin effective role in the specified workspace
{ "error": "message", "details": ["field-level errors (optional)"] }List contacts with search, pagination, and sorting. No auth required
Response: { contacts: Contact[], total: number, page: number, pageSize: number }
Create a new contact. No auth required
Request body (CreateContactInput):
{
"email": string (email) | null?,
"firstName": string | null?,
"lastName": string | null?,
"customFields": Record<string, string>
}
Response: Contact (201)
Errors: 400 validation, 409 duplicate email
Get a single contact by ID. No auth required
Response: Contact
Errors: 404 not found
Update a contact (partial update). No auth required
Request body (UpdateContactInput):
{
"email": string (email) | null?,
"firstName": string | null?,
"lastName": string | null?,
"language": string | null?,
"globallyUnsubscribed": boolean?,
"customFields": Record<string, string>?,
"communicationPreferences": Record<string, string>?
}
Response: Contact
Errors: 400 validation, 404 not found
Delete a contact. Requires workspace admin role. Global admins delete the contact globally; workspace admins remove it from their workspace only (the contact record is deleted if it has no remaining workspace links).
Auth: Workspace admin (via X-Workspace-Id header)
Response: { success: true }
Errors: 403 forbidden, 404 not found
Bulk import contacts from CSV data. No auth required
Request body (ImportContactsInput):
{
"rows": Record<string, string>[],
"mapping": Record<string, string>,
"constantValues": Record<string, unknown>, // optional — fixed values applied to all rows (e.g. {"_tags": ["csv-import"]})
"skipDuplicates": boolean
}
Response: { total, created, updated, skipped, errors: [{row, error}] }
Errors: 400 validation
List interactions for a contact (paginated). No auth required
Response: { interactions: Interaction[], total, page, pageSize }
Errors: 404 contact not found
Create an interaction for a contact. No auth required
Request body (CreateInteractionInput):
{
"type": "email_sent" | "email_received" | "call" | "meeting" | "note" | "form_submission" | "event_attended" | "action_taken" | "stage_change",
"subject": string | null?,
"body": string | null?,
"occurredAt": string (datetime)?,
"metadata": Record<string, string>
}
Response: Interaction (201)
Errors: 400 validation, 404 contact not found
List tags for a contact. No auth required
Response: Tag[]
Errors: 404 contact not found
Add a tag to a contact. No auth required
Request body (ContactTagInput):
{
"tagId": string (uuid)
}
Response: Tag[] (updated list)
Errors: 400 validation, 404 contact not found
Remove a tag from a contact. No auth required
Request body (ContactTagInput):
{
"tagId": string (uuid)
}
Response: Tag[] (updated list)
Errors: 400 validation
Get tags for multiple contacts (batch). No auth required
Response: Record<contactId, Tag[]>
Delete an interaction. No auth required
Response: { success: true }
Errors: 404 not found
List all tags. No auth required
Response: Tag[]
Create a tag. No auth required
Request body (CreateTagInput):
{
"name": string,
"color": string | null?
}
Response: Tag (201)
Errors: 400 validation, 409 duplicate name
Update a tag. No auth required
Request body (UpdateTagInput):
{
"name": string?,
"color": string | null?
}
Response: Tag
Errors: 404 not found
Delete a tag. No auth required
Response: { success: true }
Errors: 404 not found
List all custom field definitions. No auth required
Response: FieldDefinition[]
Create a custom field definition. No auth required
Request body (CreateFieldInput):
{
"name": string,
"label": string,
"fieldType": "text" | "number" | "date" | "select" | "multiselect" | "boolean" | "url" | "email",
"options": string[] | null?,
"sortOrder": number,
"scope": "core" | "global_internal" | "workspace"?,
"workspaceId": string (uuid) | null?
}
Response: FieldDefinition (201)
Errors: 400 validation, 409 duplicate name
Update a field definition. No auth required
Request body (UpdateFieldInput):
{
"label": string?,
"fieldType": "text" | "number" | "date" | "select" | "multiselect" | "boolean" | "url" | "email"?,
"options": string[] | null?,
"sortOrder": number?
}
Response: FieldDefinition
Errors: 404 not found
Delete a field definition. No auth required
Response: { success: true }
Errors: 404 not found
List all segments. Auth: Session
Response: Segment[]
Create a segment. Auth: Admin
Request body (CreateSegmentInput):
{
"name": string,
"description": string | null?,
"filter": {
"match": "all" | "any",
"conditions": {
"field": string,
"operator": string,
"value": unknown
}[]
},
"crossWorkspace": boolean?
}
Response: Segment (201)
Errors: 400 validation
Get a single segment. Auth: Session
Response: Segment
Errors: 404 not found
Update a segment. Auth: Admin
Request body (UpdateSegmentInput):
{
"name": string?,
"description": string | null?,
"filter": {
"match": "all" | "any",
"conditions": {
"field": string,
"operator": string,
"value": unknown
}[]
}?,
"crossWorkspace": boolean?
}
Response: Segment
Errors: 404 not found
Delete a segment. Auth: Admin
Response: { success: true }
Errors: 404 not found
Preview contacts matching a segment filter. Auth: Session
Request body (SegmentPreviewInput):
{
"filter": {
"match": "all" | "any",
"conditions": {
"field": string,
"operator": string,
"value": unknown
}[]
}
}
Response: { count: number, contacts: { id, email, first_name, last_name }[] }
Errors: 400 validation
List all campaigns. Auth: Session
Response: Campaign[]
Create a campaign. Auth: Admin
Request body (CreateCampaignInput):
{
"name": string, // max 200 chars
"subject": string, // max 998 chars (RFC 2822)
"body": string, // max 500,000 chars
"fromName": string | null?,
"fromEmail": string (email) | null?,
"segmentId": string (uuid) | null?,
"categoryId": string (uuid) | null?,
"scheduledAt": string (datetime) | null?,
"allowNoUnsubscribe": boolean?
}
Response: Campaign (201)
Errors: 400 validation
Get a single campaign. Auth: Session
Response: Campaign
Errors: 404 not found
Update a campaign. Auth: Admin
Request body (UpdateCampaignInput):
{
"name": string?,
"subject": string?,
"body": string?,
"fromName": string | null?,
"fromEmail": string (email) | null?,
"segmentId": string (uuid) | null?,
"categoryId": string (uuid) | null?,
"scheduledAt": string (datetime) | null?,
"allowNoUnsubscribe": boolean?
}
Response: Campaign
Errors: 404 not found
Delete a campaign. Auth: Admin
Response: { success: true }
Errors: 404 not found
Queue a draft campaign for sending. Auth: Admin
Response: { queued: true, message: "Campaign queued for sending." }
Errors: 400 already sent, 404 not found
Send a preview email for a campaign. Auth: Admin
Request body (CampaignPreviewInput):
{
"email": string (email)
}
Response: { success: true }
Errors: 400 validation/send error
List email records for a campaign. Auth: Session
Response: Email[]
Check whether the server-side unsubscribe infrastructure is configured. Used by the campaign editor to warn when no unsubscribe mechanism is available for categorized campaigns. Auth: Admin
Response:
{
"secretConfigured": boolean,
"listUnsubscribeEnabled": boolean
}
List all scripts. Auth: Admin
Response: Script[]
Create a script. Auth: Admin
Request body (CreateScriptInput):
{
"name": string,
"description": string | null?,
"code": string,
"cronSchedule": string | null?
}
Response: Script (201)
Errors: 400 validation
Get a single script. Auth: Admin
Response: Script
Errors: 404 not found
Update a script. Auth: Admin
Request body (UpdateScriptInput):
{
"name": string?,
"description": string | null?,
"code": string?,
"cronSchedule": string | null?,
"enabled": boolean?
}
Response: Script
Errors: 404 not found
Delete a script. Auth: Admin
Response: { ok: true }
Errors: 404 not found
Execute a script manually. Auth: Admin
Response: ScriptRun
Get run history for a script. Auth: Admin
Response: ScriptRun[]
List all automation rules. Auth: Admin
Response: AutomationRule[]
Create an automation rule. Auth: Admin
Request body (CreateAutomationInput):
{
"name": string,
"description": string | null?,
"config": {
"match": "all" | "any",
"conditions": {
"field": string,
"operator": string,
"value": unknown
}[],
"actions": {
"type": "set_field",
"field": string,
"value": unknown
} | {
"type": "add_tag",
"tag": string
} | {
"type": "remove_tag",
"tag": string
}[]
}
}
Response: AutomationRule (201)
Errors: 400 validation
Get a single automation rule. Auth: Admin
Response: AutomationRule
Errors: 404 not found
Update an automation rule. Auth: Admin
Request body (UpdateAutomationInput):
{
"name": string?,
"description": string | null?,
"config": {
"match": "all" | "any",
"conditions": {
"field": string,
"operator": string,
"value": unknown
}[],
"actions": {
"type": "set_field",
"field": string,
"value": unknown
} | {
"type": "add_tag",
"tag": string
} | {
"type": "remove_tag",
"tag": string
}[]
}?,
"isActive": boolean?
}
Response: AutomationRule
Errors: 404 not found
Delete an automation rule. Auth: Admin
Response: { success: true }
Errors: 404 not found
Execute an automation rule now. Auth: Admin
Response: { affected: number }
Errors: 404 not found
List all users. Auth: Admin
Response: { id, name, email, image, isAdmin }[]
Update user role. Auth: Admin
Request body (UpdateUserInput):
{
"role": "admin" | "member" | "viewer"
}
Response: { id, name, email, isAdmin }
Errors: 400 validation, 404 not found
List API keys for the current workspace, with creator info. Auth: Workspace Admin (requires X-Workspace-Id header)
Response: { id, name, keyPrefix, userId, workspaceId, isActive, lastUsedAt, createdAt, creatorName, creatorEmail }[]
Create an API key scoped to the current workspace (raw key returned once). Auth: Workspace Admin (requires X-Workspace-Id header)
Request body (CreateApiKeyInput):
{
"name": string
}
Response: { id, name, keyPrefix, workspaceId, rawKey, createdAt } (201)
Errors: 400 validation / missing workspace
Workspace scoping: API keys are scoped to the workspace in which they are created. When a request uses an API key without an explicit X-Workspace-Id header, it defaults to the key's creation workspace. Keys inherit the creator's effective role in each workspace.
Revoke an API key. Must be a workspace admin in the key's workspace. Auth: Workspace Admin
Response: { success: true }
Errors: 403 forbidden, 404 not found
List all support tickets with optional filters, sorting by newest or most voted. Auth: Session
Response: { tickets: Ticket[], total, stats: { open, in_progress, resolved, closed } }
Create a support ticket (creator is auto-subscribed to notifications). Auth: Session
Request body (CreateTicketInput):
{
"title": string,
"description": string,
"type": "bug" | "feature",
"priority": "low" | "medium" | "high"?
}
Response: Ticket (201)
Errors: 400 validation
Get a ticket with replies, vote status, and subscription status. Auth: Session
Response: { ticket: Ticket & { hasVoted }, replies: Reply[], isSubscribed }
Errors: 404 not found
Update a ticket (admin: status/priority/type; owner: title/desc on open tickets). Auth: Session
Request body (UpdateTicketInput):
{
"title": string?,
"description": string?,
"type": "bug" | "feature"?,
"status": "open" | "in_progress" | "resolved" | "closed"?,
"priority": "low" | "medium" | "high"?
}
Response: Ticket
Errors: 400 validation, 403 not authorized, 404 not found
Delete a ticket and all its replies (admin only). Auth: Admin
Response: { success: true }
Errors: 403 not admin, 404 not found
List replies for a ticket. Auth: Session
Response: Reply[]
Errors: 404 not found
Post a reply to a ticket (replier is auto-subscribed to notifications). Auth: Session
Request body (CreateTicketReplyInput):
{
"body": string
}
Response: Reply (201)
Errors: 400 validation, 404 not found
Toggle upvote on a ticket (one vote per user). Auth: Session
Response: { upvoted: boolean, upvoteCount: number }
Subscribe to email notifications for a ticket. Auth: Session
Response: { subscribed: true }
Unsubscribe from email notifications for a ticket. Auth: Session
Response: { subscribed: false }
One-click unsubscribe from ticket notifications via email link (HMAC token). No auth required
Response: HTML confirmation page
Errors: 400 missing params, 403 invalid token
Get current user's global subscription status for all tickets. Auth: Session
Response: { subscribedToAll: boolean }
Toggle global subscription to all ticket notifications. Auth: Session
Response: { subscribedToAll: boolean }
Get ticket counts by status (global, cross-workspace). Auth: Session
Response: { open: number, in_progress: number, resolved: number, closed: number }
Receive MailerSend delivery events (sent, delivered, bounced, opened, clicked). Auth: HMAC-SHA256 signature via signature header (verified against MAILERSEND_WEBHOOK_SIGNING_SECRET).
Response: { ok: true }
Receive Tally form submissions, creates/updates contacts and logs interactions. Auth: HMAC-SHA256 signature via tally-signature header (verified against TALLY_WEBHOOK_SIGNING_SECRET). New contacts are linked to the global workspace.
Response: { status: "created"|"updated", contactId }
Errors: 400 invalid JSON / no email
Initiate Gmail OAuth flow. Redirects the user to Google's consent screen requesting gmail.readonly scope. This is a separate OAuth flow from the login flow. Auth: Session
Response: Redirect to Google OAuth
Handle the Gmail OAuth callback. Exchanges the authorization code for tokens, encrypts them, and stores the email connection. Auth: Session
Response: Redirect to /dashboard/my-email-contacts
List the current user's email connections. Auth: Session
Response: EmailConnection[]
Disconnect an email connection and revoke the OAuth token. Auth: Session
Response: { success: true }
Errors: 404 not found, 403 not owner
Update default sync settings for an email connection. Auth: Session
Request body:
{
"syncInteractions": boolean?,
"interactionsVisible": boolean?
}
Response: EmailConnection
Errors: 404 not found, 403 not owner
List contacts from the user's Gmail with CRM match status (matched by email). Auth: Session
Response: { contacts: GmailContact[], total: number }
Errors: 404 not found, 403 not owner
Import Gmail contacts to the active workspace. Auth: Session
Request body:
{
"emails": string[]
}
Response: { imported: number, skipped: number }
Errors: 400 validation, 404 not found, 403 not owner
Trigger a manual email sync (enqueues a worker job). Auth: Session
Response: { queued: true }
Errors: 404 not found, 403 not owner
List per-contact sync settings for the current user's email connections. Auth: Session
Response: EmailContactSetting[]
Update sync settings for a specific contact. Auth: Session
Request body:
{
"syncInteractions": boolean?,
"interactionsVisible": boolean?
}
Response: EmailContactSetting
Errors: 404 not found
Bulk update per-contact sync settings. Auth: Session
Request body:
{
"settings": {
"contactId": string (uuid),
"syncInteractions": boolean?,
"interactionsVisible": boolean?
}[]
}
Response: { updated: number }
Errors: 400 validation
All sandbox endpoints require admin role and only function when EMAIL_MODE=sandbox. Returns 404 in live mode.
Check current email mode. No auth required.
Response: { mode: "sandbox" | "live" }
List captured sandbox emails with optional filters. Admin required.
Query params: campaignId, to (email), workspaceId, status, since (ISO timestamp), limit (default 50, max 500), offset
Response: { emails: SandboxEmail[], total: number }
Get a single sandbox email with full detail (rendered HTML body, headers, etc.). Admin required.
Response: Full SandboxEmail object
Simulate a recipient event on one sandbox email. Calls the same internal logic as the Mailersend webhook handler. Admin required.
Body: { event: "delivered" | "opened" | "clicked" | "bounced" | "complained" | "unsubscribed", url?: string }
Response: Updated SandboxEmail object
Simulate an event on all matching sandbox emails. Admin required.
Body: { filter: { campaignId?, to?, workspaceId?, status?, since? }, event: string }
Response: { affected: number }
Clear sandbox emails. Admin required.
Body (optional): { campaignId: string } — scope deletion to one campaign, or omit to clear everything.
Response: { deleted: number }
Used in segment creation and preview endpoints:
{
"match": "all" | "any",
"conditions": {
"field": string,
"operator": string,
"value": unknown
}[]
}
Supported operators: eq, neq, contains, not_contains, starts_with, gt, lt, gte, lte, in, has, not_has, is_set, is_not_set, is_empty (alias for is_not_set), is_not_empty (alias for is_set), after, before