18+ Only  |  Gambling can be addictive — please gamble responsibly  |  Gambling Help: 1800 858 858  |  GambleAware

Developers / Errors

Errors

Every failure is returned as RFC 7807 application/problem+json, with a stable type URI you can branch on.

The shape

Errors never come back as bare strings or HTML. The body is always the same four fields:

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 60
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0

{
  "type":   "https://puntersedge.online/errors/rate-limit",
  "title":  "Rate limit exceeded",
  "status": 429,
  "detail": {
    "error": "rate_limit_exceeded",
    "message": "Rate limit exceeded: 30 requests/minute on free plan.",
    "upgrade_url": "https://puntersedge.online/api",
    "retry_after_seconds": 60
  }
}

Branch on type or status, not on title. The type URI is stable; wording is not.

One inconsistency worth coding around: detail is a string for most errors, an array of field errors for 422, and an object for 429. Type-check it before you render it, or a validation error will crash your error handler.

Status codes

Code What happened What to do
401
Unauthorized
The X-API-Key header is missing, or the key is not valid. Send your key as X-API-Key. Sandbox endpoints under /v1/demo/ need no key at all.
402
Payment Required
The monthly credit allowance for your plan is exhausted. Wait for the monthly reset or upgrade. This is about credits, not request rate — a 402 will not clear by retrying.
403
Forbidden
The key is valid but not permitted to use this endpoint. Four causes: historical odds or movements (403 on Free and Hobby); webhook management (403 on Free, Hobby and Standard); the closing-line archive, /v1/racing/closing-lines and /v1/racing/closing-lines/coverage — a flat 403 on Free and Hobby, a 403 on Standard when the date range you asked for lies entirely outside their rolling 90-day window; a request from an IP outside the key's whitelist. (/v1/data-quality/audit/* is admin-only and 403s everything else; /v1/data-quality/summary is open to every plan.) There is no racing-vs-sports gate: every paid plan reaches both racing and sports. Check your plan's endpoint set and any IP whitelist on the key. The archive endpoints say in the 403 body which window your plan can read.
404
Not Found
No such route, or the requested resource does not exist. Enumerate endpoints from /openapi.json rather than guessing paths.
405
Method Not Allowed
The path exists but does not accept this HTTP method — a GET to /v1/keys/rotate or /v1/webhooks/{id}, or a POST to an odds endpoint. Look the path up in /openapi.json and reissue with the method it lists. Odds reads are GET; account and webhook management use POST or DELETE.
422
Validation Error
A parameter is missing, malformed or out of range. detail is an array of field errors for this code — unlike other errors, where it is a string. Read each entry's loc to find the offending parameter.
429
Too Many Requests
You exceeded your plan's requests-per-minute limit. Honour the Retry-After header. See rate limits.
500
Internal Server Error
An unexpected failure on our side. Safe to retry with exponential backoff. Persisting? Tell us and check status.
503
Service Unavailable
The datastore behind the API is briefly unavailable — a transient outage or restart on our side, never a problem with your request. Honour the Retry-After header (a few seconds) and retry — unlike a 500, a 503 is explicitly retryable. Persisting? Check status.

402 vs 429 — they are not the same problem

These are the two that get conflated, and retry logic that treats them alike will spin forever:

Handling them

import time, requests

def call(url, key, attempts=4):
    for i in range(attempts):
        r = requests.get(url, headers={"X-API-Key": key}, timeout=30)
        if r.status_code == 429:                     # too fast — wait it out
            time.sleep(int(r.headers.get("Retry-After", 60)))
            continue
        if r.status_code == 402:                     # out of credits — retrying cannot help
            raise RuntimeError(r.json()["detail"])
        if r.status_code >= 500:                     # our fault — back off
            time.sleep(2 ** i)
            continue
        r.raise_for_status()
        return r.json()
    raise RuntimeError("exhausted retries")

The official Python SDK already does this, raising typed exceptions (AuthenticationError, RateLimitError, NotFoundError, ServerError) with automatic retry on 5xx.

This site contains wagering-related analysis and is intended for Australian users aged 18+. Gambling involves risk. Please gamble responsibly.