This feature is only available on the Royal Jelly plan
Authentication
All API requests require an API token. You can create one from Settings โ API Tokens in your team settings.
Include your token in the Authorization header of every request:
Authorization: Bearer YOUR_API_TOKEN
API tokens have full access to your team’s data โ treat them like passwords. Never share tokens in public repositories or client-side code. Jelly automatically tracks when each token was last used.
Base URL
https://app.letsjelly.com/api
Labels
List Labels
Get all labels for your team, sorted alphabetically.
GET /api/labels
Response:
[
{
"id": 1,
"name": "Bug",
"color": "cherry"
},
{
"id": 2,
"name": "Feature",
"color": "mint"
}
]
Create Label
Create a new label for your team.
POST /api/labels
Content-Type: application/json
| Parameter | Required | Description |
|---|---|---|
name |
Yes | The label name (must be unique within your team) |
color |
No | One of the available colors listed below |
Available colors:
Example request:
{
"name": "Important",
"color": "cherry"
}
Success response:
{
"id": 3,
"name": "Important",
"color": "cherry"
}
Conversation Labels
Add a Label to a Conversation
Apply an existing label to a conversation. The conversation_id is the conversation’s slug (the URL-friendly identifier you see in the address bar).
POST /api/conversations/:conversation_id/labels
Content-Type: application/json
| Parameter | Required | Description |
|---|---|---|
label_id |
Yes | The ID of the label to apply |
Example request:
{
"label_id": 456
}
Success response (201 Created):
Returns all labels currently on the conversation.
[
{
"id": 3,
"name": "Important",
"color": "cherry"
}
]
Draft Replies
Create a Draft Reply
Create a draft reply to the most recent message in a conversation. The draft will appear in the conversation in the Jelly UI, ready for a team member to review and send.
POST /api/conversations/:conversation_id/draft_reply
Content-Type: application/json
| Parameter | Required | Description |
|---|---|---|
body |
Yes | The HTML body of the draft reply |
member_id |
No | The ID of the team member to assign the draft to. When provided, the draft appears as theirs and they can edit it immediately. When omitted, the draft is unowned and any team member can claim it. |
message_id |
No | The ID of a specific message to reply to. Defaults to the most recent sent message in the conversation. |
to |
No | Override the To address (auto-populated from the original message if omitted) |
cc |
No | Override the CC address |
bcc |
No | Override the BCC address. If your team has an auto-BCC configured, it will be applied unless you provide this field. |
Example request:
{
"body": "<p>Thanks for reaching out! We'll look into this right away.</p>",
"member_id": 42
}
Success response (201 Created):
{
"id": "123",
"subject": "Re: Stair car",
"inbound": false,
"sent_at": null,
"url": "https://app.letsjelly.com/teams/your-team/conversations/stair-car#message_123",
"from": ["contact@bluth.company"],
"to": ["sally@sitwell.test"],
"cc": [],
"html_body": "<p>Thanks for reaching out! We'll look into this right away.</p>",
"text_body": "",
"attachments_count": 0,
"sender": {
"type": "member",
"id": "42",
"name": "Michael Bluth",
"email": "michael@bluth.company"
}
}
If a draft reply already exists for that message, the endpoint returns 409 Conflict with the existing draft in the response body. This prevents accidentally overwriting a draft that a team member may have already edited.
Contacts
Find Contact by Email
Look up an existing contact by their email address.
GET /api/contacts/for_email?email=alice@example.com
| Parameter | Required | Description |
|---|---|---|
email |
Yes | The email address to look up |
Success response (200 OK):
{
"id": "abc123",
"name": "Alice Smith",
"email": "alice@example.com",
"url": "https://app.letsjelly.com/teams/your-team/contacts/abc123"
}
Returns a 404 if no contact is found with that email address.
Create or Update Contact
Create a new contact or update an existing one. If a contact with the given email already exists, it will be updated instead of creating a duplicate.
POST /api/contacts
Content-Type: application/json
| Parameter | Required | Description |
|---|---|---|
email |
Yes | The contact’s email address |
name |
No | The contact’s display name |
note |
No | A private note about the contact (supports HTML). When provided, replaces the existing note. Omit to leave unchanged. |
links |
No | An object mapping link titles to URLs. Replaces all existing links when provided. |
labels |
No | An array of label names. Replaces all existing contact labels when provided. Labels must already exist on the team. |
Example request:
{
"email": "alice@example.com",
"name": "Alice Smith",
"note": "VIP customer, handle with care",
"links": {
"LinkedIn": "https://linkedin.com/in/alice",
"Website": "https://alice.example.com"
},
"labels": ["VIP", "Partner"]
}
Success response (201 Created or 200 OK):
Returns 201 when creating a new contact, or 200 when updating an existing one.
{
"id": 42,
"email": "alice@example.com",
"name": "Alice Smith",
"note": "VIP customer, handle with care",
"links": [
{ "title": "LinkedIn", "url": "https://linkedin.com/in/alice" },
{ "title": "Website", "url": "https://alice.example.com" }
],
"labels": [
{ "name": "VIP", "color": "cherry" },
{ "name": "Partner", "color": "mint" }
]
}
When links or labels are included in a request, they replace all existing values for that contact. The note field also replaces the existing note when provided. Omit these fields entirely if you only want to update the name.
Error Handling
All API errors are returned as JSON, never HTML.
| Status | Meaning | Example |
|---|---|---|
401 |
Missing or invalid API token | {"error": "Invalid API token"} |
404 |
Resource or endpoint not found | {"error": "Resource not found"} |
409 |
Conflict (e.g. draft already exists) | Returns the existing resource |
422 |
Validation error | {"errors": ["Name can't be blank"]} |
500 |
Server error | {"error": "Internal server error"} |
Examples
curl
# List labels
curl "https://app.letsjelly.com/api/labels" \
-H "Authorization: Bearer YOUR_API_TOKEN"
# Create a label
curl -X POST "https://app.letsjelly.com/api/labels" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Bug", "color": "cherry"}'
# Apply label to a conversation
curl -X POST "https://app.letsjelly.com/api/conversations/my-conversation/labels" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label_id": 123}'
# Find a contact by email
curl "https://app.letsjelly.com/api/contacts/for_email?email=alice@example.com" \
-H "Authorization: Bearer YOUR_API_TOKEN"
# Create or update a contact
curl -X POST "https://app.letsjelly.com/api/contacts" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com", "name": "Alice Smith", "note": "VIP customer", "labels": ["VIP"]}'
# Create a draft reply in a conversation
curl -X POST "https://app.letsjelly.com/api/conversations/my-conversation/draft_reply" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "<p>Thanks for reaching out!</p>", "member_id": 42}'
JavaScript
const API_TOKEN = 'YOUR_API_TOKEN';
const BASE_URL = 'https://app.letsjelly.com/api';
const headers = {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json'
};
// List labels
async function listLabels() {
const response = await fetch(`${BASE_URL}/labels`, { headers });
return response.json();
}
// Create a label
async function createLabel(name, color) {
const response = await fetch(`${BASE_URL}/labels`, {
method: 'POST',
headers,
body: JSON.stringify({ name, color })
});
return response.json();
}
// Find a contact by email
async function findContact(email) {
const response = await fetch(
`${BASE_URL}/contacts/for_email?email=${encodeURIComponent(email)}`,
{ headers }
);
return response.json();
}
// Create or update a contact
async function upsertContact(email, { name, note, links, labels } = {}) {
const response = await fetch(`${BASE_URL}/contacts`, {
method: 'POST',
headers,
body: JSON.stringify({ email, name, note, links, labels })
});
return response.json();
}
// Create a draft reply in a conversation
async function createDraftReply(conversationId, body, { memberId, messageId } = {}) {
const response = await fetch(`${BASE_URL}/conversations/${conversationId}/draft_reply`, {
method: 'POST',
headers,
body: JSON.stringify({ body, member_id: memberId, message_id: messageId })
});
return response.json();
}
Rate Limits
The API currently has no rate limits, but please be respectful and avoid making excessive requests.