New features, changes & important service notices — announced first on our Telegram channel. Updates & important notices — first on Telegram.

Join @inplayguru_info
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).

What it covers
  • 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
What it does not cover
  • 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.

spec URL OpenAPI 3.0.3
https://inplayguru.com/developers/api/openapi.json

Open the spec Download JSON

a typed TypeScript client in one command shell
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 #

  1. Generate your key
    Open 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.
  2. Send it as a bearer token
    Every request carries the key in the Authorization header. Nothing else is needed: no session, no cookies, no CSRF token.
    your first request curl
    curl -H "Authorization: Bearer YOUR_API_KEY" \
         https://inplayguru.com/api/v1/strategies
  3. Keep it secret
    The 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.
The plan is checked on every call. A key keeps working for as long as the account is on the Ultra plan. If the plan lapses, requests answer 403 with code plan_required until it is active again; the key itself stays valid.

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.

GET /api/v1/strategies (shortened) JSON
{
    "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.

GET /api/v1/strategies/960718 (shortened) JSON
{
    "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.
create a strategy curl
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.

mute alerts on one strategy curl
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.

Slots are enforced. When every strategy slot of the account is in use, create, duplicate and import answer 403 with code strategy_limit_reached plus max and current. Delete a strategy to make room.

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.
yesterday's misses of one strategy, with the numbers at pick time curl
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"
response (shortened, no include) JSON
{
    "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 }
}
A pick is recorded twice. First at the moment it fires, with everything the strategy saw. Then once its match has ended, when the result, the final score and the full-time numbers are filled in. Until that second moment those fields are null and result reads pending. To collect completed picks, re-read the last few hours with created_after, or receive each one as it is settled through pick.settled webhooks.

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.
match.prematch and match.odds_series (shortened) JSON
"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.

401
The key was not accepted
No key on the request, a mistyped one, or one that has since been regenerated. Check the Authorization header, or generate a fresh key on the API settings page.
missing_api_key invalid_api_key expired_api_key
403
The plan does not include the API
The account is not on the Ultra plan. Your key stays valid and starts working again by itself once the plan is active. The response carries upgrade_url.
plan_required
403
Every strategy slot is in use
Create, duplicate and import need a free slot. Delete a strategy to make room. The response carries max and current.
strategy_limit_reached
404
Nothing there for your account
The strategy or pick does not exist, or belongs to someone else: both answer the same way. For imports, no strategy stands behind that share key.
unknown_share_key
422
Understood, but not applicable as sent
A field failed validation, and the errors object names each field and why. Or the request asks for something that cannot apply, and the code says what.
no_rules nothing_to_update conflicting_cursors unknown_strategy invalid_strategy_id unknown_league unknown_include
429
A short pause
Over the per-account rate. Wait the seconds given in Retry-After, never more than a minute, and carry on where you left off. Nothing is lost.
rate_limited
5xx
Something failed on our side
Retry with a growing pause between attempts. Reads are always safe to repeat. For a write, read the strategy or pick back first to see whether it went through.
a refusal with a code JSON
{
    "message": "Add at least one rule in the app before enabling this strategy.",
    "code": "no_rules"
}
a validation error JSON
{
    "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.

recent.mjs, the last 24 hours across every strategy Node.js
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.

tail.py, run every minute Python
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.

mute.sh bash + jq
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.