ryOS ryOS / Docs
GitHub Launch

Utility APIs

ryOS provides several utility API endpoints for common operations like fetching link previews, checking iframe embeddability, sharing applets, Sync v2, and admin operations. These endpoints are implemented as Node.js API functions.

Many utility endpoints now use the shared apiHandler + request-auth utilities for consistent CORS/auth/method handling.

Link Preview

Fetches metadata from URLs for rich link previews. Supports Open Graph, Twitter Cards, and standard meta tags with special handling for YouTube URLs.

Endpoint

MethodPathDescription
GET/api/link-previewFetch URL metadata for previews

Request

Query Parameters:
ParameterTypeRequiredDescription
urlstringYesThe URL to fetch metadata from (must be HTTP or HTTPS)

Response

Success Response (200):
{
  "url": "https://example.com/page",
  "title": "Page Title",
  "description": "Page description from meta tags",
  "image": "https://example.com/og-image.jpg",
  "siteName": "Example Site"
}
Response Fields:
FieldTypeDescription
urlstringThe original URL
titlestring?Page title (from <title>, og:title, or twitter:title)
descriptionstring?Page description (from og:description, twitter:description, or meta description)
imagestring?Preview image URL (from og:image or twitter:image)
siteNamestring?Site name (from og:site_name or hostname)
Error Responses:
StatusDescription
400Missing or invalid URL
403Unauthorized origin
405Method not allowed
408Request timeout
429Rate limit exceeded
503Network error

Rate Limits

  • Global limit: 10 requests per minute per IP
  • Per-host limit: 5 requests per minute per IP per target hostname

Example

// Fetch link preview
const response = await fetch('/api/link-preview?url=https://github.com');
const metadata = await response.json();

console.log(metadata);
// {
//   url: "https://github.com",
//   title: "GitHub: Let's build from here",
//   description: "GitHub is where over 100 million developers...",
//   image: "https://github.githubassets.com/images/modules/site/social-cards/campaign-social.png",
//   siteName: "GitHub"
// }

Notes

  • YouTube URLs are handled specially using the oEmbed API for reliable metadata
  • Responses are cached for 1 hour (Cache-Control: public, max-age=3600)
  • HTML entities in metadata are automatically decoded
  • Relative image URLs are converted to absolute URLs

iFrame Check

Checks if a URL allows iframe embedding and optionally proxies content to bypass embedding restrictions. Also supports retrieving cached historical versions of pages.

Endpoint

MethodPathDescription
GET/api/iframe-checkCheck iframe embeddability or proxy content

Request

Query Parameters:
ParameterTypeRequiredDescription
urlstringYesThe URL to check or proxy
modestringNoOperation mode: check, proxy, ai, or list-cache (default: proxy)
yearstringNoYear for Wayback Machine or AI cache (e.g., "2020", "1000 BC")
monthstringNoMonth for Wayback Machine (e.g., "01" for January)
themestringNoTheme name - font overrides are skipped for "macosx" theme

Modes

check Mode

Performs a header-only check to determine if the URL allows iframe embedding.

Response (200):
{
  "allowed": true,
  "reason": null,
  "title": "Page Title"
}
{
  "allowed": false,
  "reason": "X-Frame-Options: DENY",
  "title": "Page Title"
}

proxy Mode (Default)

Proxies the content with embedding-blocking headers removed. Injects:

  • <base> tag for relative URL resolution
  • Click interceptor script for navigation handling
  • History API patch for cross-origin compatibility
  • Font override styles (unless theme is "macosx")

Response: The proxied HTML content with modified headers. Custom Response Headers:
HeaderDescription
X-Proxied-Page-TitleURL-encoded page title (if available)
X-Wayback-Cache"HIT" if content was served from Wayback cache

ai Mode

Retrieves AI-generated historical page versions from cache.

Additional Parameters:
ParameterTypeRequiredDescription
yearstringYesHistorical year (e.g., "1995", "500 BC", "1 CE", "current")
Response (200) - Cache Hit:

Returns the cached HTML content with header X-AI-Cache: HIT.

Response (404) - Cache Miss:
{
  "aiCache": false
}

list-cache Mode

Lists all available cached years (both AI-generated and Wayback Machine) for a URL.

Response (200):
{
  "years": ["current", "2020", "2015", "2010", "1999", "500 BC"]
}

Auto-Proxy Domains

The following domains are automatically proxied regardless of mode:

  • wikipedia.org (and subdomains)
  • wikimedia.org (and subdomains)
  • wikipedia.com
  • cursor.com

Rate Limits

ModeGlobal LimitPer-Host Limit
check / proxy300/min per IP100/min per IP per host
ai / list-cache120/min per IPN/A

Example

// Check if URL can be embedded
const checkResponse = await fetch('/api/iframe-check?url=https://example.com&mode=check');
const { allowed, reason } = await checkResponse.json();

if (!allowed) {
  // Use proxy mode instead
  const iframeSrc = `/api/iframe-check?url=https://example.com&mode=proxy`;
  iframe.src = iframeSrc;
}

// List cached historical versions
const cacheResponse = await fetch('/api/iframe-check?url=https://example.com&mode=list-cache');
const { years } = await cacheResponse.json();
// years: ["2020", "2015", "2010"]

// Load Wayback Machine version
const waybackSrc = `/api/iframe-check?url=https://example.com&mode=proxy&year=2015&month=06`;

Share Applet

Manages sharing of user-created applets (mini HTML/JS applications). Supports creating, retrieving, updating, and deleting shared applets.

Endpoint

MethodPathDescription
GET/api/share-appletRetrieve applet by ID or list all applets
POST/api/share-appletSave or update an applet
DELETE/api/share-appletDelete an applet (admin only)
PATCH/api/share-appletUpdate applet metadata (admin only)

Authentication

Most operations require authentication via headers:

HeaderDescription
AuthorizationBearer token: Bearer <token>
X-UsernameUsername of the authenticated user

GET - Retrieve Applet

Query Parameters:
ParameterTypeRequiredDescription
idstringNoApplet ID to retrieve
liststringNoSet to "true" to list all applets

*One of id or list=true is required.

Response - Single Applet (200):
{
  "content": "<html>...</html>",
  "title": "My Applet",
  "name": "my-applet",
  "icon": "game",
  "windowWidth": 400,
  "windowHeight": 300,
  "createdAt": 1704067200000,
  "createdBy": "username",
  "featured": false
}
Response - List Applets (200):
{
  "applets": [
    {
      "id": "abc123...",
      "title": "Featured Applet",
      "name": "featured-applet",
      "icon": "star",
      "createdAt": 1704067200000,
      "featured": true,
      "createdBy": "ryo"
    },
    {
      "id": "def456...",
      "title": "My Applet",
      "name": "my-applet",
      "icon": "game",
      "createdAt": 1704000000000,
      "featured": false,
      "createdBy": "user123"
    }
  ]
}

POST - Save Applet

Requires authentication.

Request Body:
FieldTypeRequiredDescription
contentstringYesHTML content of the applet
titlestringNoDisplay title
namestringNoApplet name/slug
iconstringNoEmoji or icon
windowWidthnumberNoDefault window width
windowHeightnumberNoDefault window height
shareIdstringNoExisting ID to update (author must match)
Response (200):
{
  "id": "abc123def456...",
  "shareUrl": "https://os.ryo.lu/applet-viewer/abc123def456...",
  "updated": false,
  "createdAt": 1704067200000
}
Response Fields:
FieldTypeDescription
idstringUnique applet ID (32 characters)
shareUrlstringFull URL to view the applet
updatedbooleanWhether an existing applet was updated
createdAtnumberTimestamp of creation/update

DELETE - Delete Applet (Admin Only)

Requires admin authentication (user "ryo" with valid token).

Query Parameters:
ParameterTypeRequiredDescription
idstringYesApplet ID to delete
Response (200):
{
  "success": true
}

PATCH - Update Applet (Admin Only)

Currently only supports updating the featured status.

Query Parameters:
ParameterTypeRequiredDescription
idstringYesApplet ID to update
Request Body:
{
  "featured": true
}
Response (200):
{
  "success": true,
  "featured": true
}

Example

// Save a new applet
const response = await fetch('/api/share-applet', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
    'X-Username': username
  },
  body: JSON.stringify({
    content: '<html><body><h1>Hello World</h1></body></html>',
    title: 'Hello World Applet',
    icon: '👋',
    windowWidth: 400,
    windowHeight: 300
  })
});

const { id, shareUrl } = await response.json();
console.log(`Applet shared at: ${shareUrl}`);

// Retrieve an applet
const applet = await fetch(`/api/share-applet?id=${id}`).then(r => r.json());

// List all applets
const { applets } = await fetch('/api/share-applet?list=true').then(r => r.json());

Stocks API

Provides real-time stock quote data for the Dashboard Stocks widget. Uses Yahoo Finance for quote and chart data.

Endpoint

MethodPathDescription
GET/api/stocksFetch stock quotes and optional chart data

Request

Query Parameters:
ParameterTypeRequiredDescription
symbolsstringYesComma-separated stock symbols (max 20)
chartstringNoSymbol to fetch chart data for
rangestringNoChart time range: 1d, 5d, 1mo, 3mo, 6mo (default), 1y, 2y

Response

Success (200):
{
  "quotes": [
    {
      "symbol": "AAPL",
      "price": 198.50,
      "change": 2.30,
      "changePercent": 1.17,
      "name": "Apple Inc."
    }
  ],
  "chart": [
    {
      "timestamp": 1704067200000,
      "close": 195.20
    }
  ]
}
Response Fields:
FieldTypeDescription
quotesStockQuote[]Array of stock quotes
chartChartPoint[]Optional chart data (only when chart param provided)
Error Responses:
StatusDescription
400Missing symbols parameter or no valid symbols
500Failed to fetch stock data

Caching

Responses include Cache-Control: public, max-age=60, stale-while-revalidate=300.

Example

const response = await fetch('/api/stocks?symbols=AAPL,GOOGL,MSFT&chart=AAPL&range=1mo');
const { quotes, chart } = await response.json();

Currency Rate

Proxies foreign-exchange rates for currency conversion features. Uses Frankfurter as the primary source and open.er-api.com as a fallback.

Endpoint

MethodPathDescription
GET/api/currency-rateFetch the exchange rate for a currency pair

Request

Query Parameters:
ParameterTypeRequiredDescription
fromstringYes3-letter base currency code (e.g. USD)
tostringYes3-letter quote currency code (e.g. EUR)

Response

Success (200):
{
  "rate": 0.92,
  "rateDate": "2026-02-29",
  "source": "frankfurter"
}
FieldTypeDescription
ratenumberUnits of to per 1 unit of from
rateDatestringDate the rate is valid for (YYYY-MM-DD)
sourcestringfrankfurter, open.er-api, or identity (when from === to)
Error Responses:
StatusDescription
400from/to are not valid 3-letter currency codes
429Rate limit exceeded
502Could not load exchange rate from either source

Rate Limits

ScopeLimitWindow
Burst30 requests1 minute
Daily2000 requests24 hours

Apple Token APIs

Mint short-lived Apple-issued ES256 JWTs for client-side Apple SDKs. Both endpoints sign and cache a token in memory, returning the same { token, expiresAt } shape and a Cache-Control: private header. They return 500 (with the missing env vars) when not configured.

Endpoints

MethodPathDescription
GET/api/mapkit-tokenMapKit JS developer token (Maps app + AI maps tools)
GET/api/musickit-tokenMusicKit JS developer token (iPod "Apple Music" mode)

Response

Success (200):
{
  "token": "eyJ...",
  "expiresAt": 1704067200000
}
EndpointToken TTLResponse cache
/api/mapkit-token30 minutes5 minutes
/api/musickit-token7 days30 minutes
musickit-token falls back to the MAPKIT_* key material when the MUSICKIT_* variants are unset (see Environment Variables).

TV Create Channel

AI-assisted creation of a themed YouTube "TV channel" for the TV app. Given a short description, an AI planner produces a channel name, tagline, and 2–4 diverse YouTube search queries, then fans those out against the YouTube Data API and returns a de-duplicated video lineup. Requires authentication.

Endpoint

MethodPathDescription
POST/api/tv/create-channelGenerate a themed channel + video lineup

Request

{
  "description": "lofi study beats"
}
FieldTypeRequiredDescription
descriptionstringYesChannel idea (2–280 characters)

Response

Success (200):
{
  "name": "Lofi Desk",
  "description": "mellow beats to study to",
  "queries": ["lofi hip hop study", "chillhop instrumental", "jazzy lofi mix"],
  "videos": [
    { "id": "abc123", "url": "https://youtu.be/abc123", "title": "Lofi Mix", "artist": "Channel Name" }
  ]
}
Error Responses:
StatusDescription
404No videos found for the channel idea
429Rate limit exceeded (burst or daily)
500YouTube API not configured
502AI failed to plan the channel

Rate Limits

Limited per username (and per IP as a backstop):

ScopeLimitWindow
Burst5 requests1 minute
Daily30 requests24 hours

Analytics Events

First-party product-analytics ingestion. Accepts a batch of client events; malformed events are dropped per-event and the batch is capped server-side. Auth is optional (a username is recorded when present). The endpoint resolves a coarse country bucket from the request but never persists the raw IP.

Endpoint

MethodPathDescription
POST/api/analytics/eventsRecord a batch of analytics events

Request

{
  "events": [
    { "name": "app_open", "ts": 1704067200000, "props": { "app": "finder" } }
  ]
}

Only events (an array) is required; individual event shapes are validated/sanitized downstream.

Response

Success: 204 No Content (empty body).

Sync APIs

Cloud sync uses a journal-based delta protocol (v2): all synced state is a per-user map of key → document, changes travel as ops with hybrid-logical- clock timestamps, and a single integer cursor (seq) answers "am I up to date?". Conflicts resolve per key via last-writer-wins; losing writes receive the winning entry inline (no 409s, no retry loops). See docs/proposals/cloud-sync-v2.md for the full design.

Endpoint Summary

MethodPathDescriptionAuth
POST/api/sync/v2/opsApply a batch of ops (per-key LWW)Required
GET/api/sync/v2/changes?since={seq}Ops after the cursor, or snapshotRequiredRequired
GET/api/sync/v2/snapshotFull key-value state + current seqRequired
POST/api/sync/v2/blobsBatch blob upload prepare (sha256 dedupe) + download URL signingRequired
GET/api/cron/sync-maintenanceMaintenance cron: blob GC + user-record healingCRON_SECRET
GET/PUT/api/sync/auto-sync-preferenceCross-device Auto Sync toggleRequired

POST /api/sync/v2/ops

Request:

{
  "clientId": "c4f2a9d1e8b30067",
  "ops": [
    { "k": "settings/theme", "v": { "current": "macosx" }, "t": "01718180000000-0000-c4f2a9d1e8b30067" },
    { "k": "stickies/note:abc", "del": true, "t": "01718180000200-0001-c4f2a9d1e8b30067" }
  ]
}

Response (200 even when individual ops lose):

{
  "ok": true,
  "seq": 1042,
  "results": [
    { "k": "settings/theme", "accepted": true, "seq": 1042 },
    { "k": "stickies/note:abc", "accepted": false,
      "winner": { "v": { "...": "..." }, "t": "01718180000500-0000-other", "seq": 1040 } }
  ]
}

Accepted ops are appended to the user's journal and broadcast on the private-sync-{username} channel as a sync-ops event with the ops inlined when small (receivers apply them with zero HTTP requests). Client HLC timestamps are clamped to serverNow + 5min.

GET /api/sync/v2/changes?since={seq}

Returns { ok, seq, ops: [...] } with the ops after since, { ops: [] } when up to date, or { seq, snapshotRequired: true } when the journal no longer covers the cursor (bounded retention; fall back to the snapshot). The ops are coalesced per key — only the newest op for each key in the window is returned (under LWW the client only needs each key's latest value to converge), while seq still reflects the server cursor.

GET /api/sync/v2/snapshot

Returns { ok, seq, entries } where entries maps every key to { v?, del?, t, seq }. Used for new-device bootstrap, journal-gap recovery, and force download. Supports ?prefix= for partial reads.

POST /api/sync/v2/blobs

Binary content (images, trash, applets, wallpapers) is content-addressed at sync/{username}/blobs/{sha256}.gz. One batched request prepares any number of uploads — blobs the server already knows are skipped entirely — and signs download URLs for the user's own objects:

{
  "upload": [{ "sha256": "ab12…", "size": 48211 }],
  "download": ["s3://bucket/sync/alice/blobs/cd34….gz"]
}

Response: per-digest { exists: true, url } (skip upload) or { exists: false, upload: { …storage instruction… } }, plus signed downloads aligned with the request array (null for foreign URLs).

GET /api/cron/sync-maintenance

Scheduled maintenance (daily via an external scheduler such as a Coolify cron task; authenticated with Authorization: Bearer ${CRON_SECRET}, like the Telegram heartbeat cron). Each run processes a bounded batch of users, walking the whole user base across runs via a scan cursor persisted at sync:maintenance:cursor:

  1. Blob garbage collection — content-addressed blobs whose digest is no
longer referenced by any KV document are deleted via mark-and-sweep: a blob must stay unreferenced across runs spanning a 24h grace window before its storage object is removed (re-referencing clears the mark).
  1. User record healing — user records persist forever; stale
TTLs left by the removed expire-on-room-message path are cleared.

Response:

{
  "success": true,
  "usersProcessed": 25,
  "scanComplete": false,
  "blobsMarked": 4,
  "blobsUnmarked": 0,
  "blobsDeleted": 2,
  "userRecordsPersisted": 3,
  "errors": 0
}

Admin API

Administrative endpoints for user, memory, analytics, Cursor-agent, Redis-browser, and moderation operations. All operations require admin authentication (user ryo with valid token).

The endpoint is rate-limited to 30 admin requests/minute.

Endpoint

MethodPathDescription
GET/api/adminQuery users and statistics
POST/api/adminPerform admin actions

Authentication

All admin endpoints require:

HeaderDescription
AuthorizationBearer token: Bearer <token>
X-UsernameMust be "ryo"

Unauthorized requests return 403 Forbidden.

GET Operations

Query Parameters:
ParameterTypeRequiredDescription
actionstringYesOperation to perform
usernamestringFor some actionsTarget username
limitnumberNoResult limit (default: 50)
Supported GET actions:
ActionDescription
getStatsSystem-wide user, room, and message statistics
getAllUsersRegistered users and ban status
getUserProfileDetailed user profile, rooms, and message counts
getUserMessagesRecent messages for a user
getUserMemoriesLong-term memories and daily notes for a user
getUserHeartbeatsRecent global-presence heartbeats for a user
getServerInfoDeployment, Redis, storage, realtime, and environment health
getAnalyticsAPI/product analytics summary or detail (detail=true)
getCursorAgentRunsRecent Cursor cloud-agent run records
listRedisKeysRedis browser key scan with summaries
getRedisKeyRedis key document preview
backupRedisKeysExport matching Redis key documents
getAuditLogAppend-only admin action audit log

getStats Action

Get system-wide statistics.

Response:
{
  "totalUsers": 150,
  "totalRooms": 12,
  "totalMessages": 5430
}

getAllUsers Action

List all registered users.

Response:
{
  "users": [
    {
      "username": "alice",
      "lastActive": 1704067200000,
      "banned": false
    },
    {
      "username": "bob",
      "lastActive": 1704000000000,
      "banned": true
    }
  ]
}

getUserProfile Action

Get detailed profile for a specific user.

Additional Parameters:
ParameterTypeRequiredDescription
usernamestringYesTarget username
Response:
{
  "username": "alice",
  "lastActive": 1704067200000,
  "banned": false,
  "banReason": null,
  "bannedAt": null,
  "messageCount": 42,
  "rooms": [
    { "id": "general", "name": "General Chat" },
    { "id": "random", "name": "Random" }
  ]
}

getUserMessages Action

Get recent messages from a specific user.

Additional Parameters:
ParameterTypeRequiredDescription
usernamestringYesTarget username
limitnumberNoMaximum messages to return (default: 50)
Response:
{
  "messages": [
    {
      "id": "msg123",
      "roomId": "general",
      "roomName": "General Chat",
      "content": "Hello everyone!",
      "timestamp": 1704067200000
    }
  ]
}

getUserMemories Action

Get long-term memories plus recent daily notes for a user.

Additional Parameters:
ParameterTypeRequiredDescription
usernamestringYesTarget username
Response:
{
  "memories": [
    {
      "key": "work",
      "summary": "Design lead at Cursor",
      "content": "...",
      "createdAt": 1704067200000,
      "updatedAt": 1704153600000
    }
  ],
  "dailyNotes": [
    {
      "date": "2026-02-29",
      "entries": [{ "timestamp": 1704067200000, "content": "..." }],
      "processedForMemories": false
    }
  ]
}

POST Operations

Request Body:
FieldTypeRequiredDescription
actionstringYesOperation to perform
targetUsernamestringYesUser to act on
reasonstringNoReason for action (for bans)
Supported POST actions:
ActionDescription
deleteUserPermanently delete a user account
banUserBan a user and invalidate sessions
unbanUserRemove a user's ban
clearUserMemoriesDelete long-term memories for a user
forceProcessDailyNotesReprocess recent daily notes into memories
startCursorAgentStart a Cursor cloud-agent run from Admin
deleteRedisKeyDelete one Redis key after key confirmation

deleteUser Action

Permanently delete a user account (cannot delete admin "ryo").

Request:
{
  "action": "deleteUser",
  "targetUsername": "spammer"
}
Response:
{
  "success": true
}
Effects:
  • Deletes user record
  • Deletes password hash
  • Invalidates all authentication tokens

banUser Action

Ban a user from the chat system (cannot ban admin "ryo").

Request:
{
  "action": "banUser",
  "targetUsername": "troublemaker",
  "reason": "Spamming in chat"
}
Response:
{
  "success": true
}
Effects:
  • Sets banned: true on user record
  • Stores ban reason and timestamp
  • Invalidates all authentication tokens (forces logout)

unbanUser Action

Remove ban from a user.

Request:
{
  "action": "unbanUser",
  "targetUsername": "reformed-user"
}
Response:
{
  "success": true
}

clearUserMemories Action

Delete all long-term memories for a user.

{
  "action": "clearUserMemories",
  "targetUsername": "alice"
}

Response:

{
  "success": true,
  "deletedCount": 12,
  "message": "Cleared 12 memories for alice"
}

forceProcessDailyNotes Action

Reset processed flags and force reprocessing of recent daily notes into long-term memories.

{
  "action": "forceProcessDailyNotes",
  "targetUsername": "alice"
}

Response:

{
  "success": true,
  "notesReset": 7,
  "notesProcessed": 6,
  "memoriesCreated": 2,
  "memoriesUpdated": 1,
  "dates": ["2026-02-23", "2026-02-24"],
  "skippedDates": []
}

Example

const adminHeaders = {
  'Authorization': `Bearer ${adminToken}`,
  'X-Username': 'ryo'
};

// Get system statistics
const stats = await fetch('/api/admin?action=getStats', {
  headers: adminHeaders
}).then(r => r.json());

// Get all users
const { users } = await fetch('/api/admin?action=getAllUsers', {
  headers: adminHeaders
}).then(r => r.json());

// Get user profile
const profile = await fetch('/api/admin?action=getUserProfile&username=alice', {
  headers: adminHeaders
}).then(r => r.json());

// Ban a user
await fetch('/api/admin', {
  method: 'POST',
  headers: {
    ...adminHeaders,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    action: 'banUser',
    targetUsername: 'spammer',
    reason: 'Repeated spam violations'
  })
});

Telegram Link API

Manages linking ryOS accounts to Telegram for cross-platform notifications and chat integration. All endpoints require authentication.

Endpoint Summary

MethodPathDescriptionAuth
POST/api/telegram/link/createCreate a Telegram link codeRequired
GET/api/telegram/link/statusCheck link statusRequired
POST/api/telegram/link/disconnectDisconnect Telegram accountRequired

POST /api/telegram/link/create

Generates a link code for connecting a Telegram account. If a pending link already exists, returns the existing code.

Response (200):
{
  "code": "abc123",
  "expiresIn": 300,
  "botUsername": "ryos_bot",
  "deepLink": "https://t.me/ryos_bot?start=link_abc123",
  "linkedAccount": null
}

If already linked, linkedAccount is populated:

{
  "code": "abc123",
  "expiresIn": 300,
  "botUsername": "ryos_bot",
  "deepLink": "https://t.me/ryos_bot?start=link_abc123",
  "linkedAccount": {
    "telegramUserId": 123456789,
    "telegramUsername": "alice_tg",
    "firstName": "Alice",
    "lastName": "Smith",
    "linkedAt": 1704067200000
  }
}

GET /api/telegram/link/status

Check whether the authenticated user has a linked Telegram account or a pending link session.

Response (200):
{
  "linked": true,
  "account": {
    "telegramUserId": 123456789,
    "telegramUsername": "alice_tg",
    "firstName": "Alice",
    "lastName": "Smith",
    "linkedAt": 1704067200000
  },
  "pendingLink": null
}

When not linked but a link is pending, account is null and pendingLink contains { code, expiresIn, botUsername, deepLink }.

POST /api/telegram/link/disconnect

Disconnect a linked Telegram account.

Response (200):
{
  "success": true
}

AirDrop API

Peer-to-peer file sharing between online ryOS users, modeled on AirDrop. Presence is tracked in a shared lobby (60-second TTL) and transfers are relayed through Redis with realtime notifications. All endpoints require authentication.

Endpoint Summary

MethodPathDescriptionAuth
POST/api/airdrop/heartbeatAnnounce presence in the AirDrop lobbyRequired
GET/api/airdrop/discoverList other nearby (online) usersRequired
POST/api/airdrop/sendOffer a file to a recipientRequired
POST/api/airdrop/respondAccept or decline a transferRequired

POST /api/airdrop/heartbeat

Adds the authenticated user to the lobby and broadcasts airdrop-presence. Response (200): { "success": true }.

GET /api/airdrop/discover

Prunes stale entries and returns online usernames other than the caller.

{
  "users": ["bob", "carol"]
}

POST /api/airdrop/send

Offers a file to an online recipient. The transfer is stored for 5 minutes and an airdrop-request event is delivered to the recipient.

{
  "recipient": "bob",
  "fileName": "notes.md",
  "fileType": "text",
  "content": "..."
}
recipient, fileName, and content are required. content is capped at 2 MB (413 if exceeded). Returns 404 if the recipient is not currently available. Response (200):
{
  "success": true,
  "transferId": "uuid"
}

POST /api/airdrop/respond

The recipient accepts or declines a pending transfer.

{
  "transferId": "uuid",
  "accept": true
}

On accept, the file payload is returned and the sender is notified (airdrop-accepted):

{
  "success": true,
  "fileName": "notes.md",
  "fileType": "text",
  "content": "...",
  "sender": "alice"
}

On decline, the sender is notified (airdrop-declined):

{
  "success": true,
  "declined": true
}

Returns 404 if the transfer expired/not found, and 403 if the transfer is not addressed to the caller.


Error Handling

All utility APIs return consistent error responses:

{
  "error": "Error message description"
}
Common HTTP Status Codes:
StatusDescription
400Bad request (missing/invalid parameters)
401Unauthorized (authentication required)
403Forbidden (insufficient permissions or blocked origin)
404Not found
405Method not allowed
408Request timeout
429Rate limit exceeded
500Internal server error
503Service unavailable

CORS

All utility APIs implement CORS with origin validation. Allowed origins are configured server-side. Requests from unauthorized origins receive 403 Forbidden.

Related