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.

bash
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 LevelMeaning
PublicNo API key required. Works unauthenticated, but some fields (e.g. narrative top movers) require Pro.
ProRequires a Pro or Enterprise API key.
EnterpriseRequires an Enterprise API key.

Get Slack alerts in 10 lines

python
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

GET/v1/indexesPublic

Returns all indexes with signal states, sparklines, and narrative summaries.

Response

json
[
  {
    "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..."
  }
]
GET/v1/indexes/{slug}Public

Returns a single index with full detail including narrative.

Narrative summary is included for all users. Top movers within the narrative require Pro.

Parameters

NameInTypeRequiredDescription
slugpathstringYesIndex slug (e.g. "crypto", "geopolitics")

Response

json
{
  "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"
  }
}
GET/v1/indexes/{slug}/constituentsPro

Returns the full constituent list with weights, live prices, and platform links.

Parameters

NameInTypeRequiredDescription
slugpathstringYesIndex slug

Response

json
[
  {
    "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"
  }
]
GET/v1/indexes/{slug}/narrativePublic

Returns the current narrative block standalone.

Top movers array is populated for Pro users only. Free users receive summary + empty array.

Parameters

NameInTypeRequiredDescription
slugpathstringYesIndex slug

Response

json
{
  "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"
}
GET/v1/indexes/{slug}/historyPublic

Returns time series data for the index. Supports time-range filtering.

Parameters

NameInTypeRequiredDescription
slugpathstringYesIndex slug
sincequeryISO 8601NoLower bound timestamp (inclusive)
untilqueryISO 8601NoUpper bound timestamp (inclusive)

Response

json
{
  "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"
    }
  ]
}

Alerts

GET/v1/alertsPro

Lists all your alert configurations.

Response

json
[
  {
    "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"
  }
]
POST/v1/alertsPro

Creates an alert. Supports signal_change and momentum_threshold types.

Parameters

NameInTypeRequiredDescription
index_slugbodystringNoIndex slug to monitor (alternative to index_id)
index_idbodyUUIDNoIndex UUID to monitor (alternative to index_slug)
alert_typebodystringYes"signal_change" or "momentum_threshold"
conditionsbodyobjectNoFor momentum_threshold: { "threshold": 5.0 }
channelsbodyobjectYesAt least one of: webhook_url, slack_webhook_url, email
cooldown_minutesbodyintegerNoMin time between alerts (1-1440, default 60)

Response

json
{
  "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"
}
PUT/v1/alerts/{alert_id}Pro

Updates an existing alert config. All fields optional.

Parameters

NameInTypeRequiredDescription
alert_idpathUUIDYesAlert config ID
conditionsbodyobjectNoUpdated conditions
channelsbodyobjectNoUpdated delivery channels
is_activebodybooleanNoEnable or disable the alert
cooldown_minutesbodyintegerNoUpdated cooldown (1-1440)

Response

json
{
  "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"
}
DELETE/v1/alerts/{alert_id}Pro

Permanently deletes an alert config.

Parameters

NameInTypeRequiredDescription
alert_idpathUUIDYesAlert config ID

Response

Returns 204 No Content.

POST/v1/alerts/test/{alert_id}Pro

Sends a test payload to all configured channels for this alert.

Parameters

NameInTypeRequiredDescription
alert_idpathUUIDYesAlert config ID to test

Response

Returns 202 Accepted. Delivery is asynchronous.

json
{
  "status": "queued",
  "channels": ["slack_webhook_url"]
}
GET/v1/alerts/historyPro

Returns the last 100 alert deliveries across all your configs.

Response

json
[
  {
    "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

GET/v1/themesPro

Lists your custom themes.

Response

json
[
  {
    "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"
  }
]
POST/v1/themesPro

Creates a self-serve custom theme. The system finds matching markets automatically.

Parameters

NameInTypeRequiredDescription
namebodystringYesTheme name (3-80 characters)
descriptionbodystringNoTheme description
theme_keywordsbodystring[]YesKeywords for market matching (2-50 entries)
anchor_phrasesbodystring[]NoHigh-signal phrases for embedding similarity
configbodyobjectNoAdvanced: exclude_keywords, min_liquidity, price_bounds, control_mode

Response

json
{
  "theme_id": "theme_abc123",
  "name": "EU AI Regulation",
  "slug": "eu-ai-regulation",
  "status": "building",
  "candidate_count": 0,
  "constituent_count": 0
}
GET/v1/themes/{theme_id}Pro

Returns theme detail with index data.

Parameters

NameInTypeRequiredDescription
theme_idpathUUIDYesTheme ID

Response

json
{
  "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"
}
PATCH/v1/themes/{theme_id}Pro

Updates theme config. All fields optional.

Parameters

NameInTypeRequiredDescription
theme_idpathUUIDYesTheme ID
namebodystringNoUpdated name
theme_keywordsbodystring[]NoUpdated keywords

Response

Returns the updated theme.

json
{
  "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"
}
DELETE/v1/themes/{theme_id}Pro

Deletes a custom theme and its index.

Parameters

NameInTypeRequiredDescription
theme_idpathUUIDYesTheme ID

Response

Returns 204 No Content.

GET/v1/themes/{theme_id}/candidatesPro

Returns market candidates pending review for a theme.

Parameters

NameInTypeRequiredDescription
theme_idpathUUIDYesTheme ID
statusquerystringNoFilter: "pending", "approved", or "rejected" (default: "pending")

Response

json
[
  {
    "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"
  }
]
POST/v1/themes/{theme_id}/candidates/reviewPro

Approve or reject market candidates for a theme.

Parameters

NameInTypeRequiredDescription
theme_idpathUUIDYesTheme ID
market_idsbodystring[]YesMarket IDs to review
actionbodystringYes"approve" or "reject"

Response

json
{
  "approved": 3,
  "rejected": 1
}
POST/v1/themes/generateEnterprise

White-glove theme generation. Describe a concept in plain English and AI generates a comprehensive theme.

Parameters

NameInTypeRequiredDescription
descriptionbodystringYesPlain-text description (20-500 chars). E.g. "Track escalation risk in the South China Sea"
sensitivitybodystringNo"broad", "balanced" (default), or "strict"
examplesbodystring[]NoExample market titles for calibration

Response

json
{
  "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

javascript
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

json
// 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

python
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

javascript
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

TierRateWindow
Public (no key)30 req1 min
Pro300 req1 min
EnterpriseCustomCustom

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:

json
{
  "detail": "Rate limit exceeded. Retry after 12s."
}
CodeMeaningFix
200SuccessN/A
201CreatedN/A
204Deleted (no body)N/A
401Missing or invalid API keyCheck your Authorization header
403Insufficient tier for endpointRequest Pro or Enterprise access
404Resource not foundCheck slug / ID, or verify access to custom themes
422Validation errorCheck request body against parameter requirements
429Rate limit exceededWait for X-RateLimit-Reset
500Internal server errorRetry 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.