Skip to content

Emails API

Managing email templates plus sending, previewing and logging emails to candidates. See API Conventions for shared rules.

Endpoints

There are three base paths.

/api/v1/email-templates (system-wide templates)

Method Path Summary Role
GET / List system templates viewer

/api/v1/projects/{projectId}/email-templates (project templates)

Method Path Summary Role
GET / List project templates viewer
POST / Create a template member
PATCH /{templateId} Update a template member
DELETE /{templateId} Delete a template member
POST /preview Preview a template member
POST /send Send using a template member

/api/v1/projects/{projectId}/emails (sending and logs)

Method Path Summary Role
GET /logs List email logs viewer
POST /send Bulk send emails member
POST /preview Preview an email member

All endpoints require Authorization: Bearer <access_token>; writes require member or above.

/email-templates/send and /emails/send are identical

Preview and send exist under both paths with the same input and output; either works.

The EmailTemplate Object

Field Type Description
id integer Template ID
projectId integer | null System-wide when NULL
type enum invitation / reminder / confirmation / cancellation
name string Template name
subject string Subject (supports placeholders)
body string Body (supports placeholders)
createdAt / updatedAt string(date-time) Creation / update timestamps

Placeholders

Subject and body may embed placeholders for substitution.

Placeholder Substituted value
{{candidate_name}} The candidate's name
{{project_name}} The project name

The set of usable placeholders follows the implementation in services/email.ts. Use "preview" to check the rendered result.


List System Templates

Returns templates whose project_id is NULL (shared by all projects).

URI

GET /api/v1/email-templates

Response (200 OK)

data is an array of EmailTemplate (not paginated).

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Server internal error 500 Internal Server Error

List Project Templates

URI

GET /api/v1/projects/{projectId}/email-templates

Response (200 OK)

data is an array of EmailTemplate.

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": [
    {
      "id": 1,
      "projectId": 1,
      "type": "invitation",
      "name": "First invitation",
      "subject": "[{{project_name}}] Interview invitation",
      "body": "Dear {{candidate_name}},\n\nThank you for...",
      "createdAt": "2026-01-15T09:00:00Z",
      "updatedAt": "2026-01-15T09:00:00Z"
    }
  ],
  "path": "/api/v1/projects/1/email-templates",
  "method": "GET"
}

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Project does not exist 404 Not Found

Create Template

URI

POST /api/v1/projects/{projectId}/email-templates

Request Body

{
  "type": "invitation",
  "name": "First invitation",
  "subject": "[{{project_name}}] Interview invitation",
  "body": "Dear {{candidate_name}},\n\nThank you for your cooperation."
}

Validation Rules

Field Rules
type Required; invitation / reminder / confirmation / cancellation
name Required, 1โ€“255 characters
subject Required, 1โ€“500 characters
body Required, at least 1 character

Response (201 Created)

data holds the created EmailTemplate.

Errors

Description Status code Status name
Validation error 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Project does not exist 404 Not Found

Update Template

URI

PATCH /api/v1/projects/{projectId}/email-templates/{templateId}

Path Parameters

Field Rules
templateId Required positive integer

Request Body

All creation fields become optional.

{
  "subject": "[{{project_name}}] About your interview slot"
}

Response (200 OK)

data holds the updated EmailTemplate.

Errors

Description Status code Status name
Validation error 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Template does not exist 404 Not Found

Delete Template

URI

DELETE /api/v1/projects/{projectId}/email-templates/{templateId}

Response (204 No Content)

No body.

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Template does not exist 404 Not Found

Note

Because email_logs.template_id has no FK constraint, delivery logs survive template deletion.


Preview Email

Renders the template for a single candidate. Nothing is sent.

URI

POST /api/v1/projects/{projectId}/email-templates/preview
POST /api/v1/projects/{projectId}/emails/preview

Request Body

{
  "templateId": 1,
  "candidateId": 12
}

Validation Rules

Field Rules
templateId Required positive integer
candidateId Required positive integer (project_candidates.id)

Response (200 OK)

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": {
    "subject": "[Spring 2026 User Interviews] Interview invitation",
    "body": "Dear Taro Yamada,\n\nThank you for your cooperation.",
    "to": "yamada@example.com"
  },
  "path": "/api/v1/projects/1/emails/preview",
  "method": "POST"
}

Errors

Description Status code Status name
Validation error 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Template or candidate does not exist 404 Not Found

Bulk Send Emails

URI

POST /api/v1/projects/{projectId}/email-templates/send
POST /api/v1/projects/{projectId}/emails/send

Request Body

{
  "templateId": 1,
  "candidateIds": [12, 34, 56]
}

Validation Rules

Field Rules
templateId Required positive integer
candidateIds Required array of positive integers, at least one

Response (200 OK)

The result carries success / failure counts plus details for each failure.

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": {
    "sent_count": 5,
    "failed_count": 1,
    "errors": [
      { "candidateId": 56, "email": "invalid@example", "message": "Invalid email address" }
    ]
  },
  "path": "/api/v1/projects/1/emails/send",
  "method": "POST"
}

This response is snake_case

sent_count / failed_count are not camelCase.

Errors

Description Status code Status name
Validation error (e.g. empty candidateIds) 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Template or project does not exist 404 Not Found
Delivery failure (EMAIL_SEND_ERROR) 500 Internal Server Error

Side Effects

  • Each send records the rendered subject and body in email_logs
  • Sending an invitation issues a scheduling token (scheduling_tokens) and substitutes its URL into the body
  • Candidate status may transition as a result

PROTOTYPE_MODE

While PROTOTYPE_MODE=true (the default), no email is actually sent. The decision goes through isEmailEnabled().


List Email Logs

URI

GET /api/v1/projects/{projectId}/emails/logs

Query Parameters

Field Type Required Default Rules
page integer - 1 Positive integer
limit integer - 20 1โ€“100
sort string - - -

Response (200 OK)

data is an array of EmailLog, with pagination.

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": [
    {
      "id": 1,
      "projectCandidateId": 12,
      "templateId": 1,
      "subject": "Interview invitation",
      "body": "Dear Taro Yamada...",
      "sentAt": "2026-01-15T09:00:00Z",
      "status": "sent",
      "sentBy": 1,
      "candidateName": "Taro Yamada",
      "candidateEmail": "yamada@example.com",
      "senderName": "Admin"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "totalCount": 42, "totalPages": 3, "hasNext": true, "hasPrevious": false },
  "path": "/api/v1/projects/1/emails/logs",
  "method": "GET"
}
Field Type Description
id integer Log ID
projectCandidateId integer Recipient candidate
templateId integer | null Template used
subject / body string Rendered subject and body
sentAt string(date-time) Sent at
status enum sent / delivered / bounced / failed
sentBy integer User who triggered the send
candidateName / candidateEmail string | null Candidate details (joined)
senderName string | null Sender name (joined)

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Project does not exist 404 Not Found

For a single candidate's history, use GET /{candidateId}/email-logs in the Candidates API.