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:sendpermission 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:readto 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.
| 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
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"{
"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.
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.
Store what comes back
A 202 response means Intray queued the email. It does not mean the email
was delivered.
{
"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.
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
- Sign up in your product with an email address you can read, or run the curl example with your own address in
to. - Open the inbox and choose the Done view. The conversation is there, with your email as its only entry.
- Reply to the email from your mail app.
- The thread moves to Todo and shows the reply under the email your app sent.
Pilot limits
- An email has one
torecipient. ccandbccare 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 has the full retry rules.