GuidesSupport inbox in your app
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.
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
- A shared channel in Rasket Inbox, such as
support@on your domain. Settings → Developers in the Inbox lists your channels and their IDs. - 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.
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"
}
}'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();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.
{
"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 has the details.
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"
}'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();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 shows, then mark your ticket answered. The event names the conversation and carries no message 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:
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"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();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, and every call is on the Inbox API reference.