# Jarvis API Contract Base URL: `http://:/api/v1` --- ## Authentication None. Local network / self-hosted only. All endpoints are open. Secure at the network level (VPN, firewall). --- ## REST Endpoints ### GET /stories Returns paginated stories sorted by signal score descending. **Query params** | Param | Type | Default | Description | |-------|------|---------|-------------| | limit | int | 20 | Max stories per page (max 100) | | after | string | — | Cursor for next page | | topic | string | — | Filter by topic slug e.g. `finance`, `tech` | | min_signal | int | — | Only return stories above this score | **Response 200** ```json { "data": [ { "id": "story_2847", "headline": "Bank of Uganda cuts interest rates", "summary": "The Bank of Uganda reduced its benchmark lending rate by 50 basis points, citing easing inflation and the need to stimulate credit growth.", "topic": "finance", "signalScore": 91, "scoreBreakdown": { "sourceAuthority": 28, "freshness": 22, "localRelevance": 15, "crossSourceConfirmation": 16, "topicImportance": 10 }, "sourceCount": 7, "sources": [ { "id": "src_001", "name": "Daily Monitor", "url": "https://monitor.co.ug", "publishedAt": "2026-06-18T08:03:00Z", "isBreaking": true } ], "consensus": "All 7 sources confirm the rate cut. Daily Monitor and NilePost agree on the 50bp figure.", "conflict": null, "cachedAt": null, "updatedAt": "2026-06-18T09:45:00Z", "createdAt": "2026-06-18T08:03:00Z" } ], "nextCursor": "cursor_abc123", "hasMore": true, "total": 248 } ``` --- ### GET /stories/:id Returns full story detail including article timeline. **Response 200** ```json { "id": "story_2847", "headline": "Bank of Uganda cuts interest rates", "summary": "...", "topic": "finance", "signalScore": 91, "scoreBreakdown": { "sourceAuthority": 28, "freshness": 22, "localRelevance": 15, "crossSourceConfirmation": 16, "topicImportance": 10 }, "sourceCount": 7, "consensus": "All 7 sources confirm the rate cut.", "conflict": "Two regional outlets dispute impact on mortgage rates.", "timeline": [ { "articleId": "art_001", "source": "Daily Monitor", "headline": "BoU cuts lending rate by 50 basis points", "publishedAt": "2026-06-18T08:03:00Z", "isBreaking": true }, { "articleId": "art_002", "source": "NilePost", "headline": "Central bank eases policy amid inflation slowdown", "publishedAt": "2026-06-18T08:21:00Z", "isBreaking": false } ], "updatedAt": "2026-06-18T09:45:00Z", "createdAt": "2026-06-18T08:03:00Z" } ``` --- ### GET /articles/:id Returns full article content for offline caching. **Response 200** ```json { "id": "art_001", "storyId": "story_2847", "source": "Daily Monitor", "sourceUrl": "https://monitor.co.ug", "headline": "BoU cuts lending rate by 50 basis points", "body": "Full article text...", "imageUrl": "https://...", "author": "John Doe", "publishedAt": "2026-06-18T08:03:00Z" } ``` --- ### GET /feeds Returns all configured RSS feeds with health state. **Response 200** ```json { "data": [ { "id": "feed_001", "name": "Daily Monitor", "url": "https://monitor.co.ug/rss", "health": "active", "pollIntervalSeconds": 300, "failureCount": 0, "lastFetchedAt": "2026-06-18T09:40:00Z", "articleCountToday": 44 } ] } ``` --- ### POST /feeds Add a new RSS feed. **Request** ```json { "url": "https://example.com/rss", "name": "Example News", "pollIntervalSeconds": 900 } ``` **Response 201** Returns the created feed object. --- ### DELETE /feeds/:id Remove a feed. **Response 204** No content. --- ### GET /health Server health check. Used by the app on launch to verify connection. **Response 200** ```json { "status": "ok", "version": "1.0.0", "storiesCount": 248, "feedsCount": 37, "uptime": 86400 } ``` --- ## WebSocket ### Connect `ws://:/ws` No auth headers required. --- ### Events (server → client) #### story.updated Signal score changed or new article joined the story. ```json { "type": "story.updated", "storyId": "story_2847", "signalScore": 94, "sourceCount": 9, "updatedAt": "2026-06-18T10:01:00Z" } ``` App behaviour: refetch `GET /stories/story_2847`, update local cache. --- #### story.created New story formed from a new cluster. ```json { "type": "story.created", "storyId": "story_2901", "headline": "Parliament passes new budget", "signalScore": 42, "topic": "politics", "createdAt": "2026-06-18T10:05:00Z" } ``` App behaviour: insert into feed, resort by signal score. --- #### story.stale Story has gone cold — no new articles in decay window. ```json { "type": "story.stale", "storyId": "story_2800", "updatedAt": "2026-06-18T10:10:00Z" } ``` App behaviour: dim story in feed or remove based on user preference. --- #### feed.health A feed's health state changed. ```json { "type": "feed.health", "feedId": "feed_012", "health": "failing", "failureCount": 3, "updatedAt": "2026-06-18T10:12:00Z" } ``` App behaviour: update feed health indicator in feed manager. --- ### Ping / Pong Server sends `{"type":"ping"}` every 30s. Client must respond with `{"type":"pong"}`. No response in 60s → server closes connection. Client should reconnect with exponential backoff: 1s → 2s → 4s → 8s → max 60s. --- ## Error shapes All errors follow this shape: ```json { "error": { "code": "story_not_found", "message": "No story with id story_9999", "status": 404 } } ``` | Code | Status | Meaning | |------|--------|---------| | `story_not_found` | 404 | Story ID does not exist | | `feed_not_found` | 404 | Feed ID does not exist | | `invalid_cursor` | 400 | Pagination cursor is malformed | | `invalid_url` | 400 | Feed URL is not a valid RSS feed | | `server_error` | 500 | Internal server error |