# Add a support inbox to your app in 5 minutes

When someone fills in your Contact us form, open a conversation in your Support channel. Your team answers it in Rasket Inbox, and the reply arrives in the customer's mail.

Using an AI coding agent? Give it this:

```text
Add a support inbox to this app using Rasket's Inbox API.
Read the Markdown of Rasket's guide first: /docs/inbox-api/support-inbox.md on the Rasket website.
1. RASKET_API_KEY holds a Rasket API key whose Inbox access is "Open conversations only" (or "Read and reply"). Never print or log it.
2. Where users submit the Contact us form, call POST /inbox/conversations with channel "support@yourdomain.com", from {email, name} of the user, the form's subject and message as text, external_id "ticket_<your id>", and a few metadata strings such as user_id and plan.
3. Send an Idempotency-Key header made from your own ticket id, so a retry never opens a second conversation.
4. On 200 or 201, show the user "Thanks — we'll reply by email."
5. On 403 inbox_not_set_up or invalid_permission, log it and show a mailto:support@yourdomain.com link instead.
```

## Before you start

1. A shared channel in Rasket Inbox, such as `support@` on your domain. Settings → Developers in the Inbox lists your channels and their IDs.
2. Somewhere in your server code that handles the form.

## 1. Make a key

In the dashboard, open **API keys → Create API key**. Set **Inbox access** to **Open conversations only** and limit it to your Support channel. A key like this can open conversations and can't read anything back, so it is safe on a form backend. Only a team admin can give a key Inbox access.

## 2. Open a conversation

Call this where your form is submitted. `from` is the person who wrote in: replies go to that address.

Create a conversation:

```sh
curl -X POST "https://api.rasket.com/inbox/conversations" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: contact-58213" \
  -d '{
  "channel": "support@lumen.app",
  "from": {
    "email": "priya@harborcoffee.co",
    "name": "Priya Raman"
  },
  "subject": "Can I move my plan to annual?",
  "text": "Hi! We love Lumen. Can we switch to yearly billing?",
  "external_id": "ticket_58213",
  "metadata": {
    "user_id": "usr_8412",
    "plan": "Team"
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
    "Idempotency-Key": "contact-58213",
  },
  body: JSON.stringify({
    channel: "support@lumen.app",
    from: {
      email: "priya@harborcoffee.co",
      name: "Priya Raman"
    },
    subject: "Can I move my plan to annual?",
    text: "Hi! We love Lumen. Can we switch to yearly billing?",
    external_id: "ticket_58213",
    metadata: {
      user_id: "usr_8412",
      plan: "Team"
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/inbox/conversations",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
        "Idempotency-Key": "contact-58213",
    },
    json={
    "channel": "support@lumen.app",
    "from": {
      "email": "priya@harborcoffee.co",
      "name": "Priya Raman"
    },
    "subject": "Can I move my plan to annual?",
    "text": "Hi! We love Lumen. Can we switch to yearly billing?",
    "external_id": "ticket_58213",
    "metadata": {
      "user_id": "usr_8412",
      "plan": "Team"
    }
  },
)

id = response.json()["id"]
```

You get back `201` and the new conversation. It is in Rasket Inbox straight away, marked as sent by your app, with your `metadata` beside it.

The response:

```text
{
  "object": "inbox_conversation",
  "id": "0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
  "channel": {
    "id": "0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20",
    "address": "support@lumen.app"
  },
  "status": "open",
  "origin": "app",
  "external_id": "ticket_58213",
  "message_id": "0192f8e1-7a52-7d10-8c44-91a0b2c3d4e5",
  "created": true
}
```

Sending the same `external_id` again adds the new message to the same conversation and reopens it, so a follow-up from your app stays in one thread. Nothing else joins two conversations — not the sender, not the subject. Send an `Idempotency-Key` made from your own ticket ID, so a retry never opens a second one.

## 3. Bring email replies home too

Add `inbox` to the emails you already send. Replies to them open conversations in that channel, linked to the email. The [replies guide](https://www.rasket.com/docs/inbox-api/replies) has the details.

Send an email whose replies go to the inbox:

```sh
curl -X POST "https://api.rasket.com/emails" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: receipt-8412-october" \
  -d '{
  "from": "Lumen <billing@lumen.app>",
  "to": ["priya@harborcoffee.co"],
  "subject": "Your Lumen receipt — October",
  "html": "<p>Thanks for your payment of $49.</p>",
  "inbox": "support@lumen.app"
}'
```

```ts
const response = await fetch("https://api.rasket.com/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
    "Idempotency-Key": "receipt-8412-october",
  },
  body: JSON.stringify({
    from: "Lumen <billing@lumen.app>",
    to: ["priya@harborcoffee.co"],
    subject: "Your Lumen receipt — October",
    html: "<p>Thanks for your payment of $49.</p>",
    inbox: "support@lumen.app"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/emails",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
        "Idempotency-Key": "receipt-8412-october",
    },
    json={
    "from": "Lumen <billing@lumen.app>",
    "to": ["priya@harborcoffee.co"],
    "subject": "Your Lumen receipt — October",
    "html": "<p>Thanks for your payment of $49.</p>",
    "inbox": "support@lumen.app"
  },
)

id = response.json()["id"]
```

## 4. Hear when your team replies

Add a webhook endpoint in the dashboard and turn on `inbox.reply.sent`. Check the signature as the [webhooks reference](https://www.rasket.com/docs/api-reference/webhooks) shows, then mark your ticket answered. The event names the conversation and carries no message text:

An inbox.reply.sent event:

```text
{
  "type": "inbox.reply.sent",
  "created_at": "2026-09-09T10:16:44.902Z",
  "data": {
    "conversation_id": "0199d3a4-7a40-7c55-b1e3-0f0a3b6d2e91",
    "channel": {
      "id": "0199d3a4-1b20-7a11-9e02-4c6d8e0f1a22",
      "address": "support@acme.example"
    },
    "origin": "email",
    "actor": {
      "type": "member",
      "id": "0199d3a4-2c31-7b22-8f13-5d7e9f102b33",
      "name": "Dana Park"
    },
    "subject": "Order #1042 arrived damaged",
    "status": "pending",
    "assignee": {
      "user_id": "0199d3a4-2c31-7b22-8f13-5d7e9f102b33",
      "name": "Dana Park"
    },
    "message_id": "0199d3a4-7a52-7d10-8c44-91b2e6f0aa03",
    "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
  }
}
```

To find your own ticket, read the conversation with a key that has Inbox access **Read** and take its `external_id`:

Retrieve a conversation:

```sh
curl -X GET "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

> Only a team admin, or a key with no channel limit, can subscribe an endpoint to Inbox events. A restricted channel's events go to no endpoint.

## If something goes wrong

| Answer | What to do |
| --- | --- |
| `403 invalid_permission` | The key has no Inbox access, or only Read. Edit it in the dashboard under API keys. |
| `403 inbox_not_set_up` | Your project has no channel yet. Create one in Rasket Inbox, under Settings → Mailboxes. |
| `404 not_found` | The channel doesn't exist, or the key isn't allowed to use it — a key limited to other channels, or a restricted channel. |
| `422 validation_error` | Usually no text or html, a body over 256 KB, more than 20 metadata keys, or an attachment that isn't base64. |
| `422 invalid_parameter` | The external_id belongs to a conversation in another channel. |
| `429 rate_limit_exceeded` | The key opened 500 conversations or messages in the last hour. Wait and retry with the same Idempotency-Key. |

Every refusal is on [Errors](https://www.rasket.com/docs/errors), and every call is on the [Inbox API reference](https://www.rasket.com/docs/api-reference/inbox).
