TAG API — ENDPOINT REFERENCE
=============================
Base URL : https://tag.nsamaandcompany.com/tag_api
Version  : v1
Auth     : Bearer token in Authorization header (required on all endpoints marked [AUTH])
Dates    : All dates returned in ISO 8601 UTC format  e.g. 2026-05-29T20:38:01Z


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SYSTEM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/health
  Check if the API is running.
  Returns: { app: "TAG" }


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AUTH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

POST /api/v1/auth/request-otp
  Send an OTP code to the user's email.
  Body: { phone, email }

POST /api/v1/auth/verify-otp
  Verify the OTP code.
  Body: { phone, email, otp_code, purpose }

POST /api/v1/auth/register
  Register a new user after OTP is verified.
  Body: { phone, email, full_name, age, sex, otp_code }
  Returns: { token, user }

POST /api/v1/auth/login
  Log in with OTP code.
  Body: { phone, email, otp_code }
  Returns: { token, user }

GET  /api/v1/auth/me                          [AUTH]
  Get the currently logged-in user's info.

POST /api/v1/auth/logout                      [AUTH]
  Log out and invalidate the device JWT.

GET  /api/v1/auth/firebase/custom-token       [AUTH]
  Get a Firebase custom token for RTDB and FCM access.
  Returns: { firebase_token }

POST /api/v1/auth/update-device-token         [AUTH]
  Register or update the FCM push token for this device.
  Body: { push_token, device_uuid, device_name, platform }
  Note: device_uuid should be a stable UUID generated once per device install.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PROFILE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/profile                          [AUTH]
  Get your own profile.
  Returns: { id, public_tag_id, full_name, phone, email, age, sex, avatar_url, status }

POST /api/v1/profile/update                   [AUTH]
  Update your profile name, age, and sex.
  Body: { full_name, age, sex }

POST /api/v1/profile/avatar                   [AUTH]
  Upload a profile photo.
  Body: multipart/form-data  field: avatar (image file)
  Returns: { avatar_url }

GET  /api/v1/profile/qr                       [AUTH]
  Generate a shareable QR contact token.
  Returns: { qr_token, qr_payload, scan_endpoint }
  Note: Other users scan this to add you to their contacts.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CONTACTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/contacts                         [AUTH]
  List all contacts in your tag-book.
  Returns: [ { id, alias_name, contact: { user_id, public_tag_id, full_name, avatar_url, status } } ]
  Note: Use contact.user_id as recipient_id when starting a DM.

POST /api/v1/contacts/scan-qr                 [AUTH]
  Add a contact by scanning their QR token.
  Body: { qr_token }
  Returns: { contact_user_id, full_name, avatar_url }
  Note: Relationship is one-way — A adding B does not add A to B's contacts.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TAG SPACES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/tag-spaces                       [AUTH]
  List all active tag spaces.
  Returns: [ { id, uuid, title, description, visibility, active_members, posts_count } ]

POST /api/v1/tag-spaces                       [AUTH]
  Create a new tag space.
  Body: { title, description?, visibility (public|private), max_members? }
  Returns: { id, uuid, title, visibility, join_code (null if public) }

GET  /api/v1/tag-spaces/{uuid}                [AUTH]
  Get details of a single space.

POST /api/v1/tag-spaces/{uuid}/join           [AUTH]
  Join a space.
  Body: {} for public spaces.  { join_code } for private spaces.
  Returns: { session_id, space_tag_code }
  Note: space_tag_code is your anonymous identity inside this space. Save it.

POST /api/v1/tag-spaces/{uuid}/leave          [AUTH]
  Leave a space.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TAG SPACE POSTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/tag-spaces/{uuid}/feed           [AUTH]
  Get all posts in a space.
  Returns: [ { id, body, author: { space_tag_code }, media[], comments[], reactions } ]

POST /api/v1/tag-spaces/{uuid}/posts          [AUTH]
  Create a post inside a space. Must be a member.
  Body: multipart/form-data  fields: body (text), media_files[] (images/video/audio)
  Note: body or at least one media file is required.

POST /api/v1/tag-spaces/posts/{id}/comments   [AUTH]
  Comment on a space post. Must be a member.
  Body: { body, parent_comment_id? }

POST /api/v1/tag-spaces/posts/{id}/reactions/toggle  [AUTH]
  Toggle a reaction on a space post. Must be a member.
  Body: { reaction_type }  options: like | love | fire | laugh | sad | angry

POST /api/v1/tag-space-posts/{id}/share-to-public    [AUTH]
  Share a space post to the public timeline.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TAG SPACE CHAT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Note: Must join the space first. Senders are identified by space_tag_code (anonymous).

GET  /api/v1/tag-spaces/{uuid}/chat           [AUTH]
  Get chat message history for a space.
  Query: page (default 1), limit (default 50, max 100)
  Returns: { page, limit, has_more, messages: [ { id, type, body, media_url, sent_at, sender_code } ] }

POST /api/v1/tag-spaces/{uuid}/chat           [AUTH]
  Send a message to a space chat.
  Body (text):  JSON  { type: "text", body: "hello" }
  Body (media): multipart/form-data  { type: "image"|"audio"|"video", media: <file> }
  Returns: { id, type, body, media_url, sender_code, sent_at }

DELETE /api/v1/tag-spaces/{uuid}/chat/{id}    [AUTH]
  Delete your own chat message. Only sender can delete.

Real-time: Listen to Firebase RTDB  chats/spaces/{uuid}/last_event
  On change: re-fetch latest messages from GET /chat
Push: FCM payload  { type: "space_message", space_uuid, sender_code }


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PUBLIC POSTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/public-posts                     [AUTH]
  Get the public timeline feed.

POST /api/v1/public-posts                     [AUTH]
  Create a public post.
  Body: multipart/form-data  fields: body (text), media_files[] (images/video/audio)

GET  /api/v1/public-posts/{id}                [AUTH]
  Get a single public post.

POST /api/v1/public-posts/{id}/comments       [AUTH]
  Comment on a public post.
  Body: { body, parent_comment_id? }

POST /api/v1/public-posts/{id}/reactions/toggle  [AUTH]
  Toggle a reaction on a public post.
  Body: { reaction_type }  options: like | love | fire | laugh | sad | angry


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TAG VIBES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/tag-vibes                        [AUTH]
  Get the vibes feed (short-form audio/video).

POST /api/v1/tag-vibes                        [AUTH]
  Upload a new vibe.
  Body: multipart/form-data
    fields: body?, vibe_file (audio or video), thumbnail (required for video)

GET  /api/v1/tag-vibes/{id}                   [AUTH]
  Get a single vibe.

POST /api/v1/tag-vibes/{id}/comments          [AUTH]
  Comment on a vibe.
  Body: { body }

POST /api/v1/tag-vibes/{id}/reactions/toggle  [AUTH]
  Toggle a reaction on a vibe.
  Body: { reaction_type }  options: like | love | fire | laugh | sad | angry

POST /api/v1/tag-vibes/{id}/remaster          [AUTH]
  Create a remastered version of an existing vibe.
  Body: multipart/form-data  fields: vibe_file, thumbnail?


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SEARCH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/search/snippets                  [AUTH]
  Search public posts (snippets).
  Query: q (search term)

GET  /api/v1/search/vibes                     [AUTH]
  Search tag vibes.
  Query: q (search term)

GET  /api/v1/search/spaces                    [AUTH]
  Search tag spaces.
  Query: q (search term)

GET  /api/v1/search/trending                  [AUTH]
  Get trending content across the platform.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DIRECT MESSAGES (DMs)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/conversations                    [AUTH]
  List all your DM conversations.
  Returns: [ { id, unread_count, last_message, other_user: { id, full_name, tag_id, avatar_url } } ]

POST /api/v1/conversations                    [AUTH]
  Start a new DM with someone (or return existing one).
  Body: { recipient_id }
  Returns: { id, created_at }
  Note: If a conversation already exists it returns it with HTTP 200, not 201.

GET  /api/v1/conversations/unread-count       [AUTH]
  Get total unread message count across all DMs (for app badge).
  Returns: { unread_count }

GET  /api/v1/conversations/{id}               [AUTH]
  Get a single conversation and the other user's profile.
  Returns: { id, other_user: { id, full_name, tag_id, avatar_url, last_seen_at } }

GET  /api/v1/conversations/{id}/messages      [AUTH]
  Get paginated message history. Oldest first within each page.
  Query: page (default 1), limit (default 30, max 100)
  Returns: { page, limit, has_more, messages: [ { id, type, body, media_url, sent_at, read_at, sender } ] }

POST /api/v1/conversations/{id}/messages      [AUTH]
  Send a message.
  Body (text):  JSON  { type: "text", body: "hello" }
  Body (media): multipart/form-data  { type: "image"|"audio"|"video", media: <file> }
  Returns: { id, type, body, media_url, sent_at, read_at, sender }

PATCH /api/v1/conversations/{id}/read         [AUTH]
  Mark all messages in this conversation as read.
  Call this when the user opens the conversation screen.

DELETE /api/v1/conversations/{id}/messages/{msgId}  [AUTH]
  Delete your own message. Only sender can delete.

Real-time: Listen to Firebase RTDB  chats/direct/{conversation_id}/last_event
  On change: re-fetch latest message or refresh conversation list
Push: FCM payload  { type: "direct_message", conversation_id }


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MEDIA UPLOAD RULES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Images : JPG, PNG, WebP  — max 5 MB
Audio  : MP3, WAV, OGG, M4A — max 15 MB
Video  : MP4, WebM, MOV  — max 50 MB
          Videos must include a separate thumbnail image field.

Field name for single file uploads  : media
Field name for multiple file uploads: media_files[]


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STANDARD RESPONSE FORMAT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

All responses follow this shape:
  { "status": true|false, "message": "...", "data": ... }

Common HTTP status codes:
  200  Success
  201  Created
  400  Bad request
  401  Unauthenticated (missing or expired token)
  403  Forbidden (not allowed to perform this action)
  404  Not found
  422  Validation error  (data field contains per-field error messages)


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REAL-TIME & NOTIFICATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. After login, call GET /auth/firebase/custom-token
2. Sign into Firebase with the token
3. Listen to RTDB paths as documented per feature
4. Register FCM token via POST /auth/update-device-token

FCM data.type values:
  direct_message  — new DM received  → data.conversation_id
  space_message   — new space chat   → data.space_uuid, data.sender_code


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TOTAL ENDPOINTS: 42
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
System        1
Auth          8
Profile       4
Contacts      2
Tag Spaces    5
Space Posts   5
Space Chat    3
Public Posts  5
Tag Vibes     6
Search        4
Direct DMs    8
            ───
Total        51
