SMALL BY DESIGN

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 specification

Quick start

Call POST /v1/react from your server. Authenticate with an Emote API key from your dashboard. Keep your key on the server.

POST /v1/react
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.

FIELDTYPEDESCRIPTION
messagestring ยท requiredThe incoming user message. 1โ€“4,000 characters.
agentstringYour agentโ€™s personality or AGENTS.md contents. Up to 12,000 characters. Defaults to a warm, thoughtful assistant.
contextarrayUp 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.
reactionsemoji[]Allowed emojis from the enum below. Defaults to all 12. An empty array returns "none" without calling Jev. No duplicates.
frequencyenumreserved, 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".

๐Ÿ‘โค๏ธ๐Ÿ˜‚๐ŸŽ‰๐Ÿ˜ฎ๐Ÿ˜ข๐Ÿค”๐Ÿ”ฅ๐Ÿ’€๐Ÿ™Œ๐Ÿ‘€๐Ÿ’ฏ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."
      }
    ]
  }
}
STATUSMEANING
400 / 415 / 422Invalid JSON, unsupported content type, or invalid fields. Unknown fields are rejected.
401Missing or incorrect API key.
402Your trial or monthly evaluation allowance has been used.
409The same Idempotency-Key is already running or was used with different input.
408The caller cancelled the request.
413Request body exceeds 64 KiB.
429Service or TypeSafe rate limit. Read Retry-After.
502 / 503 / 504Provider 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.