On this page
API reference
The REST API gives your own code the account side of InPlayGuru: the strategies you built and every pick they produced, each with its result and the match data behind it, as the numbers stood at the moment of the pick and at full time. Read them, page through history, set a result by hand, create and manage strategies. Everything it returns is yours.
Overview #
Base URL: https://inplayguru.com/api/v1. The version is part of the path and v1 is the current one. The API is part of the Ultra plan and available only on it. It authenticates with a per-account key (see Authentication).
- Your strategies, with pick counts and strike rate as the app shows them
- Your picks, newest first or tailing from the last id you saw, with filters
- Rich match data behind every pick: detailed in-play stats, pre-match stats and the odds series of the picked match, as they stood at the moment of the pick and at full time
- Setting a result by hand and deleting picks
- Creating, renaming, enabling, muting, duplicating, importing and deleting strategies
- Rule editing and league filters, which stay in the app
- Real-time push. Webhooks do that; the API is for reading history and managing strategies
Endpoints at a glance
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /picks | Your picks, filtered and paged. Details |
| GET | /picks/{id} | One pick with all of its match data. Details |
| PATCH | /picks/{id} | Set the result by hand. Details |
| DELETE | /picks/{id} | Delete a pick from the history |
| GET | /strategies | All your strategies with stats. Details |
| POST | /strategies | Create an empty, disabled strategy. Details |
| POST | /strategies/import | Copy a shared strategy from its share key |
| GET | /strategies/{id} | One strategy with its per-league breakdown. Details |
| PATCH | /strategies/{id} | Rename, enable or disable, mute alerts, set outcome, note or pre-match lead. Details |
| DELETE | /strategies/{id} | Delete a strategy and its pick history |
| POST | /strategies/{id}/duplicate | Copy a strategy, rules included |
OpenAPI spec #
Want the whole API inside Postman, Insomnia or your editor without typing a single URL? Import this one file and every endpoint arrives set up: what each request needs, what it answers, ready to run in seconds. The file comes straight from the API itself, so what you import always matches it.
https://inplayguru.com/developers/api/openapi.json
npx @openapitools/openapi-generator-cli generate \
-i https://inplayguru.com/developers/api/openapi.json \
-g typescript-fetch \
-o ./inplayguru-client
OpenAPI 3.0.3 is the dialect every one of these tools reads in full. The API also accepts PUT as an alias of PATCH; the document lists PATCH only.
Authentication #
-
Generate your keyOpen the API settings page and click Generate API key. There is one key per account. Regenerating replaces it on the spot and revoking disables it; scripts using the old value fail with 401 from that moment on.
-
Send it as a bearer tokenEvery request carries the key in the Authorization header. Nothing else is needed: no session, no cookies, no CSRF token.
curl -H "Authorization: Bearer YOUR_API_KEY" \ https://inplayguru.com/api/v1/strategies -
Keep it secretThe key has full read and write access to your strategies and picks. Keep it in an environment variable or a secrets store, never in client-side code or a public repository, and regenerate it the moment you suspect it leaked.
Conventions #
Requests and responses
- Everything is JSON, UTF-8. Send request bodies as application/json; responses are JSON whatever Accept header you send.
- Successful reads and writes return the object under data. Lists add a meta object with paging information.
- Updates use PATCH (or PUT, treated the same) and are partial: send only the fields you want to change. Deletes answer 204 with an empty body.
- Booleans in query strings accept true, false, 1 and 0.
- New fields and query parameters can appear over time. Read what you need and ignore what you do not recognise.
Identifiers and timestamps
- Pick and strategy ids are integers that can exceed 32-bit range: store them as 64-bit integers or strings.
- Match, team and league ids are opaque strings, identical to the ones in webhook payloads. Never parse or order by them; compare byte-for-byte.
- The API's own timestamps (created_at, updated_at) are ISO 8601 in UTC with a +00:00 offset. Match kickoffs inside match.date keep the feed's Z notation. Parse both with a real ISO parser.
- Everything said in Parsing & precision applies here too: absent means unknown, never zero, and strike is three-state.
Pagination
The picks list is cursor based, keyed on the pick id, which only ever grows. Two cursors exist and they are never combined:
| Parameter | Direction | Use it for |
|---|---|---|
| cursor | Newest first | Paging back through history. Pass meta.next_cursor of one page as cursor of the next until it is null. |
| after_id | Oldest first | Tailing new picks. Remember the highest id you have seen (meta.last_id) and pass it back on the next poll; only picks created since are returned. |
limit sets the page size, 20 at most. Pages are small because they are data-heavy: every pick loads its match record, and an expanded pick carries its detailed stats, pre-match numbers and odds series. There is nothing you cannot read this way; you read it page by page, at the published rate, spread over time. meta.has_more tells whether another page exists. The strategies list is not paginated: it always arrives whole.
Strategies #
GET /strategies
Every strategy of the account, newest first, each with its stats, in one response. The list carries no per-league rows, so it stays light however many strategies you run.
{
"data": [
{
"id": 960718,
"name": "Late Goals from Building Pressure",
"enabled": true,
"alerts_enabled": true,
"outcome": { "id": 10001, "label": "Over 1.5 Goals Since Picked" },
"is_prematch": false,
"prematch_lead_minutes": null,
"note": "When late pressure builds but goals are still missing",
"league_filter": { "id": 4412, "name": "Top leagues" },
"rules_count": 4,
"stats": { "picks": 1342, "hits": 1012, "misses": 78, "open": 252, "strike_rate": 93 },
"share_url": "https://inplayguru.com/s/86027419",
"created_at": "2026-01-29T22:43:05+00:00",
"updated_at": "2026-09-15T18:02:44+00:00"
}
]
}
GET /strategies/{id}
One strategy in the same shape, plus leagues: the per-league breakdown the strategy page shows, one row per league the strategy has picked in, biggest first, with picks, hits, misses, open, strike rate and fair odd. Every response that carries a single strategy (create, update, duplicate, import) includes it as well.
{
"data": {
"id": 960718,
"name": "Late Goals from Building Pressure",
"stats": { "picks": 1342, "hits": 1012, "misses": 78, "open": 252, "strike_rate": 93 },
"leagues": [
{
"league": { "id": "RFJobnowRFU0Rm89", "name": "Italy Serie A", "cc": "it" },
"picks": 412, "hits": 318, "misses": 24, "open": 70,
"strike_rate": 93, "fair_odd": 1.08
},
{
"league": { "id": "TUZiaGxrb3p4RzA9", "name": "England Premier League", "cc": "gb" },
"picks": 297, "hits": 221, "misses": 19, "open": 57,
"strike_rate": 92, "fair_odd": 1.09
}
]
}
}
The other fields are the ones of the list item above. A league's id is the same opaque id pick payloads carry, so it can go straight into the picks list's league_id filter.
POST /strategies
Creates a strategy and answers 201 with the new object. It starts disabled and without rules: rules are written in the app's rule builder, and the strategy can be enabled (here or there) once it has at least one. The account's strategy slots apply exactly as in the app.
| Field | Type | Description |
|---|---|---|
| name | string, required | 1 to 80 characters. |
| outcome_id | integer | null | The outcome the strategy is judged on. Ids are the ones the app's outcome picker uses and the ones settled picks carry in outcome_id. Without an outcome, picks never settle automatically. |
| note | string | null | Free text, up to 2,000 characters. |
| alerts_enabled | boolean | Whether picks are sent to Telegram. Defaults to true. |
| prematch_lead_minutes | integer | null | Set it to make a pre-match strategy: how many minutes before kickoff it is evaluated (1 to 10,080). null means in-play. |
curl -X POST https://inplayguru.com/api/v1/strategies \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Corners after the hour", "outcome_id": 10008, "alerts_enabled": false}'
PATCH /strategies/{id}
Partial update; send any subset of name, enabled, alerts_enabled, outcome_id, note and prematch_lead_minutes, with the same rules as above. Enabling a strategy that has no rules answers 422 with code no_rules: a strategy without conditions never picks a match, so the switch cannot sit on while doing nothing. Changes reach the matching engine the same way a click in the app does.
curl -X PATCH https://inplayguru.com/api/v1/strategies/960718 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"alerts_enabled": false}'
POST /strategies/{id}/duplicate
Copies the strategy with its rules and league filter and answers 201 with the copy: named "… (Copy)", disabled, with an empty history and its own share link.
POST /strategies/import
Body: {"share_key": "86027419"}, the last segment of any share link (/s/86027419). Copies the shared strategy into your account, rules included, and enables it right away, like importing from the gallery does. Answers 201 with the new strategy, or 404 with code unknown_share_key.
DELETE /strategies/{id}
Deletes the strategy and every pick it ever made, permanently. Answers 204. There is no undo, on the API or in the app.
Picks #
GET /picks
Picks across all your strategies, or the ones you name. Without cursors the newest picks come first. Every parameter is optional.
| Parameter | Type | Description |
|---|---|---|
| strategy_id | integer or list | One id or a comma-separated list (960718,960730). Only your own strategies; an unknown id answers 422. |
| result | hit | miss | open | open is every pick without a result: not completed yet, or ended without a verdict. |
| created_after / created_before | ISO 8601 | Bounds on the pick time, inclusive. A date alone (2026-09-01) means midnight UTC. |
| league_id | string | A league id as it appears in pick payloads (match.league.id). Best combined with a date bound on very large histories. |
| match_id | string | A match id as it appears in pick payloads. Returns every pick your strategies made on that match. |
| include | list | Comma-separated expansions: snapshot, prematch, stats, odds, odds_series, details, or all. Without it each pick is a compact row (see the pick object). |
| limit | integer | Page size, 20 by default and at most. Above that: 422. |
| cursor / after_id | integer | See Pagination. Never both at once. |
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://inplayguru.com/api/v1/picks?strategy_id=960718&result=miss&created_after=2026-09-15&include=snapshot"
{
"data": [
{
"id": 738656899,
"strategy_id": 960718,
"strategy": { "id": 960718, "name": "Late Goals from Building Pressure" },
"created_at": "2026-02-14T21:17:29+00:00",
"updated_at": "2026-02-14T21:47:10+00:00",
"minute": 70,
"score": [1, 1],
"is_prematch": false,
"result": "miss",
"strike": false,
"outcome_id": 10001,
"manual_result": false,
"match": {
"id": "RGFQZVdJbS82VG9mMFlEaDU2SmJFQT09",
"date": "2026-02-14T19:45:00.000Z",
"ended": true,
"score": [1, 1],
"score_ht": [0, 1],
"favorite": 0,
"home": { "id": "blVwS0RWOENsd009", "name": "Inter Milan", "cc": "it" },
"away": { "id": "VWxIQllid2tKbjg9", "name": "Juventus", "cc": "it" },
"league": { "id": "RFJobnowRFU0Rm89", "name": "Italy Serie A", "cc": "it" }
}
}
],
"meta": { "limit": 20, "has_more": true, "next_cursor": "738656899", "last_id": null }
}
GET /picks/{id}
One pick with every expansion applied: the numbers at the moment of the pick, both teams' pre-match stats, the full-time stats and quotes, the odds series of the match and the full team, league, season and stadium objects. The richest way to read a pick, meant for one at a time.
PATCH /picks/{id}
Sets the result by hand. Body: {"strike": true} for a hit, false for a miss, null to clear it. Exactly what the history page does when you set a result yourself: the automatic outcome is dropped (outcome_id becomes null, manual_result turns true) and the strategy's strike rate follows. Answers the updated pick.
DELETE /picks/{id}
Removes the pick from the strategy history and from its counts, permanently, the same way the history page's delete does. Answers 204.
Objects #
Strategy
| Field | Type | Description |
|---|---|---|
| id | integer | The strategy id, the same one share links and webhook payloads carry. |
| name / note | string / string | null | As configured. |
| enabled | boolean | Whether the engine evaluates it. |
| alerts_enabled | boolean | Whether its picks are sent to Telegram. Picks are recorded and delivered to webhooks either way. |
| outcome | object | null | id and label of the outcome picks are judged on. |
| is_prematch / prematch_lead_minutes | boolean / integer | null | Pre-match strategies fire a set number of minutes before kickoff; in-play ones carry null. |
| league_filter | object | null | id and name of the attached league filter. Filters are managed in the app. |
| rules_count | integer | Number of rules. Zero means the strategy cannot be enabled yet. |
| stats | object | picks, hits, misses, open (picks without a result) and strike_rate, hits as a percentage of settled picks, 0 to 100, or null while nothing has settled. The same numbers the strategy page shows. |
| share_url | string | null | The public share link. Anyone with it can import a copy of the strategy. |
| leagues | array | Single-strategy responses only. One row per league the strategy has picked in, biggest first: league (opaque id, name, cc), picks, hits, misses, open, strike_rate and fair_odd (100 divided by the strike rate, as on the strategy page). Empty until the first pick. |
| created_at / updated_at | string (ISO 8601) | When it was created and last changed. |
Pick
A pick is described at two moments: the moment of the pick, with everything the strategy saw when it fired, and full time, with how the match ended. Lists return a compact row; include adds the detailed data, each part tied to its moment:
| include | As of | What you get |
|---|---|---|
| snapshot | The moment of the pick | Score, clock, the detailed in-play stats (shots, corners, attacks, possession, xG, momentum, cards and more) and the quotes of every market, exactly as the strategy evaluated them. |
| prematch | Before kickoff | Both teams' pre-match stats: goal, corner and shot averages, result, BTTS, clean-sheet and goal-line percentages, over the last 5 and last 10 games (all, home-only, away-only) and head-to-head. Plus the pre-match quotes. |
| stats | Full time | The same detailed stats at full time, and at half time. |
| odds | Full time | The quotes of every market at full time. |
| odds_series | Full time | The odds time series of the picked match: every recorded quote per market, in minute order, from before kickoff to the final whistle. |
| details | Before kickoff | Full team and league objects (form, formation, manager, table positions), season and stadium. |
Everything marked full time is null until match.ended is true, then it is final. Stat maps, quotes, team and league objects have the same shapes as in webhook payloads, so a parser written for events reads them as they are; stat keys, market ids and the timer stage map are documented once, on the webhooks page.
| Field | Type | Description |
|---|---|---|
| id / strategy_id | integer | The pick and the strategy that made it. strategy repeats the strategy's id and name for convenience. |
| created_at / updated_at | string (ISO 8601) | When the pick fired, and when it last changed (settlement, manual result). |
| minute | integer | null | Match minute at pick time. null for pre-match picks. |
| score | [int, int] | null | Score at the moment of the pick, [home, away]. null for pre-match picks. |
| is_prematch | boolean | null | Whether the pick fired before kickoff. |
| result | hit | miss | pending | unresolved | pending until the match has ended; unresolved once it ended without a verdict (strategies with no outcome, or a verdict the platform could not compute). The derived, human-friendly view of strike. |
| strike | true | false | null | Hit, miss, no result. The raw three-state value webhooks send too; never collapse it with a truthiness check. |
| outcome_id | integer | null | The outcome the result was settled on. null while open, and after a result set by hand. |
| manual_result | boolean | true when the result was set by hand, in the app or through the API. |
| match | object | Always: id, date, ended, the final score and score_ht, favorite and compact home, away, league objects (id, name, country code). The includes add prematch, stats with stats_ht, odds, odds_series, and with details the full team and league objects plus season and stadium. |
| snapshot | object | With include=snapshot: score, timer, stats and odds at the moment the pick fired. |
"prematch": {
"stats": {
"goals_scored_avg": {
"last_5": { "all": [1.8, 1.4], "home": [2.2, 1.6], "away": [1.4, 1.2] },
"last_10": { "all": [1.6, 1.3], "home": [2.0, 1.5], "away": [1.2, 1.1] },
"h2h": 1.6
},
"btts_pc": {
"last_5": { "all": [60, 40], "home": [80, 20], "away": [40, 60] },
"last_10": null,
"h2h": 60
}
},
"odds": {
"1": { "odd_home": 2.1, "odd_draw": 3.3, "odd_away": 3.6 }
}
},
"odds_series": {
"1": [
{ "minute": null, "odd_home": 2.1, "odd_draw": 3.3, "odd_away": 3.6 },
{ "minute": 12, "odd_home": 2.0, "odd_draw": 3.4, "odd_away": 3.9, "ss": "0-0" },
{ "minute": 70, "odd_home": 2.05, "odd_draw": 2.0, "odd_away": 13.0, "ss": "1-1" }
]
}
Every pre-match value is a [home, away] pair, the fixture's home team first. home and away count only the games each team played at home or away; h2h is a single number over the last five meetings. Keys ending in _pc are percentages from 0 to 100, everything else is a per-match average. A sample without enough games is null, never zero. In the series, the quote taken before kickoff comes first with minute: null.
Coverage follows the match: lower-tier competitions carry fewer stats and thinner enrichment, and a stat missing from a map means it was not covered, not that it was zero. The data almanac lists every way these numbers behave in production.
Errors #
Nothing fails silently. Every error comes back as JSON, with a message written for a person and, where your code should branch on it, a stable code. Anything in the 4xx range is refused before it touches your data, so you can fix the request and simply send it again.
{
"message": "Add at least one rule in the app before enabling this strategy.",
"code": "no_rules"
}
{
"message": "The name field is required.",
"errors": { "name": ["The name field is required."] }
}
Limits & fair use #
- Rate: 120 requests per minute per account, across every endpoint. Each response carries X-RateLimit-Limit and X-RateLimit-Remaining; past the ceiling you get 429 with Retry-After. The window is a minute on purpose: a runaway loop is stopped within seconds instead of burning an hourly budget, and a script that waits out the Retry-After is back within the same minute.
- Page size: 20 picks per page. Every pick loads its match record and an expanded pick carries its detailed stats, pre-match numbers and odds series; the pages are small so that the data stays quick to serve and to read.
- Pace: load anything you want, at that rate, spread over time. Walking a long history page by page is expected; hammering is not. A client that keeps hitting the ceiling, accidental or not, is something we notice and act on: keep it steady and it keeps working, spam it and the key gets blocked.
- Polling: following your picks by fetching them is a perfectly fine way to use the API; once a minute with after_id is plenty. For real time, webhooks are the better tool and were made for exactly that: they push each pick the second it fires, while the API's rate is set for reading history, not for polling every few seconds.
- Your data, your uses. Store and model everything the API returns for as long as you like. The boundary is the same as for webhooks: the match data inside your picks is not a feed to redistribute or resell (see Storing the data).
The API is best-effort, without a formal SLA: brief maintenance windows can interrupt it, and it may be throttled or paused to protect platform stability, with notice where feasible.
Recipes #
Fetch recent picks
Everything your strategies picked in the last 24 hours, across all of them, newest first, paging with the cursor until the window is exhausted. For real time, webhooks are the better tool and were made for exactly that: they push each pick the second it fires. Fetching this way is fine too, at the API's pace: the rate limit is set for reading history, not for polling every few seconds.
const BASE = "https://inplayguru.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.INPLAYGURU_API_KEY}` };
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const picks = [];
let cursor = null;
do {
const url = new URL(`${BASE}/picks`);
url.searchParams.set("created_after", since);
url.searchParams.set("limit", "20");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers });
if (res.status === 429) { // over the pace: wait it out, retry the same page
const wait = Number(res.headers.get("retry-after") || 5);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
continue;
}
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const body = await res.json();
picks.push(...body.data);
cursor = body.meta.next_cursor; // null on the last page
} while (cursor);
console.log(`${picks.length} picks since ${since}`);
Tail new picks into your own database
Remember the highest id you processed and ask for everything after it. Empty pages are normal between matches; the cursor simply stays where it was.
import os, json, requests
BASE = "https://inplayguru.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['INPLAYGURU_API_KEY']}"}
STATE = "last_id.txt"
last_id = int(open(STATE).read()) if os.path.exists(STATE) else 0
while True:
r = requests.get(f"{BASE}/picks", headers=HEADERS,
params={"after_id": last_id, "limit": 20, "include": "snapshot"})
r.raise_for_status()
body = r.json()
for pick in body["data"]:
store(pick) # your own persistence
if body["meta"]["last_id"] is not None:
last_id = body["meta"]["last_id"]
open(STATE, "w").write(str(last_id))
if not body["meta"]["has_more"]:
break
Mute every strategy below a strike rate
Read the list once, decide client-side, patch what needs patching. Strategies without settled picks have a null strike rate and are skipped.
BASE="https://inplayguru.com/api/v1"
AUTH="Authorization: Bearer $INPLAYGURU_API_KEY"
curl -s -H "$AUTH" "$BASE/strategies" \
| jq -r '.data[] | select(.stats.strike_rate != null and .stats.strike_rate < 60 and .alerts_enabled) | .id' \
| while read -r id; do
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
-d '{"alerts_enabled": false}' "$BASE/strategies/$id" > /dev/null
echo "muted $id"
done
What all three teach: the API is a plain HTTPS surface with no SDK to install. Any language with an HTTP client and a JSON parser is enough, and the OpenAPI spec gives you a typed client when you want one.