# Email messages

URL: https://better-helpdesk.com/docs/email/messages/

Every HelpdeskEmail kind the email adapter receives, its fields, and when it is sent.



The `email.send(message)` adapter receives a `HelpdeskEmail`, a union of six kinds told apart by `kind`. The package decides when to send and to whom; the adapter decides how the message looks and how it is delivered. Every message is sent from a job, so it goes out when [`runJobs`](/docs/reference/routes#jobs) runs, and a throw from `send` retries the job. Related guide: [Send email](/docs/email/send).

| `kind`             | To                                             | Sent when                                                                                   |
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `customer-reply`   | The customer                                   | An agent posts a reply that is not internal                                                 |
| `customer-receipt` | The customer                                   | A conversation opens in an inbox with `receipt: true`                                       |
| `agent-new`        | Agents                                         | A conversation opens, or a customer writes on a resolved one                                |
| `agent-reminder`   | Agents                                         | A customer has waited longer than the inbox's `reminderAfterHours`                          |
| `agent-mention`    | The agents picked                              | An agent picks teammates with `@` in an internal note                                       |
| `agent-sla`        | The assignee or the team, and every supervisor | A share of a response, resolution or next-reply target has passed, or the target was missed |

Every kind has these fields:

<TypeTable
  type="{
  kind: { type: 'string', required: true, description: 'Which of the six messages this is.' },
  to: { type: 'string', required: true, description: 'One recipient address.' },
  locale: {
    type: &#x22;'en' | 'de' | 'fr' | 'it'&#x22;,
    required: true,
    description: <>For customer mail, the contact's locale; anything starting with <code>de</code> is German, with <code>fr</code> French and with <code>it</code> Italian. Agent mail is always <code>en</code>.</>,
  },
  reference: { type: 'string', required: true, description: <>The conversation's reference, such as <code>ACME-1042</code>.</> },
}"
/>

## customer-reply [#customer-reply]

<TypeTable
  type="{
  body: { type: 'string', required: true, description: &#x22;The agent's reply as written, with its Markdown formatting.&#x22; },
  agentName: { type: 'string', required: true, description: <>The replying agent's name, or <code>''</code>.</> },
  subject: {
    type: 'string',
    description: &#x22;What the customer called the conversation. Absent when the conversation's first customer message is unverified.&#x22;,
  },
  replyTo: { type: 'string', description: <><code>replyToAddress(reference)</code>, when configured.</> },
  inReplyTo: { type: 'string', description: 'The Message-ID of the latest inbound email in the conversation, for threading.' },
  ratingLinks: {
    type: '{ good: string; bad: string; scores?: Record<1 | 2 | 3 | 4 | 5, string> }',
    description: <>Present when the conversation is resolved, not yet rated and not merged. With <code>ratingScale: 5</code>, <code>scores</code> holds one link per score, and <code>good</code> and <code>bad</code> are the links for 5 and 1.</>,
  },
}"
/>

The job is queued two minutes after the reply and skipped when the customer has seen the reply in the widget by then, or the contact has no email address.

Each rating link opens `{basePath}/rate/` on the origin of `adminUrl`, with a signed query string, and is valid for 30 days. The page asks for confirmation with a button, so a mail scanner that follows the link records nothing. A `bad` rating opens the conversation again. The signing key is generated on first use and stored in the `setting` table.

## customer-receipt [#customer-receipt]

<TypeTable
  type="{
  responderName: { type: 'string', description: 'Set when exactly one agent has been active in the last 30 days; the receipt is signed by them.' },
  responderTitle: { type: 'string', description: <>That agent's title from <code>agentTitles</code>, in the message's locale.</> },
  responderAvatarUrl: { type: 'string', description: &#x22;That agent's avatar URL.&#x22; },
  awayUntil: {
    type: 'string',
    description: 'ISO timestamp. Set when the most recently active agents, up to three, are all away: the earliest of their returns.',
  },
  backOn: {
    type: 'string',
    description: <>ISO timestamp of when the team answers again. For an inbox with <code>hours</code>, its next opening while it is closed; otherwise the next weekday after <code>awayUntil</code>.</>,
  },
  bookingUrl: { type: 'string', description: <>The inbox's <code>bookingLink(reference)</code>, or its <code>bookingUrl</code>.</> },
  replyTo: { type: 'string', description: <><code>replyToAddress(reference)</code>, when configured.</> },
  respondBy: { type: 'string', description: 'ISO timestamp of when the team aims to answer by, when the conversation falls under a response target.' },
}"
/>

Receipts are capped at 200 an hour across the whole helpdesk.

## agent-new and agent-reminder [#agent-new-and-agent-reminder]

Both kinds share one shape.

<TypeTable
  type="{
  subject: {
    type: 'string',
    required: true,
    description: &#x22;The conversation's title, else its subject, else the first 80 characters of the customer's first message.&#x22;,
  },
  body: {
    type: 'string',
    required: true,
    description: &#x22;The customer's newest message, at most 1,000 characters. For a reopened conversation, the message that reopened it.&#x22;,
  },
  url: { type: 'string', required: true, description: <>The conversation in the agent UI, <code>{'{adminUrl}'}/conversations/{'{id}'}/</code>.</> },
  reopened: { type: 'boolean', description: <><code>true</code> on an <code>agent-new</code> sent because a customer wrote on a resolved conversation.</> },
}"
/>

Recipients are the agents who have an email address, have not been removed and have opened the agent UI in the last 30 days. When the conversation's assignee is one of them, only the assignee. Notices of new conversations are capped at 200 an hour, counted apart for the host's signed-in users and for everyone else.

An `agent-reminder` goes once per wait, from `runJobs`, and not for a conversation that is resolved, snoozed or from a blocked contact.

## agent-mention [#agent-mention]

<TypeTable
  type="{
  subject: { type: 'string', required: true, description: <>As for <code>agent-new</code>.</> },
  body: { type: 'string', required: true, description: 'The internal note, at most 1,000 characters.' },
  url: { type: 'string', required: true, description: 'The conversation in the agent UI.' },
  authorName: { type: 'string', required: true, description: <>The note's author: their name, else their email, else <code>''</code>.</> },
}"
/>

Sent to each agent picked in the note who has an email address and has not been removed from the team, except the author.

## agent-sla [#agent-sla]

<TypeTable
  type="{
  subject: { type: 'string', required: true, description: <>As for <code>agent-new</code>.</> },
  body: { type: 'string', required: true, description: &#x22;The customer's newest message, at most 1,000 characters.&#x22; },
  url: { type: 'string', required: true, description: 'The conversation in the agent UI.' },
  target: { type: &#x22;'response' | 'resolution' | 'reply'&#x22;, required: true },
  stage: { type: &#x22;'warning' | 'breach'&#x22;, required: true, description: <><code>warning</code> once the configured share of the target has passed, <code>breach</code> once the deadline has.</> },
  due: { type: 'string', required: true, description: 'The deadline, as an ISO timestamp.' },
}"
/>

Sent to the assignee, or to the whole team when there is none, and to every supervisor, under the same conditions as `agent-new`. At most one warning and one breach go out per target per conversation; a new tier or priority that moves the deadline into the future starts both over.

## Recorded sends [#recorded-sends]

After a send, the conversation's timeline gets an `email.sent` row in [`conversation_event`](/docs/database/tables#conversation_event) with the kind and recipients. A reopened `agent-new` is recorded as `agent-reopened`.
