Periscope REST API
Complete endpoint reference for browser context sync, retrieval, search, and management
Base URL: https://periscope.uselovelace.com/api/v1
The REST API is the primary interface for syncing browser context from the extension and querying context history from agents and applications. For real-time streaming, see the WebSocket API. For a higher-level client, see the TypeScript and Rust SDKs.
Authentication
All requests require a Bearer token in the Authorization header:
Authorization: Bearer YOUR_API_KEY
API keys are generated through the Lovelace Developer Portal. Keys can be scoped to specific Periscope operations (read, write, delete) for least-privilege access.
OpenAPI Specification
The auto-generated OpenAPI specification is available at:
https://periscope.uselovelace.com/openapi.json
Rate Limiting
All endpoints enforce per-user rate limits. Every response includes rate limit headers:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 115
X-RateLimit-Reset: 1709380800
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
When you receive a 429 response, wait until X-RateLimit-Reset before retrying. See Privacy & Security for rate limit thresholds per endpoint.
Endpoints
POST /context/sync
Sync a browser context from the extension to the Periscope service. This is the primary ingestion endpoint — the extension calls it whenever the user navigates to a new page or triggers a context capture.
The request body is a SyncMessage containing the context payload. The service applies privacy filtering before storing the context.
Request:
{
"id": "msg_001",
"type": "create",
"timestamp": "2026-03-02T12:00:00Z",
"userId": "user_456",
"extensionId": "ext_chrome_abc",
"payload": {
"type": "context",
"operation": "create",
"context": {
"id": "ctx_new123",
"type": "page",
"tabId": "tab_789",
"userId": "user_456",
"capturedAt": "2026-03-02T12:00:00Z",
"privacyLevel": "public",
"tabMetadata": {
"id": "tab_789",
"url": "https://docs.example.com/guide",
"title": "Example Guide",
"isActive": true,
"isLoading": false,
"windowId": 1,
"lastUpdated": "2026-03-02T12:00:00Z"
}
}
}
}
Request fields:
| Field | Type | Description |
|---|---|---|
id | string | Unique message ID for idempotency and correlation |
type | string | Operation type: create, update, delete, heartbeat, batch, config, error |
timestamp | string | ISO 8601 timestamp of when the message was created |
userId | UserId | Authenticated user ID (branded type) |
extensionId | string | Identifier for the browser extension instance |
payload | SyncPayload | One of 4 payload variants — see Sync Protocol |
Response (201):
{
"messageId": "msg_001",
"status": "success",
"contextId": "ctx_new123",
"timestamp": "2026-03-02T12:00:00.123Z"
}
Response (207 — partial success for batch):
{
"messageId": "batch_001",
"status": "partial",
"results": [
{ "contextId": "ctx_1", "status": "success" },
{
"contextId": "ctx_2",
"status": "error",
"error": { "code": "validation_error", "message": "Missing tabMetadata" }
}
]
}
POST /context/batch-sync
Sync multiple browser contexts in a single request. Useful when the extension has accumulated multiple context changes while offline or to reduce API calls.
Request:
{
"id": "batch_001",
"type": "batch",
"timestamp": "2026-03-02T12:00:00Z",
"userId": "user_456",
"extensionId": "ext_chrome_abc",
"messages": [
{
"id": "msg_001",
"type": "create",
"timestamp": "2026-03-02T11:58:00Z",
"payload": {
"type": "context",
"operation": "create",
"context": { "...": "..." }
}
},
{
"id": "msg_002",
"type": "create",
"timestamp": "2026-03-02T11:59:00Z",
"payload": {
"type": "context",
"operation": "create",
"context": { "...": "..." }
}
}
]
}
| Field | Type | Description |
|---|---|---|
messages | SyncMessage[] | Array of individual sync messages (max 100 per batch) |
Response (207):
Each item in results corresponds to the message at the same index. The overall status code is 207 (Multi-Status) — check individual results to determine which items succeeded or failed.
{
"messageId": "batch_001",
"status": "partial",
"results": [
{ "contextId": "ctx_abc123", "status": "success" },
{ "contextId": "ctx_def456", "status": "success" }
],
"synced": 2,
"failed": 0
}
GET /context/current
Get the most recent browser context for the authenticated user. This is the endpoint agents typically poll to understand what the user is currently looking at.
For real-time updates without polling, subscribe to the current-context WebSocket channel instead.
Response (200):
{
"contextId": "ctx_abc123",
"userId": "user_456",
"type": "page",
"url": "https://docs.example.com/guide",
"title": "Example Guide",
"content": "Full page content extracted from the browser...",
"tabId": "tab_123",
"timestamp": "2026-03-02T12:00:00Z",
"privacyLevel": "public",
"tabMetadata": {
"id": "tab_123",
"url": "https://docs.example.com/guide",
"title": "Example Guide",
"isActive": true,
"isLoading": false,
"windowId": 1,
"lastUpdated": "2026-03-02T12:00:00Z"
}
}
Response fields:
| Field | Type | Description |
|---|---|---|
contextId | ContextId | Unique identifier for this context snapshot (branded type) |
userId | UserId | The authenticated user who generated this context |
type | ContextType | One of: page, selection, element, form, media, navigation, custom |
url | string | Current page URL from the active browser tab |
title | string | Page title from the document's <title> tag |
content | string | Extracted page content (text, HTML, or markdown depending on context type) |
tabId | TabId | Browser tab identifier (branded type) |
timestamp | string | ISO 8601 timestamp of when the context was captured |
privacyLevel | PrivacyLevel | One of public, private, restricted, filtered — see Privacy Model |
tabMetadata | TabMetadata | Tab state at time of capture (URL, title, active status, window) |
Response (404): Returned when no context has been synced yet.
GET /context/history
Get browsing context history for the authenticated user. Returns contexts in reverse chronological order (newest first).
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Maximum number of results (1–200) |
offset | number | 0 | Pagination offset |
since | string | — | ISO 8601 timestamp for start of range (inclusive) |
until | string | — | ISO 8601 timestamp for end of range (exclusive) |
type | string | — | Filter by context type: page, selection, element, form, media, navigation, custom |
privacyLevel | string | — | Filter by privacy level: public, private, restricted, filtered |
Response (200):
{
"contexts": [
{
"contextId": "ctx_abc123",
"type": "page",
"url": "https://docs.example.com/guide",
"title": "Example Guide",
"timestamp": "2026-03-02T12:00:00Z",
"privacyLevel": "public"
}
],
"total": 142,
"limit": 50,
"offset": 0
}
History responses omit content to keep payloads small. Use GET /context/{contextId} to fetch full content for a specific context.
GET /context/search
Search browsing context by content, URL, or metadata. Returns results ranked by relevance.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
query | string | — | Full-text search query (required unless urlPattern is provided) |
urlPattern | string | — | URL substring or pattern match |
type | string | — | Filter by context type |
privacyLevel | string | — | Filter by privacy level |
startTime | string | — | ISO 8601 timestamp for start of range |
endTime | string | — | ISO 8601 timestamp for end of range |
limit | number | 20 | Maximum number of results (1–1000) |
offset | number | 0 | Pagination offset |
Response (200):
{
"results": [
{
"contextId": "ctx_abc123",
"type": "page",
"url": "https://docs.example.com/auth",
"title": "Authentication Guide",
"snippet": "...configure OAuth 2.1 authentication...",
"timestamp": "2026-03-02T12:00:00Z",
"relevance": 0.95
}
],
"total": 5,
"query": "authentication",
"limit": 20,
"offset": 0
}
| Field | Description |
|---|---|
snippet | Text excerpt surrounding the search match, with context for relevance |
relevance | Score from 0.0 to 1.0 indicating how well the result matches the query |
GET /context/{contextId}
Get a specific context by ID. Returns the full context including content.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
contextId | ContextId | The context identifier (e.g., ctx_abc123) |
Response (200):
{
"contextId": "ctx_abc123",
"userId": "user_456",
"type": "page",
"url": "https://docs.example.com/guide",
"title": "Example Guide",
"content": "Full page content...",
"tabId": "tab_123",
"timestamp": "2026-03-02T12:00:00Z",
"privacyLevel": "public",
"tabMetadata": {
"id": "tab_123",
"url": "https://docs.example.com/guide",
"title": "Example Guide",
"isActive": true,
"isLoading": false,
"windowId": 1,
"lastUpdated": "2026-03-02T12:00:00Z"
}
}
Response (404):
{
"error": "not_found",
"message": "Context not found"
}
DELETE /context/{contextId}
Delete a specific context. Users can only delete their own contexts. Deletion is permanent and cannot be undone.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
contextId | ContextId | The context identifier to delete |
Response (204): No content — the context was successfully deleted.
Response (403): Returned when attempting to delete another user's context.
Response (404): Returned when the context does not exist.
Error Responses
All error responses follow a consistent format:
{
"error": "error_code",
"message": "Human-readable description of what went wrong"
}
| Status | Error Code | Description | Resolution |
|---|---|---|---|
400 | bad_request | Invalid request body or query parameters | Check request format against the schema above |
401 | unauthorized | Missing or invalid authentication token | Verify your API key is valid and included in the Authorization header |
403 | forbidden | Insufficient permissions for this operation | Check that your API key has the required scope |
404 | not_found | Resource does not exist | Verify the context ID or check that the extension has synced |
207 | partial_success | Some items in a batch succeeded, others failed | Check individual results entries for error details |
429 | rate_limited | Too many requests in the current window | Wait until X-RateLimit-Reset and retry |
500 | internal_error | Unexpected server error | Retry with exponential backoff; contact support if persistent |
REST vs WebSocket
Use REST when you need to:
- Fetch the current context on demand (polling)
- Search or query context history
- Sync context from custom integrations
- Delete or manage stored contexts
Use WebSocket when you need to:
- React to context changes in real time
- Build agents that continuously monitor browsing activity
- Minimize latency between user action and agent response
Most agents use WebSocket for real-time awareness and REST for historical queries. See the Agent Integration Guide for recommended patterns.
Next Steps
- WebSocket API — Real-time streaming protocol
- SDKs — TypeScript and Rust client libraries that wrap this API
- Sync Protocol — Understand the SyncMessage format in depth
- Browser Context Model — The data model behind context types