# Send a welcome email and receive the replies

Use this guide when your app sends an email to one person after an event, such as a welcome message after signup, and you want your team to see the reply. Your server sends the email through Intray from a shared address such as support. The conversation stays out of the Todo list until the customer writes back. When they reply, the thread moves to Todo, and your team sees the email your app sent above the reply.

This is for email to one person about something they did in your product. It is not a tool for newsletters or campaigns. Read the [email use policy](/email-use) for what you may send.

<Note title="Sending is turned on per workspace during the pilot">
  Until Intray staff enable sending for your workspace, `GET /addresses` reports
  `can_send: false` with the reason "Sending limits are missing or sending is
  paused", and `POST /messages` returns `503 sending_not_configured`. Ask your
  Intray contact to enable it before you build on this guide.
</Note>

## Before you start

- A shared address in Intray on a verified domain, for example support.
- An API key with the `messages:send` permission that is allowed to use that address. A workspace owner creates it under **Settings**, then **Developers**. Choose **Email signups** as the primary use. The primary use is a label for your team and grants nothing on its own.
- Add `conversations:read` to the same key if you also want to [read delivery status](/docs/guide-delivery-status).
- Three values in your server's secret store. The key must never reach browser code or a mobile app bundle.

| Name                | Where it comes from                                                       |
| ------------------- | ------------------------------------------------------------------------- |
| `INTRAY_API_URL`    | **Settings**, then **Developers**. It already ends in `/v1`.              |
| `INTRAY_API_KEY`    | The same page, shown once when you create the key. Starts with `intray_`. |
| `INTRAY_ADDRESS_ID` | The `id` of the address from `GET /addresses`, in the first step below.   |

## Send the email

<Steps>
  <Step title="Find the address ID and check that it can send">
    Every send names its address by ID. List the addresses the key may use, and
    look at `can_send` for the one you want.

    <CodeGroup>

    ```sh title="curl"
    curl "$INTRAY_API_URL/addresses" \
      -H "Authorization: Bearer $INTRAY_API_KEY"
    ```

    ```ts title="TypeScript"
    const response = await fetch(`${process.env.INTRAY_API_URL}/addresses`, {
      headers: { Authorization: `Bearer ${process.env.INTRAY_API_KEY}` },
    });
    const { data } = await response.json();
    ```

    </CodeGroup>

    ```json title="200 OK"
    {
      "data": [
        {
          "id": "<address ID>",
          "address": "support@example.com",
          "name": "Example support",
          "status": "active",
          "domain_status": "active",
          "can_send": true,
          "sending_reason": null
        }
      ]
    }
    ```

    When `can_send` is false, `sending_reason` says why. The usual reasons are
    that the key lacks `messages:send`, that the address or its domain is not
    ready to send, or that sending is not enabled for the workspace yet. Save
    the `id` as `INTRAY_ADDRESS_ID`.

  </Step>
  <Step title="Send the email from your signup code">
    Call this from your server after the signup is saved. Build the
    `Idempotency-Key` from your own record's ID. If the call is retried with the
    same key and the same body, Intray returns the first response and does not
    send a second email.

    <CodeGroup>

    ```ts title="server/welcome-email.ts"
    // Server only. `user` comes from your saved signup record.
    export async function sendWelcomeEmail(user: {
      id: string;
      email: string;
      name: string;
    }) {
      const base = process.env.INTRAY_API_URL;
      const key = process.env.INTRAY_API_KEY;
      const addressId = process.env.INTRAY_ADDRESS_ID;
      if (!base || !key || !addressId) {
        throw new Error("Intray is not configured");
      }

      const response = await fetch(`${base}/messages`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${key}`,
          "Content-Type": "application/json",
          "Idempotency-Key": `welcome-${user.id}`,
        },
        body: JSON.stringify({
          address_id: addressId,
          to: [{ address: user.email, name: user.name }],
          subject: "Welcome to Example",
          text: `Hi ${user.name},\n\nThanks for signing up. Reply to this email if you need a hand, and someone on our team will answer.`,
          external_id: user.id,
        }),
      });

      const result = await response.json();
      if (!response.ok) {
        throw new Error(`${result.error.code}: ${result.error.message}`);
      }
      // Save both IDs on your signup record.
      return {
        messageId: result.message_id as string,
        conversationId: result.conversation.id as string,
      };
    }
    ```

    ```sh title="curl"
    curl "$INTRAY_API_URL/messages" \
      -H "Authorization: Bearer $INTRAY_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: welcome-user-123" \
      -d '{
        "address_id": "<address ID>",
        "to": [{ "address": "ada@example.com", "name": "Ada" }],
        "subject": "Welcome to Example",
        "text": "Thanks for signing up. Reply to this email if you need a hand.",
        "external_id": "user-123"
      }'
    ```

    </CodeGroup>

    Supply `text`, `html`, or both. `external_id` is your own reference, up to
    200 characters, and it does not deduplicate anything. The
    `Idempotency-Key` header is what prevents a duplicate send.

  </Step>
  <Step title="Store what comes back">
    A `202` response means Intray queued the email. It does not mean the email
    was delivered.

    ```json title="202 Accepted"
    {
      "message_id": "<message ID>",
      "conversation": { "id": "<conversation ID>", "...": "..." },
      "delivery": { "status": "queued" },
      "suppressed": []
    }
    ```

    Store `message_id` and `conversation.id` on your signup record. The API has
    no endpoint that lists conversations, so these IDs are how you find the
    thread again. Use `message_id` to
    [check delivery](/docs/guide-delivery-status).

  </Step>
</Steps>

## Where the conversation appears

A new email sent through the API starts in **Done**. A product can send many welcome emails in a day, and none of them need an answer from your team, so they stay out of Todo. Send `"initial_triage": "todo"` in the request when the team should see the conversation straight away.

When the customer replies, the reply joins the same thread and Intray moves the thread to **Todo**. Your team opens it and sees the email your app sent, followed by the reply. A reply that Intray filters as spam does not reopen the thread.

Your team answers from the inbox, and the reply goes out from the same shared address.

## The opt-out link

Intray adds an opt-out link to the end of every email sent through the API. In a plain text email it reads "Stop emails from this workspace", followed by the link. The email headers also support one-click unsubscribe in mail apps that offer it.

A recipient who opts out receives no further email from your workspace. That applies to every key and every address, and to email that was queued and not yet sent. A later `POST /messages` to that recipient returns `422`, and the message names the address.

## Check that it works

1. Sign up in your product with an email address you can read, or run the curl example with your own address in `to`.
2. Open the inbox and choose the **Done** view. The conversation is there, with your email as its only entry.
3. Reply to the email from your mail app.
4. The thread moves to **Todo** and shows the reply under the email your app sent.

## Pilot limits

- An email has one `to` recipient.
- `cc` and `bcc` are rejected when they are not empty.
- Attachments are not supported.
- The email goes out as the address in `address_id`. You cannot set a custom From or Reply-To.
- Batches, campaigns, and scheduled sends are not supported.

Email your team composes in the inbox is not affected by these limits.

## When it fails

Every error has the shape `{ "error": { "code", "message", "request_id" } }`. Include the `request_id` when you report a problem.

| Status and code                             | What happened                                                        | What to do                                                                                 |
| ------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `503 sending_not_configured`                | Sending is not enabled for the workspace yet                         | Ask your Intray contact to enable it. Retrying does not help until then.                   |
| `403 forbidden`                             | The key lacks `messages:send`                                        | Create a new key with that permission. Permissions on an existing key cannot be changed.   |
| `403 sending_paused`                        | Sending is paused, by staff or after recent bounces or complaints    | Stop sending and contact Intray.                                                           |
| `404 not_found`                             | The key is not allowed to use that address                           | Check `INTRAY_ADDRESS_ID` against `GET /addresses` for this key.                           |
| `409 address_disabled`                      | The address is disabled in Intray                                    | Ask a workspace owner to enable the address, or use another one.                           |
| `422`                                       | A field is invalid or unknown, or the recipient is suppressed        | Read `message`. It names the field, or the address that bounced, complained, or opted out. |
| `429 rate_limited`                          | More than 60 requests a minute for the key, or 600 for the workspace | Wait for the `Retry-After` header, then retry with the same `Idempotency-Key`.             |
| `429 send_limit` or `429 integration_limit` | The workspace or the key used up its sending capacity                | Retry later with the same `Idempotency-Key` and the same body.                             |
| `429 recipient_limit`                       | This recipient has had too many emails, or too many without a reply  | Do not retry. Wait for the recipient to reply before sending more.                         |
| `409 idempotency_conflict`                  | The same `Idempotency-Key` was used with a different body            | Use one key per signup, and send the same body on every retry.                             |

When a call times out or returns `429` or a `5xx` status, save a retry job and send the same `Idempotency-Key` and the same body after a delay. Do not create a new key for each attempt, because a new key can send a second email. The [Conversation API reference](/docs/conversation-api#retries) has the full retry rules.

## Next

<CardGroup cols={2}>
  <Card
    title="Check whether an email was delivered"
    href="/docs/guide-delivery-status"
  >
    Read the delivery status of the email you sent, and handle bounces.
  </Card>
  <Card
    title="Send a contact form to the inbox"
    href="/docs/guide-contact-form"
  >
    Turn a form submission into a conversation in Todo without sending email.
  </Card>
</CardGroup>
