Spaces:
Running
A newer version of the Gradio SDK is available:
6.6.0
Chat API Documentation
Complete documentation for implementing real-time chat features in the Lojiz app.
Base URL
REST API: https://api.lojiz.com/api/conversations
WebSocket: wss://api.lojiz.com/ws/chat?token=JWT_TOKEN
Authentication
All endpoints require JWT authentication:
- REST: Include
Authorization: Bearer <token>header - WebSocket: Pass token as query parameter
?token=<token>
REST API Endpoints
1. Start/Get Conversation
Creates a new conversation or returns existing one between two users.
POST /api/conversations/
Request Body:
{
"listing_id": "675abc123...",
"message": "Hi, I'm interested in this property" // optional
}
Response:
{
"success": true,
"is_new": true,
"conversation": {
"id": "674xyz789...",
"listing_id": "675abc123...",
"participants": ["user_123", "user_456"],
"listing_title": "2BR Apartment in Lekki",
"listing_image": "https://...",
"last_message": null,
"unread_count": {"user_123": 0, "user_456": 0},
"status": "active",
"created_at": "2024-12-24T10:00:00.000000",
"updated_at": "2024-12-24T10:00:00.000000"
},
"property_card": {
"listing_id": "675abc123...",
"title": "2BR Apartment in Lekki",
"price": 500000,
"currency": "NGN",
"bedrooms": 2,
"bathrooms": 2,
"location": "Lekki Phase 1, Lagos",
"image_url": "https://...",
"listing_type": "rent"
}
}
2. Get All Conversations
Returns all conversations for the current user.
GET /api/conversations/
Response:
{
"success": true,
"data": [
{
"id": "674xyz789...",
"participants": ["user_123", "user_456"],
"listing_title": "2BR Apartment",
"listing_image": "https://...",
"last_message": {
"text": "Yes, it's still available!",
"sender_id": "user_456",
"timestamp": "2024-12-24T11:30:00.000000"
},
"unread_count": {"user_123": 2, "user_456": 0},
"created_at": "...",
"updated_at": "..."
}
],
"total": 1
}
3. Get Messages
Returns messages for a conversation with pagination.
GET /api/conversations/{conversation_id}/messages?limit=50&before_id=...
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 50 | Number of messages (max 100) |
before_id |
string | null | Get messages before this ID (pagination) |
Response:
{
"success": true,
"data": [
{
"id": "msg_123",
"conversation_id": "674xyz789...",
"sender_id": "user_123",
"sender_name": "John Doe",
"sender_avatar": "https://...",
"message_type": "text",
"content": "Hello!",
"media": null,
"property_card": null,
"is_read": true,
"read_at": "2024-12-24T11:31:00.000000",
"is_edited": false,
"edited_at": null,
"is_deleted": false,
"deleted_at": null,
"reactions": {"π": ["user_456"]},
"created_at": "2024-12-24T11:30:00.000000",
"can_edit": false,
"can_delete_for_everyone": false
}
],
"total": 1
}
4. Send Message (REST Fallback)
Use when WebSocket is unavailable.
POST /api/conversations/{conversation_id}/messages
Request Body:
{
"message_type": "text",
"content": "Hello!",
"media_url": null,
"property_card": null
}
Message Types:
| Type | Description |
|---|---|
text |
Plain text message |
image |
Image attachment |
document |
Document attachment |
voice |
Voice message |
property_inquiry |
Property card message |
5. Mark as Read
POST /api/conversations/{conversation_id}/read
6. Edit Message
PUT /api/conversations/{conversation_id}/messages/{message_id}
Request Body:
{
"content": "Updated message text"
}
Constraints:
- β Sender only
- β Text messages only
- β Within 15 minutes of sending
Error Responses:
403: "You can only edit your own messages"400: "Only text messages can be edited"400: "Edit window expired (15 minutes)"
7. Delete Message
DELETE /api/conversations/{conversation_id}/messages/{message_id}?for=everyone|me
Query Parameter:
| Value | Description | Constraints |
|---|---|---|
everyone |
Delete for all participants | Sender only, within 1 hour |
me |
Delete for yourself only | Anyone, no time limit |
8. Add Reaction
POST /api/conversations/{conversation_id}/messages/{message_id}/reactions
Request Body:
{
"emoji": "π"
}
9. Remove Reaction
DELETE /api/conversations/{conversation_id}/messages/{message_id}/reactions/{emoji}
Example: DELETE /api/conversations/123/messages/456/reactions/π
10. Clear Chat (Persistent)
POST /api/conversations/{conversation_id}/clear
Clears all messages in a conversation for the current user only. Other participant still sees all messages.
β οΈ NEW BEHAVIOR (v2.0): Clear is now persistent across sessions and devices.
When a user clears a chat:
- A
cleared_attimestamp is stored on the conversation- Messages with
created_at <= cleared_atwill never be shown to that user again- This persists even after logout/login or using a new device
- New messages sent AFTER the clear will be visible to both participants
Response:
{
"success": true,
"conversation_id": "674xyz789...",
"cleared_count": 25,
"cleared_at": "2024-12-26T00:22:00.000000"
}
Frontend Implementation:
// Clear chat and handle response
Future<void> clearChat(String conversationId) async {
final response = await api.delete('/conversations/$conversationId/clear');
if (response['success']) {
// Store cleared_at locally for optimistic UI
final clearedAt = response['cleared_at'];
// Remove all messages from local state
setState(() {
messages.clear();
});
// Optionally store cleared_at for reference
await prefs.setString('cleared_$conversationId', clearedAt);
}
}
Important Notes:
- No frontend changes required for basic functionality - the backend handles filtering
- The
cleared_attimestamp is stored server-side on the conversation document - When fetching messages, the server automatically filters out messages before
cleared_at - New messages sent after clearing the chat will appear for both users
11. Online Status
Get Single User Status:
GET /api/conversations/users/{user_id}/online-status
Get Bulk Status:
POST /api/conversations/users/online-status
{
"user_ids": ["user_123", "user_456"]
}
WebSocket API
Connection
final wsUrl = 'wss://api.lojiz.com/ws/chat?token=$jwtToken';
final channel = WebSocketChannel.connect(Uri.parse(wsUrl));
Connection Confirmed
Server sends after successful connection:
{
"action": "connected",
"user_id": "user_123",
"timestamp": "2024-12-24T10:00:00.000000"
}
WebSocket Actions (Client β Server)
Send Message
{
"action": "send_message",
"conversation_id": "674xyz789...",
"message_type": "text",
"content": "Hello!",
"media": null,
"property_card": null
}
Typing Indicator
{
"action": "typing",
"conversation_id": "674xyz789...",
"is_typing": true
}
Mark Read
{
"action": "mark_read",
"conversation_id": "674xyz789..."
}
Heartbeat (Keep Alive)
Send every 30 seconds:
{
"action": "heartbeat"
}
Edit Message
{
"action": "edit_message",
"conversation_id": "674xyz789...",
"message_id": "msg_123",
"new_content": "Updated text"
}
Delete Message
{
"action": "delete_message",
"conversation_id": "674xyz789...",
"message_id": "msg_123",
"delete_for": "everyone"
}
delete_for: "everyone" or "me"
Add Reaction
{
"action": "add_reaction",
"conversation_id": "674xyz789...",
"message_id": "msg_123",
"emoji": "π"
}
Remove Reaction
{
"action": "remove_reaction",
"conversation_id": "674xyz789...",
"message_id": "msg_123",
"emoji": "π"
}
Clear Chat
{
"action": "clear_chat",
"conversation_id": "674xyz789..."
}
WebSocket Events (Server β Client)
New Message
{
"action": "new_message",
"conversation_id": "674xyz789...",
"message": {
"id": "msg_456",
"sender_id": "user_456",
"sender_name": "Jane Smith",
"content": "Hi there!",
"message_type": "text",
"is_edited": false,
"reactions": {},
"created_at": "2024-12-24T11:35:00.000000",
"can_edit": true,
"can_delete_for_everyone": true
}
}
User Typing
{
"action": "user_typing",
"conversation_id": "674xyz789...",
"user_id": "user_456",
"user_name": "Jane Smith",
"is_typing": true
}
Message Read
{
"action": "message_read",
"conversation_id": "674xyz789...",
"read_by": "user_456",
"read_at": "2024-12-24T11:36:00.000000"
}
User Status Changed
{
"action": "user_status_changed",
"user_id": "user_456",
"is_online": true,
"last_seen": null
}
Message Edited
{
"action": "message_edited",
"conversation_id": "674xyz789...",
"message_id": "msg_123",
"new_content": "Updated message",
"edited_at": "2024-12-24T11:40:00.000000",
"edited_by": "user_123"
}
Message Deleted
{
"action": "message_deleted",
"conversation_id": "674xyz789...",
"message_id": "msg_123",
"deleted_for": "everyone",
"deleted_at": "2024-12-24T11:41:00.000000",
"deleted_by": "user_123"
}
Reaction Added
{
"action": "reaction_added",
"conversation_id": "674xyz789...",
"message_id": "msg_123",
"emoji": "π",
"user_id": "user_456"
}
Reaction Removed
{
"action": "reaction_removed",
"conversation_id": "674xyz789...",
"message_id": "msg_123",
"emoji": "π",
"user_id": "user_456"
}
Chat Cleared (Persistent)
Received after successfully clearing a chat:
{
"action": "chat_cleared",
"conversation_id": "674xyz789...",
"cleared_count": 25,
"cleared_at": "2024-12-26T00:22:00.000000"
}
Frontend Handling:
case 'chat_cleared':
final conversationId = event['conversation_id'];
final clearedAt = event['cleared_at'];
// Clear all messages from local state for this conversation
setState(() {
messages.clear();
});
// Store cleared_at for reference (optional)
// The server will filter messages on next fetch automatically
break;
Note: After clearing, the next time you fetch messages for this conversation, the server will only return messages created AFTER the
cleared_attimestamp. This is automatic - no client-side filtering is needed.
Heartbeat Acknowledgment
{
"action": "heartbeat_ack",
"timestamp": "2024-12-24T11:45:00.000000"
}
Message Object Schema
interface Message {
id: string;
conversation_id: string;
sender_id: string;
sender_name: string;
sender_avatar: string | null;
message_type: "text" | "image" | "document" | "voice" | "property_inquiry" | "system";
content: string | null;
media: {
url: string;
filename: string;
size: number;
mime_type: string;
duration?: number; // for voice messages
thumbnail_url?: string; // for images/videos
} | null;
property_card: {
listing_id: string;
title: string;
price: number;
currency: string;
bedrooms: number;
bathrooms: number;
location: string;
image_url: string;
listing_type: string;
} | null;
is_read: boolean;
read_at: string | null; // ISO datetime
is_edited: boolean;
edited_at: string | null; // ISO datetime
is_deleted: boolean;
deleted_at: string | null; // ISO datetime
reactions: {
[emoji: string]: string[]; // emoji -> array of user IDs
};
created_at: string; // ISO datetime
// Action availability (computed by server)
can_edit: boolean; // true if sender + text + <15 min
can_delete_for_everyone: boolean; // true if sender + <1 hour
}
UI Implementation Guide
Show/Hide Edit Button
if (message.canEdit) {
// Show edit button in message options
}
Show/Hide Delete Options
// "Delete for everyone" - only when allowed
if (message.canDeleteForEveryone) {
showDeleteForEveryoneOption();
}
// "Delete for me" - always available
showDeleteForMeOption();
Display Edited Label
if (message.isEdited) {
Text("(edited)", style: TextStyle(fontSize: 12, color: Colors.grey));
}
Display Deleted Message
if (message.isDeleted) {
Text("π« This message was deleted",
style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey));
}
Display Reactions
Row(
children: message.reactions.entries.map((entry) {
final emoji = entry.key;
final users = entry.value;
return Chip(
label: Text("$emoji ${users.length}"),
onPressed: () => toggleReaction(message.id, emoji),
);
}).toList(),
)
Handle WebSocket Events
channel.stream.listen((data) {
final event = jsonDecode(data);
switch (event['action']) {
case 'new_message':
addMessage(event['message']);
break;
case 'message_edited':
updateMessageContent(event['message_id'], event['new_content']);
break;
case 'message_deleted':
if (event['deleted_for'] == 'everyone') {
markMessageAsDeleted(event['message_id']);
} else {
removeMessageFromList(event['message_id']);
}
break;
case 'reaction_added':
addReactionToMessage(event['message_id'], event['emoji'], event['user_id']);
break;
case 'reaction_removed':
removeReactionFromMessage(event['message_id'], event['emoji'], event['user_id']);
break;
case 'user_typing':
showTypingIndicator(event['user_id'], event['is_typing']);
break;
case 'user_status_changed':
updateOnlineStatus(event['user_id'], event['is_online']);
break;
}
});
Time Windows Summary
| Feature | Time Limit | Who Can Do It | Persistent? |
|---|---|---|---|
| Edit message | 15 minutes | Sender only | N/A |
| Delete for everyone | 1 hour | Sender only | Yes |
| Delete for me | No limit | Anyone | Yes |
| Add reaction | No limit | Any participant | Yes |
| Clear chat | No limit | Current user only | Yes (NEW) |
Clear Chat - Persistence Details
How It Works
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLEAR CHAT FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. User A clears chat β
β βββ Backend stores: conversation.cleared_at.userA = now() β
β βββ Backend marks existing messages with deleted_for β
β β
β 2. User A logs out and logs back in (or uses new device) β
β βββ Fetches messages for conversation β
β βββ Backend query: WHERE created_at > cleared_at.userA β
β βββ Returns: Empty list (all messages were before clear) β
β β
β 3. User B (other participant) fetches same conversation β
β βββ Backend query: WHERE created_at > cleared_at.userB β
β βββ cleared_at.userB is NULL (never cleared) β
β βββ Returns: All messages (sees everything) β
β β
β 4. User B sends new message AFTER User A cleared β
β βββ New message created_at > User A's cleared_at β
β βββ Both users can see the new message β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Points for Frontend
- No client-side filtering needed - The server handles all filtering based on
cleared_at - Clear is immediate - After clearing, fetch messages returns empty or only new messages
- Persists across sessions - User won't see old messages even after logout/login
- Persists across devices - User won't see old messages on any device
- Other user unaffected - The other participant sees all messages as normal
- New messages visible - Messages sent AFTER the clear are visible to both users