Developer Reference
BetEdge API v1
REST API for sports-betting data, Pinnacle-anchored model picks, track-record analytics, and real-time webhooks. Base URL: https://betedge.tips/api/v1
Authentication
All API requests require a Bearer token in the Authorization header. Keys have the prefix betedge_ followed by 64 lowercase hex characters. Generate and manage keys at /dashboard/api-keys.
curl -X GET "https://betedge.tips/api/v1/picks" \
-H "Authorization: Bearer betedge_<your_api_key>"const response = await fetch("https://betedge.tips/api/v1/picks", {
headers: { Authorization: "Bearer betedge_<your_api_key>" },
});import requests
resp = requests.get(
"https://betedge.tips/api/v1/picks",
headers={"Authorization": "Bearer betedge_<your_api_key>"},
)Error responses
401 invalid_api_key— Missing, invalid, or revoked key403 tier_upgrade_required— Endpoint not available on your tier
Rate limits and quotas
Rate limits use a sliding-window algorithm per API key. Monthly quotas reset at the start of each calendar month UTC.
| Tier | Requests/hour | Monthly cap |
|---|---|---|
| Starter | 100 | 10,000 |
| Growth | 1,000 | 100,000 |
| Enterprise | 10,000 | 1,000,000 |
Response headers on every request
- X-RateLimit-Limit — requests allowed per hour
- X-RateLimit-Remaining — requests remaining in current window
- X-RateLimit-Reset — Unix timestamp when the window resets
429 rate limit exceeded
{
"error": "rate_limit_exceeded",
"retry_after_seconds": 42
}
// Or for monthly cap:
{
"error": "monthly_quota_exceeded",
"resets_at": "2026-06-01T00:00:00.000Z"
}GET /api/v1/picks
/api/v1/picksReturns a paginated list of published and settled picks. Picks in admin_review status are never returned.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| sport | string? | Filter by sport slug (e.g. soccer, tennis) |
| league | string? | Filter by league name (e.g. Premier League) |
| since | ISO 8601? | Return picks with kickoff_at after this timestamp |
| limit | integer? | Results per page (default 50, max 200) |
| cursor | string? | Pagination cursor from previous response |
Response shape
{
"data": [
{
"id": "uuid",
"sport": "soccer",
"league": "Premier League",
"home_team": "Arsenal",
"away_team": "Chelsea",
"kickoff_at": "2026-05-25T15:00:00Z",
"pick": "Arsenal -0.5 Asian Handicap",
"odds": 1.91,
"edge_pct": 4.2,
"confidence": 3,
"status": "published",
"settled_at": null,
"result": null
}
],
"next_cursor": "2026-05-25T14:00:00Z",
"count": 47
}curl "https://betedge.tips/api/v1/picks?sport=soccer&limit=20" \
-H "Authorization: Bearer betedge_<your_api_key>"const resp = await fetch(
"https://betedge.tips/api/v1/picks?sport=soccer&limit=20",
{ headers: { Authorization: "Bearer betedge_<key>" } }
);
const { data, next_cursor, count } = await resp.json();resp = requests.get(
"https://betedge.tips/api/v1/picks",
params={"sport": "soccer", "limit": 20},
headers={"Authorization": "Bearer betedge_<key>"},
)
data = resp.json()["data"]Error responses
400 invalid_cursor— Cursor format is invalid401 invalid_api_key— Missing or invalid Bearer token
GET /api/v1/track-record
/api/v1/track-recordReturns aggregated performance statistics. Starter-tier requests return 403.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| sport | string? | Scope to a single sport |
| days_back | integer? | Lookback window in days (default 30, max 365) |
Response shape
{
"overall": {
"roi_pct": 8.3,
"win_rate": 54.1,
"total_settled": 420,
"clv_avg_pct": 2.1,
"edge_avg_pct": 3.8
},
"per_sport": [
{ "sport": "soccer", "roi_pct": 9.1, "total_settled": 180 }
],
"series": [
{ "date": "2026-05-01", "cumulative_pnl_pct": 4.2 }
]
}curl "https://betedge.tips/api/v1/track-record?days_back=30" \
-H "Authorization: Bearer betedge_<growth_or_enterprise_key>"const resp = await fetch(
"https://betedge.tips/api/v1/track-record?days_back=30",
{ headers: { Authorization: "Bearer betedge_<key>" } }
);
const { overall, per_sport, series } = await resp.json();resp = requests.get(
"https://betedge.tips/api/v1/track-record",
params={"days_back": 30},
headers={"Authorization": "Bearer betedge_<key>"},
)
overall = resp.json()["overall"]Error responses
403 tier_upgrade_required— Starter tier; minimum_tier: growth401 invalid_api_key— Missing or invalid Bearer token
GET /api/v1/match-analyses/:matchId
/api/v1/match-analyses/:matchIdReturns the AI-generated analysis text for a specific match. The matchId corresponds to the id field in pick objects.
Response shape
{
"match_id": "uuid",
"analysis": "Arsenal vs Chelsea — Pinnacle fair value is 1.87 on Arsenal ...",
"created_at": "2026-05-25T08:00:00Z"
}curl "https://betedge.tips/api/v1/match-analyses/<matchId>" \
-H "Authorization: Bearer betedge_<growth_or_enterprise_key>"const resp = await fetch(
`https://betedge.tips/api/v1/match-analyses/${matchId}`,
{ headers: { Authorization: "Bearer betedge_<key>" } }
);
const { analysis } = await resp.json();resp = requests.get(
f"https://betedge.tips/api/v1/match-analyses/{match_id}",
headers={"Authorization": "Bearer betedge_<key>"},
)
analysis = resp.json()["analysis"]Error responses
404 not_found— No analysis found for this matchId403 tier_upgrade_required— Starter tier; minimum_tier: growth401 invalid_api_key— Missing or invalid Bearer token
POST /api/v1/webhooks
/api/v1/webhooksCreates a webhook subscription. The hmac_secret is returned once — store it immediately for signature verification. Maximum 5 subscriptions per API key.
Request body (JSON)
| Parameter | Type | Description |
|---|---|---|
| url | string | HTTPS endpoint to deliver events to (public host required) |
| events | string[] | Array of event types: "pick.published" and/or "pick.settled" |
Response shape
// 201 Created
{
"subscription_id": "uuid",
"url": "https://yourserver.com/webhook",
"events": ["pick.published"],
"hmac_secret": "abc123..." // Store immediately — not retrievable after this response
}curl -X POST "https://betedge.tips/api/v1/webhooks" \
-H "Authorization: Bearer betedge_<growth_or_enterprise_key>" \
-H "Content-Type: application/json" \
-d '{"url":"https://yourserver.com/webhook","events":["pick.published"]}'const resp = await fetch("https://betedge.tips/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: "Bearer betedge_<key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://yourserver.com/webhook",
events: ["pick.published", "pick.settled"],
}),
});
const { subscription_id, hmac_secret } = await resp.json();resp = requests.post(
"https://betedge.tips/api/v1/webhooks",
headers={"Authorization": "Bearer betedge_<key>"},
json={
"url": "https://yourserver.com/webhook",
"events": ["pick.published", "pick.settled"],
},
)
secret = resp.json()["hmac_secret"] # store this immediatelyError responses
409 duplicate_url— A subscription already exists for this URL429 subscription_limit— Maximum 5 subscriptions per API key400 invalid_url— URL must be HTTPS and publicly reachable403 tier_upgrade_required— Starter tier; minimum_tier: growth
GET /api/v1/picks/stream
/api/v1/picks/streamServer-Sent Events (SSE) stream. Delivers new published picks as they happen. Sends a heartbeat comment every 30 seconds to keep the connection alive. Starter and Growth tiers return 403.
Event format
event: pick.published
data: {"id":"uuid","sport":"soccer","odds":1.91,"edge_pct":4.2,...}
: heartbeatcurl -N "https://betedge.tips/api/v1/picks/stream" \
-H "Authorization: Bearer betedge_<enterprise_key>" \
-H "Accept: text/event-stream"const evtSource = new EventSource(
"https://betedge.tips/api/v1/picks/stream",
{ withCredentials: false } // use a proxy that injects the Bearer header
);
evtSource.addEventListener("pick.published", (e) => {
const pick = JSON.parse(e.data);
console.log("New pick:", pick.id);
});import sseclient, requests
resp = requests.get(
"https://betedge.tips/api/v1/picks/stream",
headers={"Authorization": "Bearer betedge_<enterprise_key>"},
stream=True,
)
client = sseclient.SSEClient(resp)
for event in client.events():
if event.event == "pick.published":
import json; pick = json.loads(event.data)Error responses
403 tier_upgrade_required— Starter/Growth tier; minimum_tier: enterprise401 invalid_api_key— Missing or invalid Bearer token
Webhook signature verification
Every webhook delivery includes an X-BetEdge-Signature header containing an HMAC-SHA256 signature of the raw request body, formatted as sha256=<hex>. Verify the signature using the hmac_secret returned when you created the subscription.
Delivery headers
- X-BetEdge-Signature: sha256=<hex>
- X-BetEdge-Event: pick.published
- Content-Type: application/json
Verification examples
import { createHmac } from "node:crypto";
function verifyBetEdgeSignature(
rawBody: string,
signature: string,
secret: string
): boolean {
const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
// Constant-time compare to prevent timing attacks
const buf1 = Buffer.from(signature);
const buf2 = Buffer.from(expected);
if (buf1.length !== buf2.length) return false;
return require("node:crypto").timingSafeEqual(buf1, buf2);
}import hashlib, hmac
def verify_betedge_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Retry schedule
Failed deliveries are retried with exponential backoff: 1 min, 5 min, 30 min, 2 hr, 12 hr. After 5 failed attempts the subscription is marked exhausted and deliveries stop.
Error codes
| HTTP status | error code | Description |
|---|---|---|
| 400 | invalid_cursor | Pagination cursor is malformed |
| 400 | invalid_url | Webhook URL must be HTTPS and publicly reachable |
| 401 | invalid_api_key | Bearer token missing, invalid, or revoked |
| 403 | tier_upgrade_required | Endpoint not available on your current tier |
| 404 | not_found | Requested resource does not exist |
| 409 | duplicate_url | Webhook subscription for this URL already exists |
| 429 | rate_limit_exceeded | Hourly rate limit reached; see Retry-After header |
| 429 | monthly_quota_exceeded | Monthly request cap reached; resets at start of next month |
| 429 | subscription_limit | Maximum 5 webhook subscriptions per API key |
| 500 | internal_error | Unexpected server error; retry with exponential backoff |