Skip to content

Latest commit

History

History
1057 lines (679 loc) · 22.3 KB

File metadata and controls

1057 lines (679 loc) · 22.3 KB

API Reference

Auto-generated from Zod schemas. Run npm run docs:api to regenerate.

Authentication

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; pass X-Workspace-Id header 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 Format

{ "error": "message", "details": ["field-level errors (optional)"] }

Contacts

GET /api/contacts

List contacts with search, pagination, and sorting. No auth required

Response: { contacts: Contact[], total: number, page: number, pageSize: number }

POST /api/contacts

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 /api/contacts/:id

Get a single contact by ID. No auth required

Response: Contact

Errors:404 not found

PUT /api/contacts/:id

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 /api/contacts/:id

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

POST /api/contacts/import

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

GET /api/contacts/:id/interactions

List interactions for a contact (paginated). No auth required

Response: { interactions: Interaction[], total, page, pageSize }

Errors:404 contact not found

POST /api/contacts/:id/interactions

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

GET /api/contacts/:id/tags

List tags for a contact. No auth required

Response: Tag[]

Errors:404 contact not found

POST /api/contacts/:id/tags

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

DELETE /api/contacts/:id/tags

Remove a tag from a contact. No auth required

Request body (ContactTagInput):

{
"tagId": string (uuid)
}

Response: Tag[] (updated list)

Errors:400 validation

GET /api/contacts/tags?ids=uuid1,uuid2

Get tags for multiple contacts (batch). No auth required

Response: Record<contactId, Tag[]>

Interactions

DELETE /api/interactions/:id

Delete an interaction. No auth required

Response: { success: true }

Errors:404 not found

Tags

GET /api/tags

List all tags. No auth required

Response: Tag[]

POST /api/tags

Create a tag. No auth required

Request body (CreateTagInput):

{
"name": string,
"color": string | null?
}

Response: Tag (201)

Errors:400 validation, 409 duplicate name

PUT /api/tags/:id

Update a tag. No auth required

Request body (UpdateTagInput):

{
"name": string?,
"color": string | null?
}

Response: Tag

Errors:404 not found

DELETE /api/tags/:id

Delete a tag. No auth required

Response: { success: true }

Errors:404 not found

Fields

GET /api/fields

List all custom field definitions. No auth required

Response: FieldDefinition[]

POST /api/fields

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

PUT /api/fields/:id

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 /api/fields/:id

Delete a field definition. No auth required

Response: { success: true }

Errors:404 not found

Segments

GET /api/segments

List all segments. Auth: Session

Response: Segment[]

POST /api/segments

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 /api/segments/:id

Get a single segment. Auth: Session

Response: Segment

Errors:404 not found

PUT /api/segments/:id

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 /api/segments/:id

Delete a segment. Auth: Admin

Response: { success: true }

Errors:404 not found

POST /api/segments/preview

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

Campaigns

GET /api/campaigns

List all campaigns. Auth: Session

Response: Campaign[]

POST /api/campaigns

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 /api/campaigns/:id

Get a single campaign. Auth: Session

Response: Campaign

Errors:404 not found

PUT /api/campaigns/:id

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 /api/campaigns/:id

Delete a campaign. Auth: Admin

Response: { success: true }

Errors:404 not found

POST /api/campaigns/:id/send

Queue a draft campaign for sending. Auth: Admin

Response: { queued: true, message: "Campaign queued for sending." }

Errors:400 already sent, 404 not found

POST /api/campaigns/:id/preview

Send a preview email for a campaign. Auth: Admin

Request body (CampaignPreviewInput):

{
"email": string (email)
}

Response: { success: true }

Errors:400 validation/send error

GET /api/campaigns/:id/emails

List email records for a campaign. Auth: Session

Response: Email[]

GET /api/campaigns/unsubscribe-status

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
}

Scripts

GET /api/scripts

List all scripts. Auth: Admin

Response: Script[]

POST /api/scripts

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 /api/scripts/:id

Get a single script. Auth: Admin

Response: Script

Errors:404 not found

PUT /api/scripts/:id

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 /api/scripts/:id

Delete a script. Auth: Admin

Response: { ok: true }

Errors:404 not found

POST /api/scripts/:id/run

Execute a script manually. Auth: Admin

Response: ScriptRun

GET /api/scripts/:id/runs

Get run history for a script. Auth: Admin

Response: ScriptRun[]

Automations

GET /api/automations

List all automation rules. Auth: Admin

Response: AutomationRule[]

POST /api/automations

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 /api/automations/:id

Get a single automation rule. Auth: Admin

Response: AutomationRule

Errors:404 not found

PUT /api/automations/:id

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 /api/automations/:id

Delete an automation rule. Auth: Admin

Response: { success: true }

Errors:404 not found

POST /api/automations/:id/run

Execute an automation rule now. Auth: Admin

Response: { affected: number }

Errors:404 not found

Users

GET /api/users

List all users. Auth: Admin

Response: { id, name, email, image, isAdmin }[]

PUT /api/users/:id

Update user role. Auth: Admin

Request body (UpdateUserInput):

{
"role": "admin" | "member" | "viewer"
}

Response: { id, name, email, isAdmin }

Errors:400 validation, 404 not found

Api-keys

GET /api/api-keys

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 }[]

POST /api/api-keys

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.

DELETE /api/api-keys/:id

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

Support-tickets

GET /api/support-tickets

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 } }

POST /api/support-tickets

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 /api/support-tickets/:id

Get a ticket with replies, vote status, and subscription status. Auth: Session

Response: { ticket: Ticket & { hasVoted }, replies: Reply[], isSubscribed }

Errors:404 not found

PUT /api/support-tickets/:id

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 /api/support-tickets/:id

Delete a ticket and all its replies (admin only). Auth: Admin

Response: { success: true }

Errors:403 not admin, 404 not found

GET /api/support-tickets/:id/replies

List replies for a ticket. Auth: Session

Response: Reply[]

Errors:404 not found

POST /api/support-tickets/:id/replies

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

POST /api/support-tickets/:id/vote

Toggle upvote on a ticket (one vote per user). Auth: Session

Response: { upvoted: boolean, upvoteCount: number }

POST /api/support-tickets/:id/subscribe

Subscribe to email notifications for a ticket. Auth: Session

Response: { subscribed: true }

DELETE /api/support-tickets/:id/subscribe

Unsubscribe from email notifications for a ticket. Auth: Session

Response: { subscribed: false }

GET /api/support-tickets/unsubscribe

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 /api/support-tickets/subscribe-all

Get current user's global subscription status for all tickets. Auth: Session

Response: { subscribedToAll: boolean }

POST /api/support-tickets/subscribe-all

Toggle global subscription to all ticket notifications. Auth: Session

Response: { subscribedToAll: boolean }

GET /api/support-tickets/stats

Get ticket counts by status (global, cross-workspace). Auth: Session

Response: { open: number, in_progress: number, resolved: number, closed: number }

Webhooks

POST /api/webhooks/mailersend

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 }

POST /api/webhooks/tally

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

Gmail OAuth

GET /api/auth/gmail

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

GET /api/auth/gmail/callback

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

Email Connections

GET /api/email-connections

List the current user's email connections. Auth: Session

Response: EmailConnection[]

DELETE /api/email-connections/:id

Disconnect an email connection and revoke the OAuth token. Auth: Session

Response: { success: true }

Errors:404 not found, 403 not owner

PUT /api/email-connections/:id/settings

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

GET /api/email-connections/:id/contacts

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

POST /api/email-connections/:id/contacts/import

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

POST /api/email-connections/:id/refresh

Trigger a manual email sync (enqueues a worker job). Auth: Session

Response: { queued: true }

Errors:404 not found, 403 not owner

Email Contact Settings

GET /api/email-contact-settings

List per-contact sync settings for the current user's email connections. Auth: Session

Response: EmailContactSetting[]

PUT /api/email-contact-settings/:contactId

Update sync settings for a specific contact. Auth: Session

Request body:

{
"syncInteractions": boolean?,
"interactionsVisible": boolean?
}

Response: EmailContactSetting

Errors:404 not found

PUT /api/email-contact-settings

Bulk update per-contact sync settings. Auth: Session

Request body:

{
"settings": {
"contactId": string (uuid),
"syncInteractions": boolean?,
"interactionsVisible": boolean?
}[]
}

Response: { updated: number }

Errors:400 validation

Sandbox (Email Testing)

All sandbox endpoints require admin role and only function when EMAIL_MODE=sandbox. Returns 404 in live mode.

GET /api/sandbox/status

Check current email mode. No auth required.

Response:{ mode: "sandbox" | "live" }

GET /api/sandbox/emails

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 /api/sandbox/emails/:id

Get a single sandbox email with full detail (rendered HTML body, headers, etc.). Admin required.

Response: Full SandboxEmail object

POST /api/sandbox/emails/:id/simulate

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

POST /api/sandbox/emails/simulate-bulk

Simulate an event on all matching sandbox emails. Admin required.

Body:{ filter: { campaignId?, to?, workspaceId?, status?, since? }, event: string }

Response:{ affected: number }

DELETE /api/sandbox/emails

Clear sandbox emails. Admin required.

Body (optional):{ campaignId: string } — scope deletion to one campaign, or omit to clear everything.

Response:{ deleted: number }


Appendix: Segment Filter Schema

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