Introduction
The SessionMood API is a REST API that infers a user's emotional state in real time from behavioral signals — clicks, scrolls, idle time, errors, and more. Send events as they happen in the browser, then read back a mood label with confidence score, signals, and a suggested intervention action. Every response is JSON. HTTPS only.
session_id. The engine scores the session and returns one of six mood states —
frustrated
confused
decisive
browsing
disengaged
focused — plus a recommended action to trigger in your UI.
Quick start
# 1. Send events curl -X POST "https://session-mood-api-production.up.railway.app/v1/sessions/user_abc/events" \ -H "X-Api-Key: YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"events":[{"type":"rage_click","x":240,"y":580,"ts":1746352800}]}' # 2. Read the mood curl "https://session-mood-api-production.up.railway.app/v1/sessions/user_abc/mood" \ -H "X-Api-Key: YOUR_KEY"
Authentication
All endpoints except GET /health require an API key. Pass it using either of the two
supported methods below. Both are equally valid; the header approach is preferred for
server-side calls.
Method 1 — X-Api-Key header
curl "https://session-mood-api-production.up.railway.app/v1/sessions/abc/mood" \ -H "X-Api-Key: sm_live_xxxxxxxxxxxx"
fetch("https://session-mood-api-production.up.railway.app/v1/sessions/abc/mood", { headers: { "X-Api-Key": "sm_live_xxxxxxxxxxxx" } })
Method 2 — Bearer token
curl "https://session-mood-api-production.up.railway.app/v1/sessions/abc/mood" \ -H "Authorization: Bearer sm_live_xxxxxxxxxxxx"
fetch("https://session-mood-api-production.up.railway.app/v1/sessions/abc/mood", { headers: { "Authorization": "Bearer sm_live_xxxxxxxxxxxx" } })
GET /health endpoint requires no authentication
and is safe to call from any context.Base URL
All API requests are made over HTTPS to the following base URL:
https://session-mood-api-production.up.railway.app
Every endpoint is prefixed with /v1, for example:
| Endpoint | Full URL |
|---|---|
POST /v1/sessions/:id/events | https://session-mood-api-production.up.railway.app/v1/sessions/:id/events |
GET /v1/sessions/:id/mood | https://session-mood-api-production.up.railway.app/v1/sessions/:id/mood |
POST /v1/sessions/:id/feedback | https://session-mood-api-production.up.railway.app/v1/sessions/:id/feedback |
GET /v1/analytics/moods | https://session-mood-api-production.up.railway.app/v1/analytics/moods |
DELETE /v1/sessions/:id | https://session-mood-api-production.up.railway.app/v1/sessions/:id |
GET /health | https://session-mood-api-production.up.railway.app/health |
/health endpoint lives at the root — it is the only endpoint not prefixed with /v1.Rate Limits
Rate limits are tracked per API key on a rolling monthly basis. If you exceed your plan's
session allowance, the API returns 429 Too Many Requests.
| Plan | Sessions / month | Notes |
|---|---|---|
| Free | 5,000 | Ideal for prototyping and small side projects |
| Growth | 50,000 | For production apps with moderate traffic |
| Pro | 200,000 | High-volume products; custom limits available on request |
There is no documented per-minute hard limit at this time. When a rate limit is exceeded the response body will contain the standard error shape:
{ "error": "Rate limit exceeded", "plan": "free", "limit": 5000, "reset_at": "2026-06-01T00:00:00.000Z" }
session_id
The :id path parameter identifies a unique user session. The API places no format constraints on it —
you can use your own session token, a UUID, or any opaque identifier you already generate.
Rules
- Must be consistent across all event calls for the same session. The mood is accumulated per
session_id. - URL-encode any special characters if your ID contains them (e.g. spaces, slashes).
- Never send PII as the session ID — no email addresses, names, or phone numbers. Use an opaque internal token.
- Maximum length: 256 characters.
// Option A: your own session token (already unique per user) const sessionId = authToken.userId; // e.g. "u_8f3k2a" // Option B: UUID generated client-side (persist in sessionStorage) function getSessionId() { const key = "sm_session_id"; let id = sessionStorage.getItem(key); if (!id) { id = crypto.randomUUID(); sessionStorage.setItem(key, id); } return id; }
POST /v1/keys/generate
Generate a new API key. No authentication required — this is the endpoint your signup form calls. The key is returned once and cannot be retrieved again, so store it immediately.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
customer_name | string | Yes | Name or company of the key owner |
email | string | No | Contact email for the customer |
plan | string | No | starter (default), growth, or pro |
Example request
curl -X POST "https://session-mood-api-production.up.railway.app/v1/keys/generate" \ -H "Content-Type: application/json" \ -d '{ "customer_name": "Acme Inc", "email": "dev@acme.com", "plan": "starter" }'
Response 201 Created
{
"api_key": "sm_4c9174cbe8250bb1d09ae779c168a4399e765819da244cbf",
"customer": "Acme Inc",
"email": "dev@acme.com",
"plan": "starter",
"limits": "5,000 sessions/month",
"message": "Store this key safely - it won't be shown again."
}
Error responses
// 400 — missing customer_name { "error": "customer_name is required" } // 400 — invalid plan { "error": "Invalid plan", "valid_plans": ["starter", "growth", "pro"] }
POST /v1/sessions/:id/events
Submit one or more behavioral events for a session. Events are batched — send as many as you like in a single request.
The engine updates the mood score immediately and returns the current mood in the response body, so you don't need
to make a separate GET /mood call after every batch.
Request body
- events array required Array of event objects. Must contain at least one element.
- events[].type string required One of the 12 valid event type strings (see table below).
- events[].ts number optional Unix timestamp in seconds when the event occurred. If omitted, server receipt time is used.
- events[].x number optional Viewport X coordinate in pixels (for click/rage_click).
- events[].y number optional Viewport Y coordinate in pixels (for click/rage_click).
- events[].duration_ms number optional Duration in milliseconds (for idle/hover/input_pause).
- events[].speed number optional Scroll speed in pixels/second (for scroll).
- events[].direction string optional "up" or "down" (for scroll).
- events[].url string optional Page URL (for page_view).
- events[].message string optional Error message (for error). Max 512 chars.
Valid event types
| Type | Optional fields | Description |
|---|---|---|
click | x, y, ts | Standard single click on any element |
rage_click | x, y, ts | Multiple rapid clicks in the same area — strong frustration signal |
scroll | speed, direction, ts | Scroll event; fast speed may indicate searching or skimming |
input_pause | duration_ms, ts | User stopped mid-input for more than a threshold duration |
backtrack | ts | User scrolled up or reversed direction unexpectedly |
back_nav | ts | Browser back button pressed |
page_view | url, ts | User navigated to a new page or route |
error | message, ts | A JS error or API error was surfaced to the user |
idle | duration_ms, ts | User became inactive for a period — possible disengagement |
hover | duration_ms, ts | User hovered over an element for an extended time — possible confusion |
focus | ts | User focused a form input or text field |
blur | ts | User unfocused a form input — possibly abandoned it |
Example request
curl -X POST \ "https://session-mood-api-production.up.railway.app/v1/sessions/user_abc/events" \ -H "X-Api-Key: sm_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "type": "rage_click", "x": 240, "y": 580, "ts": 1746352800 }, { "type": "error", "message": "Payment failed", "ts": 1746352802 }, { "type": "idle", "duration_ms": 4500, "ts": 1746352807 } ] }'
const res = await fetch( `https://session-mood-api-production.up.railway.app/v1/sessions/${sessionId}/events`, { method: "POST", headers: { "X-Api-Key": "sm_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ events: [ { type: "rage_click", x: 240, y: 580, ts: Date.now() / 1000 }, { type: "error", message: "Payment failed" }, { type: "idle", duration_ms: 4500 }, ], }), } ); const data = await res.json();
Responses
200 Success
{ "session_id": "user_abc", "events_stored": 3, "total_events": 6, "current_mood": "frustrated" }
events_stored— events accepted this calltotal_events— cumulative for this session
400 Invalid event type
{ "error": "Invalid event type(s)", "invalid": ["page_visit"], "valid_types": [ "click", "rage_click", "scroll", "input_pause", "backtrack", "back_nav", "page_view", "error", "idle", "hover", "focus", "blur" ] }
GET /v1/sessions/:id/mood
Retrieve the current inferred mood state for a session. The response includes the mood label, a confidence score, human-readable signal descriptions, and a recommended UI action to trigger based on the mood.
Path parameters
- :id string required The session identifier. Must match the ID used when submitting events.
Example request
curl \ "https://session-mood-api-production.up.railway.app/v1/sessions/user_abc/mood" \ -H "X-Api-Key: sm_live_xxxxxxxxxxxx"
const res = await fetch( `https://session-mood-api-production.up.railway.app/v1/sessions/${sessionId}/mood`, { headers: { "X-Api-Key": "sm_live_xxxxxxxxxxxx" } } ); const mood = await res.json(); if (mood.suggested_action === "show_live_chat") { openLiveChat(); }
Response fields
- session_idstringEcho of the requested session ID.
- moodstringCurrent mood label. One of:
frustrated,confused,decisive,browsing,disengaged,focused,neutral. - confidencenumberConfidence in the mood classification, from 0 (no data) to 1 (very high).
- signalsstring[]Array of human-readable signal identifiers that drove the current mood. E.g.
"rage_click_detected","repeated_errors". - suggested_actionstringRecommended UI action to trigger. Use this to decide whether to show live chat, a tooltip, a discount, or take no action.
- event_countnumberTotal number of events stored for this session.
- updated_atstringISO 8601 timestamp of the last event processed for this session.
Responses
200 Success
{ "session_id": "user_abc", "mood": "frustrated", "confidence": 0.89, "signals": [ "rage_click_detected", "repeated_errors" ], "suggested_action": "show_live_chat", "event_count": 6, "updated_at": "2026-05-04T10:00:00.000Z" }
404 Session not found
{ "error": "Session not found", "session_id": "user_abc" }
Returned when :id has no stored events. Submit at least one event before calling this endpoint.
POST /v1/sessions/:id/feedback
Record the outcome of a mood-triggered action. Sending feedback helps the engine learn over time which interventions work for which signals. This endpoint is optional but strongly recommended for production integrations.
Request body
-
action_taken
string
required
The action you triggered (e.g.
"show_live_chat","show_discount","no_action"). Max 128 chars. - was_helpful boolean optional Whether the action improved the user experience. Omit if unknown.
- notes string optional Free-text notes for your own records. Max 512 chars.
Example request
curl -X POST \ "https://session-mood-api-production.up.railway.app/v1/sessions/user_abc/feedback" \ -H "X-Api-Key: sm_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "action_taken": "show_live_chat", "was_helpful": true, "notes": "User started chat immediately" }'
await fetch( `https://session-mood-api-production.up.railway.app/v1/sessions/${sessionId}/feedback`, { method: "POST", headers: { "X-Api-Key": "sm_live_xxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ action_taken: "show_live_chat", was_helpful: true, }), } );
Responses
200 Success
{ "session_id": "user_abc", "feedback_recorded": true }
400 Missing required field
{ "error": "Missing required field", "field": "action_taken" }
GET /v1/analytics/moods
Retrieve aggregate mood statistics across all sessions associated with your API key. Use this to understand the emotional landscape of your user base over time — what percentage of sessions are frustrated, how many have resulted in feedback, and so on.
Example request
curl \ "https://session-mood-api-production.up.railway.app/v1/analytics/moods" \ -H "X-Api-Key: sm_live_xxxxxxxxxxxx"
Response
{ "total_sessions": 1842, "feedback_count": 310, "mood_distribution": [ { "mood": "browsing", "count": 640, "percentage": 34.7 }, { "mood": "focused", "count": 510, "percentage": 27.7 }, { "mood": "neutral", "count": 280, "percentage": 15.2 }, { "mood": "frustrated", "count": 198, "percentage": 10.7 }, { "mood": "confused", "count": 144, "percentage": 7.8 }, { "mood": "disengaged", "count": 70, "percentage": 3.8 }, { "mood": "decisive", "count": 0, "percentage": 0.0 } ] }
Response fields
- total_sessionsnumberTotal sessions ever recorded for this API key.
- feedback_countnumberNumber of sessions that have at least one feedback record.
- mood_distributionarrayOne object per mood state, ordered by count descending.
- mood_distribution[].moodstringThe mood label.
- mood_distribution[].countnumberNumber of sessions currently at this mood (based on the latest event for each session).
- mood_distribution[].percentagenumberPercentage of total sessions (rounded to one decimal).
DELETE /v1/sessions/:id
Permanently erase all stored data for a session — events, mood state, and feedback. Intended for GDPR
right-to-erasure compliance. Once deleted, the session cannot be recovered, and a subsequent
GET /mood for the same ID will return 404.
Example request
curl -X DELETE \ "https://session-mood-api-production.up.railway.app/v1/sessions/user_abc" \ -H "X-Api-Key: sm_live_xxxxxxxxxxxx"
Response
{ "session_id": "user_abc", "deleted": true, "message": "Session data permanently erased." }
GET /health
A simple health check endpoint. No authentication required. Use this in uptime monitors, load balancer health checks, or CI/CD pipelines to verify the API is reachable.
Example request
curl "https://session-mood-api-production.up.railway.app/health"
Response
{ "status": "ok", "version": "1.0.0" }
Event Types
The full catalogue of recognized event types with their optional fields and the moods they contribute to.
All fields are optional except type. Providing more fields increases classification accuracy.
| Type | Description | Optional fields | Signals toward |
|---|---|---|---|
click |
Standard single pointer click | x, y, ts |
decisive browsing |
rage_click |
Rapid repeated clicks in tight area — indicates UI unresponsiveness perceived by user | x, y, ts |
frustrated |
scroll |
Scroll gesture with optional speed & direction metadata | speed, direction, ts |
browsing confused |
input_pause |
User stopped typing mid-field for longer than a threshold | duration_ms, ts |
confused frustrated |
backtrack |
Unexpected scroll reversal — user went back up on the page | ts |
confused |
back_nav |
Browser back button pressed | ts |
frustrated confused |
page_view |
Navigation to a new page or SPA route change | url, ts |
browsing |
error |
A JS exception or API error surfaced to the user | message, ts |
frustrated |
idle |
User was inactive for a prolonged period | duration_ms, ts |
disengaged |
hover |
Extended hover over an element without clicking | duration_ms, ts |
confused browsing |
focus |
User focused a form field — indicates intent | ts |
focused decisive |
blur |
User unfocused a form field — possible abandonment | ts |
confused disengaged |
Mood States
The engine classifies each session into one of seven states. neutral is returned when
there is insufficient data for a confident classification. All others are ranked by confidence and
come with a suggested_action.
| Mood | Meaning | Key signals | suggested_action |
|---|---|---|---|
| frustrated | User is actively struggling — errors, repeated failed actions, rage clicks | rage_click, error, back_nav |
show_live_chat |
| confused | User seems lost — backtracking, long hovers, input pauses, scroll reversals | backtrack, hover, input_pause, scroll |
show_tooltip |
| decisive | User knows what they want — quick clicks, form focus, forward navigation | click, focus |
no_action |
| browsing | User is exploring at a relaxed pace — multiple page views, moderate scroll | page_view, scroll, click |
show_recommendations |
| disengaged | User has lost interest — prolonged idle, blurs without re-focus | idle, blur |
show_exit_offer |
| focused | User is actively completing a task — sustained form interaction, rapid inputs | focus, click |
no_action |
| neutral | Insufficient events to classify confidently — fewer than ~3–5 events in the session | Any / none | no_action |
Error Codes
All errors follow a consistent JSON shape with at minimum an error string.
Additional fields such as field, invalid, or valid_types
may be present depending on the error type.
| Status | Meaning | Common causes |
|---|---|---|
| 200 OK | Request succeeded | — |
| 400 Bad Request | Invalid request body | Unknown event type, missing events array, missing action_taken |
| 401 Unauthorized | Authentication failed | Missing API key, revoked key, malformed Authorization header |
| 404 Not Found | Resource does not exist | Session ID has no stored events, or was deleted |
| 429 Too Many Requests | Rate limit exceeded | Monthly session quota exhausted for your plan |
| 500 Internal Server Error | Unexpected server-side failure | Transient infrastructure issue — retry with exponential backoff |
Example error bodies
401
{ "error": "Unauthorized", "message": "Missing or invalid API key" }
500
{ "error": "Internal Server Error", "request_id": "req_7f3a2b" }
404
{ "error": "Session not found", "session_id": "user_abc" }
429
{ "error": "Rate limit exceeded", "plan": "free", "limit": 5000, "reset_at": "2026-06-01T00:00:00.000Z" }
Response Schemas
TypeScript interfaces for all response types. Use these as a reference when integrating in
typed projects, or to generate client types with tools like zod or io-ts.
POST /v1/sessions/:id/events
interface PostEventsResponse { session_id: string; // the session identifier events_stored: number; // events accepted in this request total_events: number; // cumulative event count for session current_mood: MoodLabel; // updated mood after this batch } type MoodLabel = | "frustrated" | "confused" | "decisive" | "browsing" | "disengaged" | "focused" | "neutral";
GET /v1/sessions/:id/mood
interface GetMoodResponse { session_id: string; mood: MoodLabel; confidence: number; // 0–1 signals: string[]; // e.g. ["rage_click_detected"] suggested_action: SuggestedAction; event_count: number; updated_at: string; // ISO 8601 } type SuggestedAction = | "no_action" // do nothing | "show_live_chat" // open chat widget | "show_tooltip" // display contextual help | "show_recommendations" // surface personalized content | "show_exit_offer"; // discount / retention popup
POST /v1/sessions/:id/feedback
interface PostFeedbackRequest { action_taken: string; // required was_helpful?: boolean; // optional notes?: string; // optional, max 512 chars } interface PostFeedbackResponse { session_id: string; feedback_recorded: boolean; }
GET /v1/analytics/moods
interface MoodDistributionItem { mood: MoodLabel; count: number; percentage: number; // 0–100, one decimal } interface GetAnalyticsResponse { total_sessions: number; feedback_count: number; mood_distribution: MoodDistributionItem[]; }
DELETE /v1/sessions/:id
interface DeleteSessionResponse { session_id: string; deleted: boolean; message: string; }
GET /health
interface HealthResponse { status: "ok" | "degraded" | "down"; version: string; // semver, e.g. "1.0.0" }
Error (all endpoints)
interface ApiError { error: string; // human-readable error description message?: string; // additional detail (optional) field?: string; // which field caused the 400 invalid?: string[]; // invalid event types (400) valid_types?: string[]; // full list of valid types (400) plan?: string; // current plan (429) limit?: number; // plan session limit (429) reset_at?: string; // ISO 8601 reset date (429) session_id?: string; // affected session (404) request_id?: string; // opaque ID for support (500) }