A reaction in one request.
Send the latest message, your agentโs personality, and a little conversation history. Emote asks Jev for a single reaction and returns one JSON string.
Download OpenAPI specificationQuick start
Call POST /v1/react from your server. Authenticate with an Emote API key from your dashboard. Keep your key on the server.
const response = await fetch("https://useemote.com/v1/react", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EMOTE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"message": "I finally got the job!",
"agent": "A thoughtful friend. Warm, genuine, and never over the top.",
"reactions": [
"โค๏ธ",
"๐",
"๐"
]
}),
});
if (!response.ok) throw new Error(`Emote: ${response.status}`);
const reaction = await response.json(); // an emoji or "none"The API base URL is https://useemote.com.
The request
Only message is required. Send Content-Type: application/json.
| FIELD | TYPE | DESCRIPTION |
|---|---|---|
| message | string ยท required | The incoming user message. 1โ4,000 characters. |
| agent | string | Your agentโs personality or AGENTS.md contents. Up to 12,000 characters. Defaults to a warm, thoughtful assistant. |
| context | array | Up to 12 previous messages, oldest first. Each has a role (user or assistant), content (1โ2,000 characters), and optional reaction. Exclude the current message. |
| reactions | emoji[] | Allowed emojis from the enum below. Defaults to all 12. An empty array returns "none" without calling Jev. No duplicates. |
| frequency | enum | reserved, balanced (default), or expressive. A behavioral preference, not a guaranteed reaction percentage. |
{
"message": "Amazing. Just what I needed.",
"agent": "A thoughtful assistant who understands sarcasm.",
"context": [
{
"role": "user",
"content": "My flight was cancelled and my luggage is missing.",
"reaction": "none"
},
{
"role": "assistant",
"content": "I'm sorry. That sounds really frustrating."
}
],
"reactions": [
"โค๏ธ",
"๐ข",
"๐",
"๐"
],
"frequency": "balanced"
}The response is the enum.
A successful response is a JSON string: "๐", "โค๏ธ", or "none", for example. There is no text response or explanation to parse. It is always one of your allowed emojis or "none".
Server-Timing reports Jev and total server duration in milliseconds. X-Emote-Model reports the model used. Timing data stays out of the response body.
"none" is a successful decision to stay quiet. Timeouts, invalid input, and provider failures have non-2xx statuses and error objects. An outage never becomes a fake โno reaction.โ
Alongside the reply.
Start the reaction request and your usual model request when the user sends a message. Deliver each result as it arrives. This example assumes your existing generateReply, sendReply, attachReaction, and reportReactionError functions.
// Start both from your message handler. No model tool call.
const reactionTask = fetch(
"https://useemote.com/v1/react",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EMOTE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ message, agent, context }),
signal: AbortSignal.timeout(1500),
},
).then(async (response) => {
if (!response.ok) throw new Error(`Emote: ${response.status}`);
const reaction = await response.json();
if (reaction !== "none") {
await attachReaction(messageId, reaction);
}
}).catch(reportReactionError);
// Your existing model and delivery code keep running independently.
const replyTask = generateReply(message, context)
.then(sendReply);
await Promise.allSettled([reactionTask, replyTask]);Use the message ID captured when you start the request to attach the result to the correct message. Cancel or discard reactions when a message is deleted, the conversation resets, or the result arrives too late for your interface. Keep your Emote key on your server.
Personality, context, and restraint.
Pass your agentโs existing markdown as agent. Jev uses it for tone, relationship, and reaction preferences. It does not execute tools or tasks described in the markdown.
The service reads the current message in context and can choose silence. Include previous reactions to help it avoid repeating itself. Emote does not retain message text, personality text, or conversation history. It stores the selected reaction, request identifier, usage, and timing for accounting and retry protection.
Instructions guide model behavior; they are not a correctness guarantee. Test your own conversations, languages, and personas. The playgroundโs reaction settings apply to future messages; selecting a personality starts a fresh conversation.
Messages, context, and agent instructions are sent to TypeSafe. Emote does not log message bodies. TypeSafeโs retention terms are separate; see their data handling documentation .
Errors & limits
{
"error": {
"code": "invalid_request",
"message": "Check the request fields.",
"details": [
{
"path": "message",
"message": "Message cannot be empty."
}
]
}
}| STATUS | MEANING |
|---|---|
| 400 / 415 / 422 | Invalid JSON, unsupported content type, or invalid fields. Unknown fields are rejected. |
| 401 | Missing or incorrect API key. |
| 402 | Your trial or monthly evaluation allowance has been used. |
| 409 | The same Idempotency-Key is already running or was used with different input. |
| 408 | The caller cancelled the request. |
| 413 | Request body exceeds 64 KiB. |
| 429 | Service or TypeSafe rate limit. Read Retry-After. |
| 502 / 503 / 504 | Provider error, missing server configuration, or the 1.5-second upstream deadline expired. |
Each account can make 300 requests per minute, shared across its keys. The public playground has a separate shared limit of 60 requests per minute and 1,000 per day. Message, personality, and conversation text together are limited to 12,000 UTF-8 bytes.
Latency depends on the payload, network, and provider load. The playground displays actual browser-to-server round-trip time for each request.
Usage and safe retries
Each successful Jev decision uses one evaluation, including "none". Failed calls and empty reaction lists donโt count. The Developer plan includes 100,000 evaluations for $100 per billing month, with no automatic overage charges.
Send an Idempotency-Key header (1โ128 printable ASCII characters, without spaces) for each message. Reusing it with the same input returns the saved result without another charge. If a request is still running, wait before retrying. Use a new key for a new message. There are no automatic inference retries.
X-Request-Id identifies the evaluation, X-Usage-Remaining reports the allowance available when it was reserved, and X-Idempotent-Replay: true identifies a saved result.