Skip to main content
POST
/
api
/
v1
/
ai
/
chat
Send chat message (streaming or buffered)
curl --request POST \
  --url https://handauncle-backend-prod-205012263523.asia-south1.run.app/api/v1/ai/chat \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "message": "<string>",
  "conversationId": "<string>",
  "model": "<string>",
  "prepromptKey": "<string>",
  "inputType": "text",
  "attachments": [
    {
      "fileId": "<string>",
      "data": "<string>",
      "mimeType": "<string>",
      "filename": "<string>"
    }
  ]
}
'
{
  "success": true,
  "data": {
    "conversationId": "<string>",
    "messageId": "<string>",
    "role": "assistant",
    "content": "<string>",
    "model": "<string>",
    "tokenCount": 123,
    "toolCalls": [
      {
        "toolName": "<string>",
        "args": {},
        "result": {}
      }
    ]
  },
  "meta": {
    "timestamp": "2023-11-07T05:31:56Z",
    "requestId": "<string>"
  }
}
Unified endpoint for sending a user message to the Handa Uncle assistant. Supports both JSON responses and server-sent events (SSE) streaming with the same payload.

Authentication & headers

Select your authentication method in the playground above using the security dropdown.
ScenarioRequired headers
Option 1: Registered user (bearerAuth)Authorization: Bearer <Auth0 access token>
Option 2: Guest / device flow (deviceAuth)x-device-id (required), x-platform (required: android/ios/web), x-user-id (optional), x-user-email (optional), x-user-phone (optional)
Streaming toggleX-Stream-Response: false or Stream: false to disable streaming (streaming is ON by default)
The two authentication methods (bearerAuth and deviceAuth) are mutually exclusive—choose one based on whether the user is registered or a guest. The playground will show the appropriate fields based on your selection.
Streaming is enabled by default. If you want a buffered JSON response instead of SSE, explicitly set X-Stream-Response: false or Stream: false in your request headers.
Every request is also subject to the chat rate limit (20 req/min) and the free-tier message counter enforced by usageLimitMiddleware.

Guest/device auth flow (deviceAuth option)

When using deviceAuth in the playground, provide these headers:
  • x-device-id (required): Unique device identifier
  • x-platform (required): One of android, ios, or web
  • x-user-id (optional): User ID returned from GET /app/launch
  • x-user-email (optional): Email hint for linking
  • x-user-phone (optional): Phone hint in E.164 format
The complete guest experience flow:
  1. Launch – call GET /app/launch with x-device-id and x-platform. The response returns a synthetic userId (e.g. U-813e62a0-...) even when no Auth0 account exists.
  2. Chat as guest – send POST /api/v1/ai/chat with the device headers plus x-user-id returned from the launch step. Requests work immediately until the free counter reaches the configured FREE_MESSAGE_THRESHOLD (default 100).
  3. Hit the limit – on the (threshold + 1) request the API responds with 429 FREE_LIMIT_EXCEEDED and the payload includes error.details.isGuest = true and error.details.requiresSignup = true. Frontends should show the signup modal at this point.
  4. Continue after signup – once the user signs up and receives an Auth0 access token, switch to the bearer header. The userId and conversation history stay intact because the backend links the identity via the stored device metadata.
Device headers are treated with the same chat-rate limiting and audit trails as JWTs. Spoofed or missing headers are rejected with 401 UNAUTHORIZED.

Request body

FieldTypeRequiredNotes
messagestring1–2000 characters of user input.
conversationIdstringMongoDB ObjectId; omit to create a new conversation.
modelstringOverrides the configured default (subject to allowlists).
attachments[].fileIdstringReference to a file uploaded via the File Upload service.
attachments[].datastringBase64 payload for inline uploads (max 20MB after decoding).
attachments[].mimeTypestringRequired when data is provided.
attachments[].filenamestringRequired when data is provided.
prepromptKeystringOptional identifier for a backend-managed pre-prompt. When present, the chat pipeline injects the masked instructions tied to this key right after the system prompt.
Attachments must contain either fileId or (data + mimeType + filename). The backend validates file size, ownership, and converts supported media into the AI SDK multimodal format before invoking the LLM.

Masked pre-prompts

Administrators can define reusable “masked” instructions (e.g., persona tweaks or guardrails) via the Management API (POST /api/v1/preprompts, protected by the backend secret). Client applications should fetch the user-facing catalog from GET /api/v1/public/preprompts and send the selected prepromptKey alongside the chat payload. Users only see the friendly label, while the backend silently injects the corresponding hidden prompt into the LLM context.

Guest vs registered usage messaging

usageLimitMiddleware inspects c.get('isGuest') and returns tailored limit errors:
{
  "success": false,
  "error": {
    "message": "Please sign up to continue chatting and unlock more features!",
    "code": "FREE_LIMIT_EXCEEDED",
    "details": {
      "currentCount": 100,
      "threshold": 100,
      "remaining": 0,
      "isGuest": true,
      "requiresSignup": true
    }
  },
  "meta": { "...": "..." }
}
Registered users see "Please upgrade your account..." plus "isGuest": false, "requiresSignup": false. Use the flags to decide whether to show a signup modal or upsell screen.

Responses

Buffered JSON

{
  "success": true,
  "data": {
    "conversationId": "665f5e6fcb6e4c73dc6dca01",
    "messageId": "665f5e91cb6e4c73dc6dca05",
    "role": "assistant",
    "content": "Equity funds carry higher volatility...",
    "model": "gpt-4o-mini",
    "tokenCount": 812,
    "toolCalls": [
      {
        "toolName": "portfolio_lookup",
        "args": { "accountId": "abc123" },
        "result": { "holdings": 4 }
      }
    ]
  },
  "meta": {
    "timestamp": "2025-12-01T12:00:00.000Z",
    "requestId": "req_abc"
  }
}

Streaming (SSE)

Each chunk is sent as data: <json>\n\n. Expect the following shapes:
  • {"type":"token","content":"..."} – incremental tokens.
  • {"type":"toolCall","toolName":"...","args":{...}} – tool invocation start.
  • {"type":"toolResult","toolName":"...","result":{...}} – tool output.
  • {"type":"done","conversationId":"...","messageId":"...","tokenCount":123,"meta":{"timestamp":"...","streaming":true,"durationMs":1400}}
  • {"type":"error","message":"Streaming failed","error":"..."} – terminal failures.
Clients should treat the stream as finished when a done or error event arrives.

Error codes

HTTPerror.codeWhen it fires
401UNAUTHORIZEDMissing bearer token and device headers.
429RATE_LIMIT_EXCEEDEDMore than 20 requests/min per user/device.
429FREE_LIMIT_EXCEEDEDGuest or free-tier user exhausted the free quota (details.requiresSignup will be true for guests).
500INTERNAL_ERRORUpstream failure (LLM, storage, RAG, etc.).
Retries should respect backoff when rate limited. When FREE_LIMIT_EXCEEDED, surface a signup or upgrade prompt before re-sending traffic.

Example calls

Authenticated user with streaming (default)

curl -N -X POST https://api.handauncle.com/api/v1/ai/chat \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{
        "message": "Please summarise my portfolio",
        "conversationId": "665f5e6fcb6e4c73dc6dca01"
      }'
Note: No X-Stream-Response header needed—streaming is the default behavior.

Guest/device auth with streaming (default)

curl -N -X POST https://api.handauncle.com/api/v1/ai/chat \
  -H 'x-device-id: ios-device-3f92' \
  -H 'x-user-id: U-813e62a0-a3fc-4c7e-bf4e-3f915d9e9f10' \
  -H 'x-platform: ios' \
  -H 'Content-Type: application/json' \
  -d '{ "message": "Hello from a guest!" }'

Non-streaming (buffered JSON response)

To get a complete JSON response instead of SSE streaming, explicitly disable streaming:
curl -X POST https://api.handauncle.com/api/v1/ai/chat \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -H 'X-Stream-Response: false' \
  -H 'Content-Type: application/json' \
  -d '{ "message": "What is a mutual fund?" }'
Both calls share the same response envelope; the only difference is how the caller authenticates and whether streaming is explicitly disabled.

Authorizations

Authorization
string
header
required

Auth0 access token for registered users.

Headers

x-device-id
string

Required when Authorization header is omitted. Stable identifier for the calling device.

Minimum string length: 1
x-platform
enum<string>

Client platform. Required with device auth.

Available options:
android,
ios,
web
x-user-id
string

Optional hint for mapping the device to an existing user (returned by GET /app/launch). Strongly recommended so conversations persist once the guest signs up.

x-user-email
string<email>

Optional email hint for device-auth flows.

x-user-phone
string

Optional phone hint in E.164 format for device-auth flows.

Pattern: ^\+?[1-9]\d{7,14}$
X-Stream-Response
enum<string>
default:true

Streaming is ON by default. Set to false to receive buffered JSON instead of SSE streaming.

Available options:
true,
false
Stream
enum<string>
default:true

Alias for X-Stream-Response. Streaming is ON by default. Set to false to disable.

Available options:
true,
false

Body

application/json
message
string
required
Required string length: 1 - 2000
conversationId
string

Existing conversation ID. Omit to start a new thread.

Pattern: ^[0-9a-fA-F]{24}$
model
string

Optional override for the default model.

prepromptKey
string

Optional identifier for a backend-managed pre-prompt. When supplied, the associated masked instructions are injected right after the system prompt.

Required string length: 2 - 64
Pattern: ^[a-zA-Z0-9][a-zA-Z0-9_-]{1,63}$
inputType
enum<string>

Optional hint for how the conversation was initiated. Use 'voice' for voice input, 'file' for file uploads (auto-detected if attachments present), or 'text' for plain messages (default).

Available options:
text,
file,
voice
attachments
object[]

Response

Chat response streamed or buffered.

success
enum<boolean>
required
Available options:
true
data
object
required
meta
object
required