API Documentation
REST API and WebSocket for live prediction market indexes, signal states, and narratives. Build integrations in minutes.
Getting started
1 Get an API key from your dashboard settings.
2 Include it as a Bearer token in the Authorization header.
3 That's it. You're live.
curl https://api.safehouse.tools/v1/indexes \
-H "Authorization: Bearer sh_live_your_key_here"Authentication
Include your API key as a Bearer token in the Authorization header.
- •API keys start with
sh_live_ - •Generate and revoke keys from your dashboard
| Auth Level | Meaning |
|---|---|
| Public | No API key required. Works unauthenticated, but some fields (e.g. narrative top movers) require Pro. |
| Pro | Requires a Pro or Enterprise API key. |
| Enterprise | Requires an Enterprise API key. |
Get Slack alerts in 10 lines
import requests
resp = requests.post(
"https://api.safehouse.tools/v1/alerts",
headers={"Authorization": "Bearer sh_live_..."},
json={
"index_slug": "crypto",
"alert_type": "signal_change",
"channels": {
"slack_webhook_url":
"https://hooks.slack.com/..."
},
},
)
print(f"Alert created: {resp.json()['id']}")Indexes
/v1/indexesPublicReturns all indexes with signal states, sparklines, and narrative summaries.
Response
[
{
"slug": "crypto",
"name": "Crypto & Digital Assets",
"current_value": 58.4,
"current_value_unit": "pts",
"momentum_24h": 3.2,
"momentum_24h_unit": "pts (24h)",
"signal_state": "consensus_shift",
"signal_label": "Consensus Shift",
"constituent_count": 42,
"constituent_count_unit": "markets",
"dispersion": 18.4,
"dispersion_label": "Moderate",
"volatility": 2.1,
"volatility_label": "Moderate",
"breadth": 0.82,
"breadth_label": "Strong",
"last_computed_at": "2025-01-01T12:00:00Z",
"sparkline": [
{ "t": "2025-01-01T00:00:00Z", "v": 55.2 },
{ "t": "2025-01-01T00:29:00Z", "v": 55.8 }
],
"narrative_summary": "Markets are aligning..."
}
]/v1/indexes/{slug}PublicReturns a single index with full detail including narrative.
Narrative summary is included for all users. Top movers within the narrative require Pro.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| slug | path | string | Yes | Index slug (e.g. "crypto", "geopolitics") |
Response
{
"index_id": "idx_abc123",
"slug": "crypto",
"name": "Crypto & Digital Assets",
"description": "Tracks prediction markets...",
"current_value": 58.4,
"current_value_unit": "pts",
"momentum_24h": 3.2,
"momentum_24h_unit": "pts (24h)",
"momentum_7d": -1.1,
"momentum_7d_unit": "pts (7d)",
"signal_state": "consensus_shift",
"signal_label": "Consensus Shift",
"volatility": 2.1,
"volatility_unit": "pts/day",
"volatility_label": "Moderate",
"dispersion": 18.4,
"dispersion_unit": "pts",
"dispersion_label": "Moderate",
"breadth": 0.82,
"breadth_unit": "ratio",
"breadth_label": "Strong",
"constituent_count": 42,
"constituent_count_unit": "markets",
"last_computed_at": "2025-01-01T12:00:00Z",
"narrative": {
"summary": "Markets are aligning...",
"top_movers": [
{
"market_id": "mkt_xyz",
"title": "Will Bitcoin reach $100k?",
"platform": "polymarket",
"platform_url": "https://polymarket.com/...",
"price": 0.62,
"price_unit": "probability (0-1)",
"price_change_24h": 0.08,
"price_change_24h_pct": 14.8,
"price_change_24h_pct_unit": "%",
"weight": 0.084,
"weight_unit": "ratio",
"contribution": 1.2,
"contribution_unit": "pts",
"direction": "up"
}
],
"generated_at": "2025-01-01T12:00:00Z"
}
}/v1/indexes/{slug}/constituentsProReturns the full constituent list with weights, live prices, and platform links.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| slug | path | string | Yes | Index slug |
Response
[
{
"market_id": "mkt_xyz",
"title": "Will Bitcoin reach $100k?",
"platform": "polymarket",
"platform_url": "https://polymarket.com/...",
"price": 0.62,
"price_unit": "probability (0-1)",
"price_at_computation": 0.60,
"price_at_computation_unit": "probability (0-1)",
"final_weight": 0.084,
"final_weight_unit": "ratio",
"final_weight_pct": 8.4,
"final_weight_pct_unit": "%",
"added_at": "2025-01-01T00:00:00Z"
}
]/v1/indexes/{slug}/narrativePublicReturns the current narrative block standalone.
Top movers array is populated for Pro users only. Free users receive summary + empty array.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| slug | path | string | Yes | Index slug |
Response
{
"summary": "Markets are aligning...",
"top_movers": [
{
"market_id": "mkt_xyz",
"title": "Will Bitcoin reach $100k?",
"platform": "polymarket",
"platform_url": "https://polymarket.com/...",
"price": 0.62,
"price_unit": "probability (0-1)",
"price_change_24h": 0.08,
"price_change_24h_pct": 14.8,
"price_change_24h_pct_unit": "%",
"weight": 0.084,
"weight_unit": "ratio",
"contribution": 1.2,
"contribution_unit": "pts",
"direction": "up"
}
],
"generated_at": "2025-01-01T12:00:00Z"
}/v1/indexes/{slug}/historyPublicReturns time series data for the index. Supports time-range filtering.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| slug | path | string | Yes | Index slug |
| since | query | ISO 8601 | No | Lower bound timestamp (inclusive) |
| until | query | ISO 8601 | No | Upper bound timestamp (inclusive) |
Response
{
"slug": "crypto",
"points": [
{
"time": "2025-01-01T00:00:00Z",
"value": 55.2,
"value_unit": "pts",
"constituent_count": 42,
"constituent_count_unit": "markets",
"dispersion": 16.1,
"dispersion_label": "Moderate",
"breadth": 0.78,
"breadth_label": "Moderate",
"signal_state": "active"
}
]
}Market Search
/v1/markets/searchPublicSearch markets by title using trigram similarity. Falls back to ILIKE if no trigram matches.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| q | query | string | Yes | Search query (2-200 characters) |
| limit | query | integer | No | Max results (1-50, default 20) |
Response
[
{
"market_id": "mkt_abc",
"title": "Will Bitcoin reach $100k?",
"platform": "polymarket",
"platform_url": "https://polymarket.com/...",
"price": 0.62,
"price_unit": "probability (0-1)",
"category": "crypto",
"similarity": 0.892,
"similarity_unit": "score (0-1)"
}
]Alerts
/v1/alertsProLists all your alert configurations.
Response
[
{
"id": "alert_abc123",
"index_id": "idx_abc123",
"index_name": "Crypto & Digital Assets",
"alert_type": "signal_change",
"conditions": {},
"channels": {
"slack_webhook_url": "https://hooks.slack.com/***"
},
"is_active": true,
"cooldown_minutes": 60,
"cooldown_minutes_unit": "minutes",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]/v1/alertsProCreates an alert. Supports signal_change and momentum_threshold types.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| index_slug | body | string | No | Index slug to monitor (alternative to index_id) |
| index_id | body | UUID | No | Index UUID to monitor (alternative to index_slug) |
| alert_type | body | string | Yes | "signal_change" or "momentum_threshold" |
| conditions | body | object | No | For momentum_threshold: { "threshold": 5.0 } |
| channels | body | object | Yes | At least one of: webhook_url, slack_webhook_url, email |
| cooldown_minutes | body | integer | No | Min time between alerts (1-1440, default 60) |
Response
{
"id": "alert_abc123",
"index_id": "idx_abc123",
"index_name": "Crypto & Digital Assets",
"alert_type": "signal_change",
"conditions": {},
"channels": {
"slack_webhook_url": "https://hooks.slack.com/***"
},
"is_active": true,
"cooldown_minutes": 60,
"cooldown_minutes_unit": "minutes",
"webhook_secret": "a1b2c3...",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}/v1/alerts/{alert_id}ProUpdates an existing alert config. All fields optional.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| alert_id | path | UUID | Yes | Alert config ID |
| conditions | body | object | No | Updated conditions |
| channels | body | object | No | Updated delivery channels |
| is_active | body | boolean | No | Enable or disable the alert |
| cooldown_minutes | body | integer | No | Updated cooldown (1-1440) |
Response
{
"id": "alert_abc123",
"index_id": "idx_abc123",
"index_name": "Crypto & Digital Assets",
"alert_type": "signal_change",
"conditions": {},
"channels": {
"slack_webhook_url": "https://hooks.slack.com/***"
},
"is_active": false,
"cooldown_minutes": 120,
"cooldown_minutes_unit": "minutes",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-02T08:00:00Z"
}/v1/alerts/{alert_id}ProPermanently deletes an alert config.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| alert_id | path | UUID | Yes | Alert config ID |
Response
Returns 204 No Content.
/v1/alerts/test/{alert_id}ProSends a test payload to all configured channels for this alert.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| alert_id | path | UUID | Yes | Alert config ID to test |
Response
Returns 202 Accepted. Delivery is asynchronous.
{
"status": "queued",
"channels": ["slack_webhook_url"]
}/v1/alerts/historyProReturns the last 100 alert deliveries across all your configs.
Response
[
{
"id": "hist_xyz",
"alert_config_id": "alert_abc123",
"index_id": "idx_abc123",
"index_name": "Crypto & Digital Assets",
"signal_state": "consensus_shift",
"narrative_summary": "Markets are aligning...",
"channels_sent": {
"slack_webhook_url": "delivered"
},
"sent_at": "2025-01-01T12:00:00Z"
}
]Custom Themes
/v1/themesProLists your custom themes.
Response
[
{
"theme_id": "theme_abc123",
"name": "EU AI Regulation",
"slug": "eu-ai-regulation",
"description": "Tracks EU AI Act markets",
"status": "active",
"constituent_count": 12,
"created_at": "2025-01-01T00:00:00Z"
}
]/v1/themesProCreates a self-serve custom theme. The system finds matching markets automatically.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| name | body | string | Yes | Theme name (3-80 characters) |
| description | body | string | No | Theme description |
| theme_keywords | body | string[] | Yes | Keywords for market matching (2-50 entries) |
| anchor_phrases | body | string[] | No | High-signal phrases for embedding similarity |
| config | body | object | No | Advanced: exclude_keywords, min_liquidity, price_bounds, control_mode |
Response
{
"theme_id": "theme_abc123",
"name": "EU AI Regulation",
"slug": "eu-ai-regulation",
"status": "building",
"candidate_count": 0,
"constituent_count": 0
}/v1/themes/{theme_id}ProReturns theme detail with index data.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| theme_id | path | UUID | Yes | Theme ID |
Response
{
"theme_id": "theme_abc123",
"name": "EU AI Regulation",
"slug": "eu-ai-regulation",
"description": "Tracks EU AI Act markets",
"status": "active",
"keywords": ["eu ai act", "ai regulation"],
"anchor_phrases": ["european union ai"],
"config": {
"control_mode": "auto",
"min_liquidity": 500,
"price_bounds": [0.05, 0.95],
"auto_add_threshold": 0.45,
"suggest_threshold": 0.35
},
"constituent_count": 12,
"created_at": "2025-01-01T00:00:00Z"
}/v1/themes/{theme_id}ProUpdates theme config. All fields optional.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| theme_id | path | UUID | Yes | Theme ID |
| name | body | string | No | Updated name |
| theme_keywords | body | string[] | No | Updated keywords |
Response
Returns the updated theme.
{
"theme_id": "theme_abc123",
"name": "EU AI Regulation",
"slug": "eu-ai-regulation",
"description": "Tracks EU AI Act markets",
"status": "active",
"keywords": ["eu ai act", "ai regulation"],
"anchor_phrases": ["european union ai"],
"config": {
"control_mode": "auto",
"min_liquidity": 500,
"price_bounds": [0.05, 0.95],
"auto_add_threshold": 0.45,
"suggest_threshold": 0.35
},
"constituent_count": 12,
"created_at": "2025-01-01T00:00:00Z"
}/v1/themes/{theme_id}ProDeletes a custom theme and its index.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| theme_id | path | UUID | Yes | Theme ID |
Response
Returns 204 No Content.
/v1/themes/{theme_id}/candidatesProReturns market candidates pending review for a theme.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| theme_id | path | UUID | Yes | Theme ID |
| status | query | string | No | Filter: "pending", "approved", or "rejected" (default: "pending") |
Response
[
{
"market_id": "mkt_abc",
"title": "Will the EU AI Act be enforced by 2026?",
"platform": "polymarket",
"platform_url": "https://polymarket.com/...",
"price": 0.74,
"similarity": 0.82,
"status": "pending",
"suggested_at": "2025-01-01T00:00:00Z"
}
]/v1/themes/{theme_id}/candidates/reviewProApprove or reject market candidates for a theme.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| theme_id | path | UUID | Yes | Theme ID |
| market_ids | body | string[] | Yes | Market IDs to review |
| action | body | string | Yes | "approve" or "reject" |
Response
{
"approved": 3,
"rejected": 1
}/v1/themes/generateEnterpriseWhite-glove theme generation. Describe a concept in plain English and AI generates a comprehensive theme.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| description | body | string | Yes | Plain-text description (20-500 chars). E.g. "Track escalation risk in the South China Sea" |
| sensitivity | body | string | No | "broad", "balanced" (default), or "strict" |
| examples | body | string[] | No | Example market titles for calibration |
Response
{
"theme_id": "theme_abc123",
"name": "EU AI Regulation",
"slug": "eu-ai-regulation",
"status": "building",
"candidate_count": 0,
"constituent_count": 0
}WebSocket
Connect for real-time index updates and signal state changes. Subscribe to up to 7 indexes per connection.
Endpoint: wss://api.safehouse.tools/v1/ws?token=sh_live_...
Auth: Pro
Heartbeat: Server sends heartbeat every 30s. Connection idles out after 5 min with no subscriptions.
Client messages: subscribe, unsubscribe, ping
Connection example
const url =
"wss://api.safehouse.tools/v1/ws" +
"?token=sh_live_...";
const ws = new WebSocket(url);
ws.send(JSON.stringify({
type: "subscribe",
indexes: ["crypto", "geopolitics"]
}));
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "signal_change") {
console.log(
`${data.slug}: ${data.new_label}`
);
}
};Message types
// Server -> Client: index_update (every ~30s)
{
"type": "index_update",
"slug": "crypto",
"value": 58.4,
"value_unit": "pts",
"momentum_24h": 3.2,
"momentum_24h_unit": "pts (24h)",
"signal_state": "consensus_shift",
"signal_label": "Consensus Shift",
"narrative_summary": "Markets are aligning...",
"constituent_prices": [
{ "market_id": "mkt_xyz", "price": 0.62 }
],
"ts": "2025-01-01T12:00:00Z"
}
// Server -> Client: signal_change
{
"type": "signal_change",
"slug": "crypto",
"previous_state": "active",
"new_state": "consensus_shift",
"new_label": "Consensus Shift",
"value": 58.4,
"value_unit": "pts",
"momentum_24h": 3.2,
"narrative_summary": "Markets are aligning...",
"ts": "2025-01-01T12:00:00Z"
}Code snippets
Monitor all indexes
import requests
indexes = requests.get(
"https://api.safehouse.tools/v1/indexes",
headers={"Authorization": "Bearer sh_live_..."},
).json()
for idx in indexes:
state = idx["signal_label"]
m = idx["momentum_24h"]
sign = "+" if m and m > 0 else ""
print(f"{idx['name']}: {state} ({sign}{m} pts)")Live dashboard widget
const ws = new WebSocket(
"wss://api.safehouse.tools/v1/ws" +
"?token=sh_live_..."
);
ws.onmessage = ({ data }) => {
const u = JSON.parse(data);
const el = document.getElementById(u.slug);
const sign = u.momentum_24h > 0 ? "+" : "";
el.textContent =
`${u.signal_label}: ${sign}${u.momentum_24h} pts`;
};Rate limits
| Tier | Rate | Window |
|---|---|---|
| Public (no key) | 30 req | 1 min |
| Pro | 300 req | 1 min |
| Enterprise | Custom | Custom |
Rate limit headers are included in every response:
- X-RateLimit-Limit
- X-RateLimit-Remaining
- X-RateLimit-Reset
When rate limited, you'll receive a 429 response. Wait for the reset time before retrying.
Errors
All errors return a JSON object with a detail field:
{
"detail": "Rate limit exceeded. Retry after 12s."
}| Code | Meaning | Fix |
|---|---|---|
| 200 | Success | N/A |
| 201 | Created | N/A |
| 204 | Deleted (no body) | N/A |
| 401 | Missing or invalid API key | Check your Authorization header |
| 403 | Insufficient tier for endpoint | Request Pro or Enterprise access |
| 404 | Resource not found | Check slug / ID, or verify access to custom themes |
| 422 | Validation error | Check request body against parameter requirements |
| 429 | Rate limit exceeded | Wait for X-RateLimit-Reset |
| 500 | Internal server error | Retry with exponential backoff |
Ready to integrate?
Get your API key in seconds. Start with 30 free requests per minute, upgrade to Pro for full access.
