> ## Documentation Index
> Fetch the complete documentation index at: https://docs.splashsports.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits

> Requests are limited to 10 per second per IP address. What a 429 looks like, how to retry, and how to stay under the limit.

Requests are limited to **10 per second per IP address**. Every request from the same IP address counts, whichever key
it uses, so all of the bots and processes that send from one IP address share the limit; bots on different IP
addresses do not.

Over the limit, you'll receive HTTP `429 Too Many Requests`. If the response has a `Retry-After` header, wait that
many seconds; otherwise back off (for example 1 second, with jitter) and retry. The body is:

```json theme={null}
{
  "status": "error",
  "error": "TOO_MANY_REQUESTS",
  "message": "Too many requests, try again later"
}
```

Branch on the `429` status. `TOO_MANY_REQUESTS` is not one of the [error codes](https://docs.splashsports.com/predictions/trading.md#errors) the order and position routes
return.

## Handling a `429`

* **A `429` records nothing, so retry the same request.** For `POST /trading/orders`, keep the same `request_id`: the order
  was never accepted, so it cannot be placed twice.
* **Wait before retrying.** Honour `Retry-After` when it is present; otherwise wait about a second.
* **Back off if `429`s repeat.** Wait a little longer on each consecutive `429` and add a small random delay (jitter),
  so several processes that were refused together do not all retry in the same instant.
* **Never retry instantly in a loop.** An immediate retry is refused again.
* **A `429` is not a timeout.** It is an answer, and it means the order was not accepted. No answer at all is
  different: check the order by `request_id` before resending, as in
  [Order outcomes and retries](https://docs.splashsports.com/predictions/trading.md#order-outcomes-and-retries).

## Staying under the limit

* **Throttle on your side, once per IP address.** Put every request through one client-side limiter shared by every
  process and thread that sends from the same IP address, such as a token bucket refilling at 10 per second or less. A
  limiter per process lets two processes on the same IP address spend 20 per second between them.
* **Avoid bursts.** Firing many requests at once (placing a batch of orders, or polling every open order together)
  can exceed the limit even when your average is low. Spread them out.
* **Poll at a sensible cadence.** Poll a new order every 500 ms to 1 second for the first few seconds, then every few
  seconds while it is still `pending`. Never poll in a tight loop.
* **More keys do not raise the limit.** It is per IP address, so one key per bot is fine.

## A request helper that retries on `429`

Each example sends a signed request and, on a `429`, waits the `Retry-After` seconds when the header is present and 1
second otherwise, longer on each repeat, plus up to 250 ms of jitter. Then it signs and sends the same request again,
up to five attempts, and returns the last response. Every attempt is signed afresh, because the timestamp must be
within 30 seconds of Splash's clock. A failure with no response (a timeout or a dropped connection) is raised rather
than retried: check the order by `request_id` first.

The cURL, Python and TypeScript tabs reuse the variables and the `sign()` / `request()` helpers from
[the signed-order example](https://docs.splashsports.com/predictions/authentication.md#example-place-a-signed-order); the Go tab is a complete program.

```bash cURL theme={null}
# Reuses KEY, SECRET, METHOD, URLPATH and BODY from the signed-order example.
for ATTEMPT in 1 2 3 4 5; do
  TS=$(( $(date +%s) * 1000 ))
  SIG=$(printf '%s' "$TS$METHOD$URLPATH$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
  STATUS=$(curl -s -o response.json -D headers.txt -w '%{http_code}' -X "$METHOD" "https://api.splashsports.com$URLPATH" \
    -H "Content-Type: application/json" \
    -H "SPLASH-ACCESS-KEY: $KEY" \
    -H "SPLASH-ACCESS-TIMESTAMP: $TS" \
    -H "SPLASH-ACCESS-SIGNATURE: $SIG" \
    --data-binary "$BODY")
  if [ "$STATUS" != "429" ] || [ "$ATTEMPT" = 5 ]; then break; fi
  RETRY_AFTER=$(grep -i '^retry-after:' headers.txt | tr -d '\r' | awk '{print $2}')
  case "$RETRY_AFTER" in ''|*[!0-9]*) RETRY_AFTER=1 ;; esac  # absent or not a number of seconds: 1 second
  sleep "$(( RETRY_AFTER * ATTEMPT )).$(printf '%03d' $(( RANDOM % 250 )))"
done
echo "$STATUS"; cat response.json
```

```python Python theme={null}
# Reuses request() (and sign()) from the signed-order example.
import random
import time


def retry_after_seconds(response):
    # Retry-After when it is a number of seconds; otherwise back off 1 second.
    try:
        return max(float(response.headers["Retry-After"]), 0)
    except (KeyError, ValueError):
        return 1.0


def request_with_retry(method, path, payload=None, max_attempts=5):
    for attempt in range(1, max_attempts + 1):
        response = request(method, path, payload)  # signs again with a fresh timestamp
        if response.status_code != 429 or attempt == max_attempts:
            return response
        time.sleep(retry_after_seconds(response) * attempt + random.uniform(0, 0.25))


response = request_with_retry(
    "POST",
    "/predictions-service/api/trading/orders",
    {
        "market_id": "pm_6ae1e3c09e1d4c7a2b5f80e3d6a9c412",
        "side": "yes",
        "quantity": 10,
        "price": 0.59,
        "time_in_force": "FOK",
        "request_id": "3f1c2a9e-5b7d-4e0a-9c1d-8a6b2e4f7d10",
    },
)
print(response.status_code, response.json())
```

```typescript TypeScript theme={null}
// Reuses KEY, SECRET, HOST and sign() from the signed-order example.
async function requestWithRetry(method: string, path: string, body = "", maxAttempts = 5): Promise<Response> {
  for (let attempt = 1; ; attempt++) {
    const timestamp = Date.now().toString(); // signed afresh on every attempt
    const response = await fetch(HOST + path, {
      method,
      headers: {
        ...(body ? { "Content-Type": "application/json" } : {}),
        "SPLASH-ACCESS-KEY": KEY,
        "SPLASH-ACCESS-TIMESTAMP": timestamp,
        "SPLASH-ACCESS-SIGNATURE": sign(SECRET, timestamp, method, path, body),
      },
      body: body || undefined,
    });
    if (response.status !== 429 || attempt >= maxAttempts) {
      return response;
    }
    await response.body?.cancel();
    // Retry-After when it is a number of seconds; otherwise back off 1 second.
    const header = Number(response.headers.get("Retry-After") ?? "");
    const retryAfter = response.headers.has("Retry-After") && Number.isFinite(header) && header >= 0 ? header : 1;
    await new Promise((resolve) => setTimeout(resolve, (retryAfter * attempt + Math.random() * 0.25) * 1000));
  }
}

const orderBody = JSON.stringify({
  market_id: "pm_6ae1e3c09e1d4c7a2b5f80e3d6a9c412",
  side: "yes",
  quantity: 10,
  price: 0.59,
  time_in_force: "FOK",
  request_id: "3f1c2a9e-5b7d-4e0a-9c1d-8a6b2e4f7d10",
});
const orderResponse = await requestWithRetry("POST", "/predictions-service/api/trading/orders", orderBody);
console.log(orderResponse.status, await orderResponse.json());
```

```go Go theme={null}
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"time"
)

const (
	key    = "your-key-id"
	secret = "your-secret"
	host   = "https://api.splashsports.com"
)

func sign(secret, timestamp, method, path string, body []byte) string {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + method + path))
	mac.Write(body)
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

func requestWithRetry(method, path string, body []byte, maxAttempts int) (*http.Response, error) {
	for attempt := 1; ; attempt++ {
		req, err := http.NewRequest(method, host+path, bytes.NewReader(body))
		if err != nil {
			return nil, err
		}
		timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) // signed afresh on every attempt
		if len(body) > 0 {
			req.Header.Set("Content-Type", "application/json")
		}
		req.Header.Set("SPLASH-ACCESS-KEY", key)
		req.Header.Set("SPLASH-ACCESS-TIMESTAMP", timestamp)
		req.Header.Set("SPLASH-ACCESS-SIGNATURE", sign(secret, timestamp, method, path, body))

		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err // no response: check the order by request_id before resending
		}
		if resp.StatusCode != http.StatusTooManyRequests || attempt >= maxAttempts {
			return resp, nil
		}
		resp.Body.Close()
		// Retry-After when it is a number of seconds; otherwise back off 1 second.
		retryAfter, err := strconv.Atoi(resp.Header.Get("Retry-After"))
		if err != nil || retryAfter < 0 {
			retryAfter = 1
		}
		jitter := time.Duration(time.Now().UnixNano()%250) * time.Millisecond
		time.Sleep(time.Duration(retryAfter*attempt)*time.Second + jitter)
	}
}

func main() {
	body, err := json.Marshal(map[string]any{
		"market_id": "pm_6ae1e3c09e1d4c7a2b5f80e3d6a9c412",
		"side": "yes",
		"quantity": 10,
		"price": 0.59,
		"time_in_force": "FOK",
		"request_id": "3f1c2a9e-5b7d-4e0a-9c1d-8a6b2e4f7d10",
	})
	if err != nil {
		panic(err)
	}
	resp, err := requestWithRetry("POST", "/predictions-service/api/trading/orders", body, 5)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.