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

Join @inplayguru_info
On this page

Webhooks

Webhooks push what happens inside InPlayGuru to your own software as it happens. When one of your strategies fires a pick, and again when that pick settles, we send a signed JSON POST to an HTTPS endpoint you control — ready for your own dashboards, notification pipelines, spreadsheets or research databases.

Overview #

Webhooks are part of the Ultra plan and available only on it, like everything documented here.

Two event types cover the full life of a pick. Each delivery is a single event — you will never receive batched payloads.

Event Fires Typical timing
pick.created The moment one of your strategies fires a pick. The payload carries a full snapshot of the match at that exact moment — score, timer, in-play stats and market quotes. Within seconds of the pick
pick.settled When the pick's outcome is known. The payload carries the final result, whether the pick hit, and full-time stats. Shortly after the match ends

Deliveries are per account: one endpoint URL receives the events of all your strategies. Configure it on the webhook settings page, which also shows a live delivery log of your last 20 attempts (kept 30 days).

Webhook delivery is available on eligible plans. If the settings page isn't accessible on your account, your current plan doesn't include it — contact support for options.

Quick start #

  1. Point us at your endpoint
    Save an HTTPS URL on the webhook settings page. Any route on your server that accepts a POST with a JSON body works — no registration handshake, no challenge round-trip. Saving the URL also reveals your signing secret.
  2. Acknowledge first, process after
    Your endpoint has 5 seconds to answer with a 2xx status. Respond immediately, then do the real work — anything slow (database writes, downstream calls) belongs after the response:
    server.js — a minimal receiver Node.js
    const express = require('express');
    const app = express();
    
    // Keep the raw bytes — signature verification needs them (see below).
    app.post('/inplayguru/webhook', express.raw({ type: 'application/json' }), (req, res) => {
        res.sendStatus(200);              // acknowledge first
        const event = JSON.parse(req.body);
        handleEvent(event);               // then do the real work
    });
    
    app.listen(8080);
  3. Verify the signature
    Every request is signed with your account's secret so you can prove it came from InPlayGuru. See Verifying signatures for ready-made snippets.
  4. Branch on the event type
    Switch on the top-level type field and ignore any type you don't recognise — additional event types may be introduced over time, and an unknown type must never make your endpoint fail.
  5. Watch your delivery log
    Fire a pick with any strategy and watch it arrive. The settings page records every attempt with status, latency and retry trace — your first stop whenever something looks off.

Delivery & retries #

The request

Deliveries are POST requests with a JSON body and these headers:

Header Value
Content-Type application/json
User-Agent InPlayGuru-Webhooks/1.0
X-InPlayGuru-Signature Hex HMAC-SHA256 of the raw request body, keyed with your signing secret — see Verifying signatures.
X-InPlayGuru-Timestamp Unix timestamp (seconds) of the moment this delivery attempt was sent. Each retry carries a fresh timestamp.
Cache-Control no-store

What counts as delivered

Any 2xx status returned within 5 seconds (3 seconds to establish the connection). The response body is ignored — only the status code matters. Since we don't read response bodies, don't return data in them — and never sensitive data.

Endpoints must be served over HTTPS with a valid TLS certificate. Keeping the endpoint reachable — DNS, certificates, firewall rules, hosting uptime — is your side of the contract; failed attempts are visible in your delivery log either way.

Redirects are not followed. A 301 or 302 counts as a failed delivery. Point your webhook URL at its final destination — watch out for http → https and www redirects added by proxies.

Retry policy

A delivery that fails for a reason that can clear on its own (a timeout, a connection error, a 5xx, a 408 or a 429) is retried automatically:

  • 3 total attempts per event, with 2s, 3s pauses between them.
  • Every attempt is signed independently and carries the same event_id, so retries are recognisable as the same event.
  • Statuses that cannot change on a retry (400, 401, 403, 404, 405, 410, 422) are not retried. A rejected URL, key or payload is rejected again two seconds later, so the event is marked failed on the first response.
  • After the final attempt the event is marked failed and is not sent again — there is no automatic replay of historical failed events. The full attempt trace stays visible in your delivery log, and events missed during endpoint downtime cannot be reconstructed afterwards.

Events dispatch from a dedicated delivery queue the moment they occur — pick.created normally reaches your endpoint within seconds of the strategy firing.

Events & payloads #

The envelope

Every event shares the same top-level shape:

Field Type Description
event_id string (uuid) Unique per event and stable across retries — your deduplication key.
event_created_at string (ISO 8601) When the event was generated. Use this for ordering, not arrival time.
schema_version string Payload schema version — currently 1.
type string pick.created or pick.settled.
data object The event body: pick, strategy and match objects, documented below.

pick.created

Sent the moment a strategy's conditions match. The data.match object is a snapshot of the match at pick time — the same numbers your strategy saw when it fired.

pick.created — a real delivery, values as sent JSON
{
    "event_id": "a9d04b5d-7caf-4cbb-bf87-e95f7ca7815d",
    "event_created_at": "2026-02-14T21:17:30+00:00",
    "schema_version": "1",
    "type": "pick.created",
    "data": {
        "pick": {
            "id": 738656899,
            "created_at": "2026-02-14T21:17:29+00:00"
        },
        "strategy": {
            "id": 960718,
            "name": "⚡Late Goals from Building Pressure",
            "created_at": "2026-01-29T22:43:05+00:00",
            "note": "When late pressure builds but goals are still missing, it often explodes in the last 10–20 minutes",
            "has_alerts_enabled": true,
            "strike_rate": 92.9,
            "strike_rate_league": 87.8
        },
        "match": {
            "id": "RGFQZVdJbS82VG9mMFlEaDU2SmJFQT09",
            "date": "2026-02-14T19:45:00.000Z",
            "score": [1, 1],
            "timer": { "min": 70, "live": true, "stage": 4, "ext": 0, "status": 1 },
            "home": {
                "id": "blVwS0RWOENsd009",
                "name": "Inter Milan",
                "cc": "it",
                "image_id": "b0NCWi9FdjBmbWc9",
                "form": ["W", "W", "W", "W", "W"],
                "formation": "3-5-2",
                "manager": { "id": "elMxREd5bjdCSTA9", "name": "Cristian Chivu", "cc": "ro" },
                "market_value": 666800000,
                "avg_player_age": 29.1,
                "foreigners": 16,
                "national_team_players": 15,
                "youth_national_team_players": 1
            },
            "away": {
                "id": "VWxIQllid2tKbjg9",
                "name": "Juventus",
                "cc": "it",
                "image_id": "K1pSZjY4emRIcEE9",
                "form": ["W", "D", "W", "L", "D"],
                "formation": "4-2-3-1",
                "manager": { "id": "dnJJaWFGc1czOFU9", "name": "Luciano Spalletti", "cc": "it" },
                "market_value": 560200000,
                "avg_player_age": 27.2,
                "foreigners": 18,
                "national_team_players": 17,
                "youth_national_team_players": 2
            },
            "favorite": 0,
            "league": {
                "id": "RFJobnowRFU0Rm89",
                "name": "Italy Serie A",
                "cc": "it",
                "home_pos": "1",
                "away_pos": "4",
                "round": "25"
            },
            "season": {
                "name": "Serie A 25/26",
                "round": "25",
                "max_rounds": "38",
                "date_start": "2025-08-23T00:00:00.000Z",
                "date_end": "2026-05-24T23:59:59.000Z"
            },
            "stadium": {
                "name": "Giuseppe Meazza",
                "city": "Milan",
                "country": "Italy",
                "capacity": "80018",
                "weather": {
                    "temp": 9.17,
                    "desc": "Light intensity drizzle",
                    "icon": "09n",
                    "humidity": 90,
                    "wind": 1.03
                }
            },
            "stats": {
                "action_areas": [24.9, 33.7],
                "attacks": [84, 64],
                "corners": [2, 0],
                "crosses": [16, 6],
                "crossing_accuracy": [13, 50],
                "dangerous_attacks": [60, 39],
                "fouls": [11, 7],
                "goals": [1, 1],
                "key_passes": [7, 6],
                "momentum": [92, 8],
                "off_target": [6, 1],
                "offsides": [1, 0],
                "on_target": [5, 6],
                "passing_accuracy": [90, 87],
                "penalties": [0, 0],
                "possession": [61, 39],
                "redcards": [0, 1],
                "saves": [5, 0],
                "shots_blocked": [2, 0],
                "substitutions": [4, 3],
                "xg": [0.75, 0.77],
                "yellowcards": [2, 1]
            },
            "odds": {
                "1": { "odd_home": 2.05, "odd_draw": 2, "odd_away": 13, "ss": "1-1", "suspended": false },
                "2": { "odd_home": 9.5, "odd_draw": 1.063, "odd_away": 29, "ss": "1-1", "suspended": true },
                "3_0.50": { "handicap": "0.50", "odd_over": 1.1, "odd_under": 7, "ss": "0-0", "suspended": true },
                "3_1.50": { "handicap": "1.50", "odd_over": 1.111, "odd_under": 6.5, "ss": "1-0", "suspended": true },
                "3_2.50": { "handicap": "2.50", "curr_line": true, "odd_over": 1.667, "odd_under": 2.2, "ss": "1-1", "suspended": false },
                "3_3.50": { "handicap": "3.50", "odd_over": 4.333, "odd_under": 1.222, "ss": "1-1", "suspended": false },
                "3_4.50": { "handicap": "4.50", "odd_over": 13, "odd_under": 1.04, "ss": "1-1", "suspended": false },
                "3_5.50": { "handicap": "5.50", "odd_over": 26, "odd_under": 1.01, "ss": "1-1", "suspended": true },
                "3_6.50": { "handicap": "6.50", "odd_over": 26, "odd_under": 1.01, "ss": "1-1", "suspended": true },
                "4_0.50": { "handicap": "0.50", "odd_over": 1.727, "odd_under": 2, "ss": "0-0", "suspended": true },
                "4_1.50": { "handicap": "1.50", "odd_over": 2.1, "odd_under": 1.667, "ss": "1-0", "suspended": true },
                "4_2.50": { "handicap": "2.50", "odd_over": 8, "odd_under": 1.083, "ss": "1-1", "suspended": true },
                "4_3.50": { "handicap": "3.50", "odd_over": 26, "odd_under": 1.01, "ss": "1-1", "suspended": true },
                "4_4.50": { "handicap": "4.50", "odd_over": 26, "odd_under": 1.01, "ss": "1-1", "suspended": true },
                "7_4.00": { "handicap": "4.00", "odd_over": 2.1, "odd_under": 1.7, "ss": "1-1", "suspended": true },
                "7_4.50": { "handicap": "4.50", "odd_over": 1.95, "odd_under": 1.85, "ss": "1-1", "suspended": false },
                "7_5.00": { "handicap": "5.00", "odd_over": 2.1, "odd_under": 1.7, "ss": "1-1", "suspended": true },
                "7_5.50": { "handicap": "5.50", "odd_over": 2.05, "odd_under": 1.75, "ss": "1-1", "suspended": true },
                "7_6.00": { "handicap": "6.00", "odd_over": 2.05, "odd_under": 1.75, "ss": "1-1", "suspended": true },
                "7_6.50": { "handicap": "6.50", "odd_over": 2.05, "odd_under": 1.75, "ss": "1-1", "suspended": true },
                "7_7.00": { "handicap": "7.00", "odd_over": 2.05, "odd_under": 1.75, "ss": "1-0", "suspended": true },
                "7_7.50": { "handicap": "7.50", "odd_over": 2.025, "odd_under": 1.775, "ss": "1-0", "suspended": true },
                "7_8.00": { "handicap": "8.00", "odd_over": 2.025, "odd_under": 1.775, "ss": "0-0", "suspended": true },
                "7_8.50": { "handicap": "8.50", "odd_over": 2, "odd_under": 1.8, "ss": "0-0", "suspended": true },
                "7_9.00": { "handicap": "9.00", "odd_over": 2.025, "odd_under": 1.775, "ss": "0-0", "suspended": true },
                "8_0.50": { "handicap": "0.50", "odd_over": 2.1, "odd_under": 1.7, "ss": "1-1", "suspended": true },
                "8_1.00": { "handicap": "1.00", "odd_over": 2.35, "odd_under": 1.575, "ss": "1-1", "suspended": true },
                "8_1.50": { "handicap": "1.50", "odd_over": 2.85, "odd_under": 1.4, "ss": "1-1", "suspended": true },
                "8_2.00": { "handicap": "2.00", "odd_over": 2.15, "odd_under": 1.675, "ss": "1-0", "suspended": true },
                "8_2.50": { "handicap": "2.50", "odd_over": 2.1, "odd_under": 1.7, "ss": "1-0", "suspended": true },
                "8_3.00": { "handicap": "3.00", "odd_over": 2.075, "odd_under": 1.725, "ss": "0-0", "suspended": true },
                "8_3.50": { "handicap": "3.50", "odd_over": 2.075, "odd_under": 1.725, "ss": "0-0", "suspended": true },
                "8_4.00": { "handicap": "4.00", "odd_over": 2.075, "odd_under": 1.725, "ss": "0-0", "suspended": true },
                "9": { "odd_yes": 1.5, "odd_no": 2.5, "ss": "1-0", "suspended": true },
                "10": { "odd_yes": 3.75, "odd_no": 1.25, "ss": "1-0", "suspended": true },
                "11": { "odd_yes": 11, "odd_no": 1.05, "ss": "1-1", "suspended": false },
                "13": { "odd_odd": 2.25, "odd_even": 1.571, "ss": "1-1", "suspended": false },
                "14": { "odd_home": 1.111, "odd_away": 6.5, "ss": "1-1", "suspended": false }
            }
        }
    }
}

Notice the shapes worth trusting your parser to: match, team and league IDs are opaque strings while pick and strategy IDs are integers; some numeric-looking fields (capacity, table positions, rounds) arrive as strings; and real stats maps can carry experimental keys beyond those shown — ignore what you don't recognise. This top-tier match also carries maximum enrichment — team form, formation, manager, market value, weather — none of which is guaranteed for smaller competitions.

Field reference

Field Type Description
data.pick.id integer Your pick's unique id — matches the pick shown in your strategy history.
data.pick.created_at string (ISO 8601) When the pick was generated.
data.strategy.id / .name / .note int / string / string | null The strategy that fired, as configured on your strategies page.
data.strategy.strike_rate number | null The strategy's overall strike rate (%) at event time.
data.strategy.strike_rate_league number | null The strategy's strike rate (%) in this match's league.
data.match.id string Opaque match id — stable across both events of the same pick. Never parse or decode it; compare it byte-for-byte.
data.match.score [int, int] Current score as a [home, away] pair.
data.match.timer object Match clock: min (minute), live (boolean), stage (see the stage reference below) and injury-time info where the feed provides it.
data.match.home / .away object Team objects. Beyond id (opaque string) and name, treat every field as optional enrichment that may be absent: country code, image_id, recent form, formation, manager, market_value and squad indicators (average age, foreigners, national-team players). Coverage follows league tier.
data.match.favorite 0 | 1 | null Pre-match favorite — the side with the lowest closing 1X2 quote: 0 = home, 1 = away, null = none determined. One side is designated even when the quotes are close; it's a convenience field, so apply your own definition instead if yours differs.
data.match.league object League id (opaque string), name, country code — plus current table positions (home_pos / away_pos) and round as strings, when known.
data.match.date string (ISO 8601) Kickoff time (UTC).
data.match.season object | null Competition season: name, round, max_rounds, start and end dates.
data.match.stadium object | null Venue name, city, country, capacity — plus a weather object (temperature °C, description, humidity %, wind) when conditions are known.
data.match.stats object In-play stats at pick time. Every entry is a [home, away] pair — full key list below.
data.match.odds object Live market quotes at pick time, keyed by market id — reference below.
Stat keys in data.match.stats

Each key maps to a [home, away] pair. An absent key simply means the stat isn't covered for that match — coverage depends on league tier and data availability, and lower-tier competitions naturally carry reduced depth. There is no predefined coverage guarantee per league.

Core — expected on covered matches

goals attacks dangerous_attacks corners on_target off_target yellowcards redcards

Optional — league dependent

xg momentum possession action_areas shots_blocked saves fouls penalties offsides freekicks injuries substitutions crosses crossing_accuracy key_passes passing_accuracy

Experimental — ignore these

Keys such as boiling_point, frustration, shot_quality and turbulence are proprietary metrics under active development. They may change behaviour, be recalibrated, or disappear without notice — don't build on them yet.

Stage values in data.match.timer.stage
Stage Meaning
-1Unknown
0Upcoming
1First half
2First half, extra time
3Half time
4Second half
5Second half, extra time
6Ended
Market IDs in data.match.odds

Keys are market IDs; some carry a line suffix — in 3_2.50 the part before _ is the base market id and the part after is the line value. Quote entries carry outcome-specific keys (odd_home / odd_draw / odd_away, odd_over / odd_under, odd_yes / odd_no) plus a suspended boolean. Entries may also include metadata such as handicap or the score at quote time (ss) — ignore keys you don't use. On over/under families, curr_line: true marks the line tracking the current match total. Only markets quoted at that moment are present.

Market id Market Lines observed Entry keys
11X2 — full-time resultodd_home, odd_draw, odd_away
21X2 at half timeodd_home, odd_draw, odd_away
3_{line}Match goals over/under0.50 – 8.50odd_over, odd_under
4_{line}1st-half goals over/under0.50 – 5.50odd_over, odd_under
7_{line}Asian corners over/under — full match0.50 – 25.00, 0.5 stepsodd_over, odd_under
8_{line}Asian corners over/under — 1st half0.50 – 15.00, 0.5 stepsodd_over, odd_under
9Both teams to score — full matchodd_yes, odd_no
10Both teams to score — 1st halfodd_yes, odd_no
11Both teams to score — 2nd halfodd_yes, odd_no
13Total goals odd/evenodd_odd, odd_even
14DNB (2-way)odd_home, odd_away

pick.settled

Sent when the pick's outcome is determined. Same envelope and structure as pick.created, with the match object reflecting the final state and three additional result fields:

Field Type Description
data.pick.strike boolean | null true when the pick hit, false when it missed, null when there is no verdict — see below.
data.pick.outcome_id integer | null ID of the tracked outcome for the strategy (e.g. "Over 2.5 Match Goals") — null when no verdict was reached.
data.match.score_ht [int, int] The half-time score as a [home, away] pair.

When there's no verdict

A strategy doesn't have to track an outcome — plenty exist purely to watch for a situation and alert. Picks from those strategies still receive pick.settled: settlement means the match finished and here is its final state, not a verdict exists. In that payload both result fields are null"outcome_id": null, "strike": null — while everything else (final score, score_ht, full-time stats and quotes) is present as usual, so you can run whatever evaluation matters to you against the final numbers.

Parser rule: strike is three-state. Truthiness collapses it — if (strike) treats a miss and a no-verdict identically — so compare explicitly against true, false and null, and keep no-verdict picks out of strike-rate maths on your side (they're excluded from ours). Same family of trap as the favorite field — see Parsing & precision.

The example below is abridged for the docs — real pick.settled payloads are full-size. They carry the complete strategy and match objects — teams, league, season, stadium, the full stats map and all market quotes — exactly like the pick.created example above, just with stats and quotes at their full-time values plus the three result fields. Size your parser and storage for a payload as large as pick.created, not for the shortened shape shown here.
pick.settled — abridged illustration, NOT the full payload JSON
{
    "event_id": "c4a91d7f-2e60-48b3-8f1a-6d20e5b79c44",
    "event_created_at": "2026-02-14T21:39:12+00:00",
    "schema_version": "1",
    "type": "pick.settled",
    "data": {
        "pick": {
            "id": 738656899,
            "created_at": "2026-02-14T21:17:29+00:00",
            "outcome_id": 21,
            "strike": true
        },
        "strategy": { "id": 960718, "name": "⚡Late Goals from Building Pressure" },
        "match": {
            "id": "RGFQZVdJbS82VG9mMFlEaDU2SmJFQT09",
            "score": [2, 1],
            "score_ht": [1, 0],
            "stats": { "goals": [2, 1], "corners": [4, 1] }
        }
    }
}

Triggers & the source of truth

  • Webhook events come from the same strategy engine that powers Telegram alerts — the two channels always agree on what fired and when.
  • Your live strategy configuration governs output: edit, enable, disable or duplicate a strategy and webhook events follow the new configuration from that moment on.
  • The payload reflects our live feed at the moment of trigger. Match data originates from third-party providers and isn't guaranteed error-free; events are not retroactively corrected if upstream data changes after delivery.
  • The payload structure is the same for every strategy — per-strategy custom payload shapes aren't offered. Filter and reshape on your side using the strategy fields in data.strategy.

Verifying signatures #

Every delivery carries X-InPlayGuru-Signature: the hex-encoded HMAC-SHA256 of the raw request body, keyed with the signing secret from your settings page. Recompute it and compare with a constant-time comparison — if they match, the payload is authentic and untampered.

Verify the raw bytes. Compute the HMAC over the body exactly as received, before any JSON parsing. Parsing and re-serialising can reorder keys or change whitespace, which changes the bytes — and the signature with them.
verify.js — Express Node.js
const crypto = require('crypto');

function isAuthentic(rawBody, signature, secret) {
    const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
    const a = Buffer.from(expected);
    const b = Buffer.from(signature || '');
    return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/inplayguru/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.get('X-InPlayGuru-Signature');
    if (!isAuthentic(req.body, signature, process.env.INPLAYGURU_WEBHOOK_SECRET)) {
        return res.sendStatus(401);
    }
    res.sendStatus(200);
    const event = JSON.parse(req.body);
    // handle event...
});
webhook.php PHP
<?php

$secret    = getenv('INPLAYGURU_WEBHOOK_SECRET');
$rawBody   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_INPLAYGURU_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $rawBody, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

http_response_code(200);           // acknowledge first
$event = json_decode($rawBody, true);
// handle $event...
app.py — Flask Python
import hashlib, hmac, os
from flask import Flask, request

app = Flask(__name__)
SECRET = os.environ["INPLAYGURU_WEBHOOK_SECRET"].encode()

@app.post("/inplayguru/webhook")
def webhook():
    raw = request.get_data()          # raw bytes, before any parsing
    signature = request.headers.get("X-InPlayGuru-Signature", "")
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature):
        return "", 401
    event = request.get_json()
    # handle event...
    return "", 200

Managing your secret

  • Your secret lives on the settings page. Store it like a password — an environment variable or secret store, never in your repository. If it ever leaks, rotate it immediately.
  • Rotating the secret takes effect immediately. Each delivery attempt is signed at send time, so after a rotation even retries of in-flight events are signed with the new secret — update your endpoint first, then rotate.
  • Signature verification is recommended but optional — deliveries are sent whether or not you verify, and skipping it never affects functionality. The X-InPlayGuru-Timestamp header tells you when each attempt was sent; as an extra layer you may choose to reject requests whose timestamp is older than a few minutes (say, five) to reduce the chance of replays.

Idempotency & ordering #

Deliveries can duplicate — deduplicate on event_id

If your endpoint processes an event but responds slowly or with an error, we retry — and you receive the same event again. Because event_id is stable across retries, exactly-once processing is yours for the price of a seen-ids check:

  • Record each processed event_id; skip events you've seen. A day of retention is plenty — retries span seconds, not days.
  • As a belt-and-braces key, (type, data.pick.id) is also unique per logical event.

Arrival order is not guaranteed

Events are dispatched in order, but retries and network timing mean they can arrive out of order. Order by event_created_at when sequence matters. A pick.settled always refers to an earlier pick.created with the same data.pick.id — hours may pass between the two, since settlement follows the final whistle.

Build your state machine defensively: a rare match interruption (abandonment, postponement) can mean a pick never settles. Expire picks you're tracking after a sensible window instead of waiting indefinitely for pick.settled.

Not guaranteed #

Integrations get burned by welding themselves to accidents of the current implementation — never build on:

  • Source IP addresses. Deliveries can originate from any address and there is no stable range to allowlist. Authenticate requests by their signature, never by source IP.
  • JSON formatting. Key order, whitespace and number formatting can change within what the JSON spec allows. The signature covers the exact bytes as sent — verify first, then parse; never re-serialise and expect equal bytes.
  • Header name casing. HTTP/2 lower-cases header names. Read all headers case-insensitively.
  • TLS details. Certificates, chains and cipher suites rotate. Don't pin fingerprints.
  • Exact timing. "Within seconds" is typical behaviour, not a deadline we promise. Don't build logic that breaks when a delivery takes a minute.
  • Volume patterns. Event volume follows your strategies and live data coverage; no baseline or ceiling of "normal" volume is promised.

If a behaviour matters to your integration and it isn't documented here, treat it as undefined — and tell us. If it's something we can commit to, it gets documented; if not, you'll know not to lean on it.

Limits & fair use #

Webhooks are push-only — you never need to poll, and normal strategy volumes are nowhere near any limit. The boundaries below exist to keep delivery reliable for everyone:

  • Sustained rate: up to 200 deliveries per minute per account. Beyond that, events may be queued, delayed or temporarily throttled rather than dropped.
  • Spikes queue briefly. When many matches trigger strategies at once, events can be queued for a short moment to protect platform stability — they still arrive, in dispatch order.
  • Persistently failing endpoints may be paused. Constant timeouts or 5xx responses waste retries on both sides; delivery may be paused with notice until the endpoint is fixed.
  • Don't work around the limits. Rotating endpoints to bypass caps or deliberately forcing retries can lead to suspension of webhook access.

Delivery is best-effort, without a formal SLA: brief maintenance windows can pause delivery momentarily, and a disruption at our upstream data providers can degrade or pause the event stream. In exceptional cases delivery may be throttled or temporarily disabled to protect platform stability, with notice where feasible.

Storing the data #

You're free to store the full JSON payloads. Analytics, internal modelling, automation, strategy development, historical research: that's exactly what the integration is for.

Intended use
  • Storing full payloads, for as long as you like
  • Models, signals and internal analytics built on your stored events
  • Automation, dashboards and execution engines that consume the data
  • Historical research and strategy development
Outside the scope
  • Reselling or redistributing the raw feed
  • Streaming or exposing payloads — or a substantially similar derivative feed — for others to consume as a data service
  • Repackaging the data as a standalone, competing product

If your use case evolves toward something that might overlap commercially with the data or the InPlayGuru platform itself, talk to us first — alignment over enforcement, always.

Acting on events #

A pick is one thing only: your strategy's conditions evaluating true against the live feed at one moment in time. It is not advice, not a prediction, and not an instruction to do anything. What your systems do with that signal — and every consequence of it — is yours.

If events trigger automated actions anywhere downstream, put your own guardrails between the event and the action:

  • Sanity-check the payload before acting — plausible score and timer, quote values inside ranges you'd accept, match not already hours old (see Working with the data for how live data misbehaves).
  • Cap action rates so a burst of picks — or a duplicate delivery your dedupe missed — can't multiply into a burst of actions.
  • Keep a kill switch you can flip without deploying, and start any new automation in observe-only mode until you've watched it behave across real match days.

The platform's role ends at delivering the signed event and recording the attempt. Everything after your 2xx — including acting on a payload that was delayed, duplicated, or reflected a feed error — runs at your own risk and judgement.

No-code tools #

You don't have to run a server to use webhooks. If you'd rather not write code, any automation platform with an inbound-webhook trigger can receive InPlayGuru events as-is — Zapier ("Webhooks by Zapier"), Make, n8n, Pipedream and Google Apps Script all work:

  1. Create an inbound webhook (often called a "catch hook") in your platform of choice and copy the URL it gives you.
  2. Paste that URL into the webhook settings page.
  3. Branch on the type field and map the payload fields you care about — a pick log in a spreadsheet, a Discord or Slack message per pick, a notification pipeline with your own filters.

Signature verification is available on platforms that allow a code step; on those that don't, treating the URL itself as a secret is the usual trade-off. Either way, this is simply an option — everything on this page works the same whether the receiver is your own code or a no-code tool.

Troubleshooting #

The delivery log labels every attempt with one of four outcomes:

Outcome Meaning Usual fix
Delivered Your endpoint answered 2xx in time. Nothing to do.
Rejected Your endpoint answered with a non-2xx status. Redirects land here too — they are not followed. Check your endpoint's logs for the status shown; remove URL redirects; make sure unknown event types return 200 rather than an error.
Timed out No response within 5 seconds. Respond first, process after — move slow work out of the request cycle (see Quick start).
Unreachable The connection never succeeded — DNS, refused connection, or TLS handshake failure. Verify the hostname resolves publicly, the port is open to the internet, and the TLS certificate chain is complete.

Not receiving events?

  • Confirm an endpoint URL is saved and shows Live on the settings page.
  • Webhook events fire when your strategies fire — a quiet day for your strategies is a quiet day for your endpoint. Cross-check your strategy history.
  • Check the delivery log: attempts recorded there mean we're sending and the issue is on the receiving path; an empty log means no events have fired since you set up.
  • Signature mismatches? Make sure you verify the raw body and the current secret — see Verifying signatures.

Support scope

The delivery log answers most "did it send?" questions on its own — check it first. When you do write in, include the event_id from the log, and expect this scope:

Support covers
  • Confirming your webhook is enabled and correctly configured
  • Checking delivery attempts and their status in our log
  • Providing the current schema, headers and example payloads
  • Investigating platform-side incidents and outages
Outside scope
  • Debugging your endpoint code, hosting, TLS or networking
  • Debugging downstream systems — bots, databases, pipelines, third-party tools
  • Writing or reviewing your integration code
  • Reconstructing events missed while your endpoint was down
  • Guarantees about strategy results or about external systems acting on events

A delivery marked Delivered in the log means our side worked — from there, the trail continues in your systems. Requests outside the scope above are answered with a link back to this page.

Stuck on something within scope? Contact support with the event id — it identifies the exact delivery on our side.