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 for what you may send.

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.

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.
  • Three values in your server's secret store. The key must never reach browser code or a mobile app bundle.
NameWhere it comes from
INTRAY_API_URLSettings, then Developers. It already ends in /v1.
INTRAY_API_KEYThe same page, shown once when you create the key. Starts with intray_.
INTRAY_ADDRESS_IDThe id of the address from GET /addresses, in the first step below.

Send the email

1

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.

curl "$INTRAY_API_URL/addresses" \
  -H "Authorization: Bearer $INTRAY_API_KEY"
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.

2

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.

// 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,
  };
}

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.

3

Store what comes back

A 202 response means Intray queued the email. It does not mean the email was delivered.

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.

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.

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 codeWhat happenedWhat to do
503 sending_not_configuredSending is not enabled for the workspace yetAsk your Intray contact to enable it. Retrying does not help until then.
403 forbiddenThe key lacks messages:sendCreate a new key with that permission. Permissions on an existing key cannot be changed.
403 sending_pausedSending is paused, by staff or after recent bounces or complaintsStop sending and contact Intray.
404 not_foundThe key is not allowed to use that addressCheck INTRAY_ADDRESS_ID against GET /addresses for this key.
409 address_disabledThe address is disabled in IntrayAsk a workspace owner to enable the address, or use another one.
422A field is invalid or unknown, or the recipient is suppressedRead message. It names the field, or the address that bounced, complained, or opted out.
429 rate_limitedMore than 60 requests a minute for the key, or 600 for the workspaceWait for the Retry-After header, then retry with the same Idempotency-Key.
429 send_limit or 429 integration_limitThe workspace or the key used up its sending capacityRetry later with the same Idempotency-Key and the same body.
429 recipient_limitThis recipient has had too many emails, or too many without a replyDo not retry. Wait for the recipient to reply before sending more.
409 idempotency_conflictThe same Idempotency-Key was used with a different bodyUse 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 has the full retry rules.

Next