API Reference
Base URL
All proxy requests follow this pattern:
https://www.perpetua.sh/api/proxy/{provider}/{path}
Where {provider} is the provider slug and {path} is the path you'd normally send to the provider's API.
Authentication
Include your API key as a Bearer token in the Authorization header:
Authorization: Bearer pk_your_key
Every request to the proxy must include this header. The API key identifies your account, and Perpetua injects the correct OAuth token for the upstream provider.
Supported providers
| Provider | Slug | Upstream base URL | Example path |
|---|---|---|---|
| Oura Ring | oura | https://api.ouraring.com | /v2/usercollection/daily_activity |
| Google Calendar | gcal | https://www.googleapis.com/calendar/v3 | /calendars/primary/events |
More providers (Strava, Notion, and others) are coming soon.
Request forwarding
Perpetua forwards your request to the provider's API, including:
- Method — GET, POST, PUT, PATCH, and DELETE are all supported.
- Query parameters — All query parameters are forwarded to the upstream API (except
account, which is consumed by the proxy). - Headers — Most request headers are forwarded. Hop-by-hop headers (
connection,host,authorization, etc.) are stripped and replaced as needed. - Body — Request bodies are forwarded as-is for POST, PUT, PATCH, and DELETE requests.
The upstream response is returned to you with its original status code and headers.
Multiple accounts
If you've connected more than one account for the same provider (e.g., two different Google Calendar accounts), use the account query parameter to choose which one:
curl -H "Authorization: Bearer pk_your_key" \
"https://www.perpetua.sh/api/proxy/gcal/calendars/primary/events?account=work"
Without the account parameter, the default connection is used.
Error responses
| Status | Error | Description |
|---|---|---|
| 401 | Unauthorized | Missing or invalid API key. |
| 401 | TokenRefreshFailed | The provider's OAuth token could not be refreshed. Reconnect the provider from the dashboard. |
| 404 | UnknownProvider | The provider slug is not recognized. |
| 404 | NoConnection | No connection exists for this provider (or account). Connect it from the dashboard. |
| 429 | Too many requests | Rate limit exceeded. Wait and retry. |
| 502 | UpstreamError | The upstream provider API could not be reached. |
Rate limits
API requests are rate-limited per IP address using a sliding window:
| Route | Limit |
|---|---|
| Global (all API routes) | 100 requests / minute |
Auth routes (/api/auth/connect/.../start) | 10 requests / minute |
Key management (/api/keys) | 10 requests / minute |
Billing routes (/api/stripe/*) | 20 requests / minute |
MCP endpoint (/api/mcp) | 100 requests / minute (shared with global) |
When rate-limited, the API returns a 429 status. Wait and retry after a few seconds.
MCP / Agent Integration
Perpetua exposes an MCP (Model Context Protocol) server so AI agents and IDEs like Claude Desktop, Cursor, and VS Code can discover your connected providers and make proxied API requests -- no OAuth code required.
Endpoint
https://www.perpetua.sh/api/mcp
Transport: Streamable HTTP. Auth: same Bearer pk_... API key used for the REST proxy.
Tools
The MCP server exposes three tools:
list_providers
List all available OAuth providers. Shows which providers are configured and whether you have an active connection for each.
Parameters: None.
Returns: JSON array of providers with slug, displayName, baseUrl, and connected (boolean).
list_connections
List your active OAuth connections. Shows provider, account label, granted scopes, and whether the access token is fresh or expired.
Parameters: None.
Returns: JSON array of connections with provider, account, scopes, and tokenStatus ("fresh", "expired", or "unknown").
proxy_request
Make an authenticated API request to a connected OAuth provider. Perpetua handles token refresh automatically.
| Parameter | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | Provider slug (e.g. oura, gcal) |
path | string | Yes | API path (e.g. /v2/usercollection/daily_sleep) |
method | string | No | HTTP method: GET, POST, PUT, PATCH, DELETE. Defaults to GET. |
headers | object | No | Headers to send to the upstream provider (e.g. { "Notion-Version": "2022-06-28" }) |
body | string | No | Request body (typically JSON string for POST/PUT/PATCH) |
account | string | No | Account label for multi-account providers |
query_params | object | No | Query parameters to append to the upstream URL |
Returns: The upstream API response as text. JSON responses are returned as-is. Responses over 100KB are truncated. Binary responses return an error with the content type and size.
Why MCP?
// Before: agent manages tokens
const token = await getOAuthToken("oura", "personal");
const res = await fetch("https://api.ouraring.com/v2/usercollection/daily_sleep", {
headers: { Authorization: `Bearer ${token}` }
});
// After: agent uses Perpetua (via MCP or REST proxy)
const res = await fetch("https://www.perpetua.sh/api/proxy/oura/v2/usercollection/daily_sleep", {
headers: { Authorization: `Bearer ${PERPETUA_API_KEY}` }
});
With MCP, your agent discovers providers and makes calls through tool use -- it never sees an OAuth token.
Configuration examples
Claude Desktop
Add this to your Claude Desktop configuration (claude_desktop_config.json):
{
"mcpServers": {
"perpetua": {
"type": "streamablehttp",
"url": "https://www.perpetua.sh/api/mcp",
"headers": {
"Authorization": "Bearer pk_your_key"
}
}
}
}
Cursor / VS Code (.mcp.json)
Add this to your project's .mcp.json file:
{
"mcpServers": {
"perpetua": {
"type": "streamablehttp",
"url": "https://www.perpetua.sh/api/mcp",
"headers": {
"Authorization": "Bearer pk_your_key"
}
}
}
}
Testing with curl
curl -X POST https://www.perpetua.sh/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2025-03-26" \
-H "Authorization: Bearer pk_your_key" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": { "name": "test", "version": "1.0" }
}
}'