> ## 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.

# Authentication

> Get an API key and sign every order and position request with it.

Every order and position request is signed with an API key. There is no login and no token to refresh: you compute
a signature over the request and send it in three headers.

## Get a key

API keys are issued by Splash on request. Ask your Splash contact for one. You receive two values:

* the **key id**, sent in `SPLASH-ACCESS-KEY`;
* the **secret**, used to sign requests. It is shown to you once; keep it private and never send it.

Use the secret exactly as issued: its characters, as a UTF-8 string, are the HMAC key. It looks like base64, but
do not base64-decode it.

Keys never expire. A key stays valid until it is revoked, and Splash revokes a key on request: ask your Splash
contact.

## The three headers

| Header | Value |
| - | - |
| `SPLASH-ACCESS-KEY` | Your key id. |
| `SPLASH-ACCESS-TIMESTAMP` | The time of the request in Unix **milliseconds**. It must be within 30 seconds of Splash's clock. |
| `SPLASH-ACCESS-SIGNATURE` | `base64(HMAC-SHA256(secret, timestamp + METHOD + path + body))`. |

Also send `Content-Type: application/json` on any request with a body. A signed request whose body is not
JSON is rejected with `415 UNSUPPORTED_MEDIA_TYPE`.

## What you sign

The message is four strings joined with nothing between them:

| Part | What it is |
| - | - |
| `timestamp` | Exactly the value you send in `SPLASH-ACCESS-TIMESTAMP`. |
| `METHOD` | The HTTP method in upper case: `GET`, `POST`. |
| `path` | The full path **including the base path and the query string**, exactly as sent. |
| `body` | The request body, uncompressed UTF-8 JSON, byte for byte as sent. The empty string when there is none. |

The `path` begins with `/predictions-service/api`, because that is part of the URL. For example:

* `POST https://api.splashsports.com/predictions-service/api/trading/orders` signs the path
  `/predictions-service/api/trading/orders`.
* `GET .../orders?request_id=abc` signs
  `/predictions-service/api/orders?request_id=abc`.

> [!WARNING]
>
> Sign the exact bytes you send. If you build the JSON, sign it, and then let your HTTP library
> re-serialize it, the spacing or key order can change and the signature will not match. Serialize once, sign
> that string, and send that same string.

Then compute the HMAC-SHA256 of the message with your secret as the key and base64-encode the 32-byte result.
The key is the secret string exactly as issued, encoded as UTF-8; do not base64-decode it first.

## Example: place a signed order

`POST /trading/orders` with a signed request. Pick your language; the choice carries to the other examples on this page.

```bash cURL theme={null}
KEY="your-key-id"
SECRET="your-secret"
METHOD="POST"
URLPATH="/predictions-service/api/trading/orders"
BODY='{"market_id":"pm_6ae1e3c09e1d4c7a2b5f80e3d6a9c412","side":"yes","quantity":10,"price":0.59,"time_in_force":"FOK","request_id":"3f1c2a9e-5b7d-4e0a-9c1d-8a6b2e4f7d10"}'

TS=$(( $(date +%s) * 1000 ))
SIG=$(printf '%s' "$TS$METHOD$URLPATH$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)

curl -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"
```

```python Python theme={null}
import base64, hashlib, hmac, json, time
import requests

KEY = "your-key-id"
SECRET = "your-secret"
HOST = "https://api.splashsports.com"


def sign(secret, timestamp, method, path, body):
    message = (timestamp + method + path + body).encode("utf-8")
    digest = hmac.new(secret.encode("utf-8"), message, hashlib.sha256).digest()
    return base64.b64encode(digest).decode("ascii")


def request(method, path, payload=None):
    body = json.dumps(payload, separators=(",", ":")) if payload is not None else ""
    timestamp = str(int(time.time() * 1000))
    headers = {
        "Content-Type": "application/json",
        "SPLASH-ACCESS-KEY": KEY,
        "SPLASH-ACCESS-TIMESTAMP": timestamp,
        "SPLASH-ACCESS-SIGNATURE": sign(SECRET, timestamp, method, path, body),
    }
    return requests.request(method, HOST + path, headers=headers, data=body.encode("utf-8"))


response = request(
    "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}
import { createHmac } from "node:crypto";

const KEY = "your-key-id";
const SECRET = "your-secret";
const HOST = "https://api.splashsports.com";

function sign(secret: string, timestamp: string, method: string, path: string, body: string): string {
  return createHmac("sha256", secret).update(timestamp + method + path + body, "utf8").digest("base64");
}

const method = "POST";
const path = "/predictions-service/api/trading/orders";

// Serialize once, sign that exact string, and send that exact string.
const body = 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 timestamp = Date.now().toString();

const response = await fetch(HOST + path, {
  method,
  headers: {
    "Content-Type": "application/json",
    "SPLASH-ACCESS-KEY": KEY,
    "SPLASH-ACCESS-TIMESTAMP": timestamp,
    "SPLASH-ACCESS-SIGNATURE": sign(SECRET, timestamp, method, path, body),
  },
  body,
});
console.log(response.status, await response.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 main() {
	method := "POST"
	path := "/predictions-service/api/trading/orders"

	// Marshal once, sign those exact bytes, and send those exact bytes.
	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)
	}
	timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)

	req, err := http.NewRequest(method, host+path, bytes.NewReader(body))
	if err != nil {
		panic(err)
	}
	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 {
		panic(err)
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

`--data-binary` (cURL), the `body` string (TypeScript) and the marshalled bytes (Go) send exactly what was signed.

## Example: a signed GET

A request with no body signs the empty string, and carries the query string in the signed path.

```bash cURL theme={null}
KEY="your-key-id"
SECRET="your-secret"
METHOD="GET"
URLPATH="/predictions-service/api/orders?request_id=3f1c2a9e-5b7d-4e0a-9c1d-8a6b2e4f7d10"
BODY=""

TS=$(( $(date +%s) * 1000 ))
SIG=$(printf '%s' "$TS$METHOD$URLPATH$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)

curl -X "$METHOD" "https://api.splashsports.com$URLPATH" \
  -H "SPLASH-ACCESS-KEY: $KEY" \
  -H "SPLASH-ACCESS-TIMESTAMP: $TS" \
  -H "SPLASH-ACCESS-SIGNATURE: $SIG"
```

```python Python theme={null}
# Reuses sign() and request() from the example above.
response = request(
    "GET",
    "/predictions-service/api/orders?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 example above.
const getPath = "/predictions-service/api/orders?request_id=3f1c2a9e-5b7d-4e0a-9c1d-8a6b2e4f7d10";
const getTimestamp = Date.now().toString();

const getResponse = await fetch(HOST + getPath, {
  headers: {
    "SPLASH-ACCESS-KEY": KEY,
    "SPLASH-ACCESS-TIMESTAMP": getTimestamp,
    "SPLASH-ACCESS-SIGNATURE": sign(SECRET, getTimestamp, "GET", getPath, ""),
  },
});
console.log(getResponse.status, await getResponse.json());
```

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

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"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 main() {
	method := "GET"
	path := "/predictions-service/api/orders?request_id=3f1c2a9e-5b7d-4e0a-9c1d-8a6b2e4f7d10"

	var body []byte // no body: the signature covers the empty string
	timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)

	req, err := http.NewRequest(method, host+path, bytes.NewReader(body))
	if err != nil {
		panic(err)
	}
	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 {
		panic(err)
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	fmt.Println(resp.StatusCode, string(out))
}
```

## Check your signing

If you get `Invalid signature`, compare your code against this fixed input. With

* secret `s3cret`
* timestamp `1700000000000`
* method `POST`
* path `/predictions-service/api/trading/orders`
* body `{"a":1}`

the message is `1700000000000POST/predictions-service/api/trading/orders{"a":1}` and the signature must be:

```
BjgyawjCbTXmATQ9C5owCYMIIi97a2pwehzHmiaYpmE=
```

Run your `sign` function on those inputs and compare. `openssl` computes the same value:

```bash theme={null}
printf '%s' '1700000000000POST/predictions-service/api/trading/orders{"a":1}' \
  | openssl dgst -sha256 -hmac s3cret -binary | base64
```

If you get a different value, check that you used the secret as a plain string (not base64-decoded), the method in
upper case, and the path with `/predictions-service/api`.

## Authentication errors

A failed check is `401` with `error: "NOT_AUTHORIZED"` and one of these messages:

| `message` | Cause |
| - | - |
| `Unknown API key` | No key has that id. |
| `API key revoked` | Splash revoked the key. |
| `Timestamp outside the allowed window` | `SPLASH-ACCESS-TIMESTAMP` is more than 30 seconds from server time, or not a whole number of milliseconds. Check your clock. |
| `Invalid signature` | The signature does not match what Splash computed from your request. |
| `You are not authorized to perform the requested action.` | The request had no `SPLASH-ACCESS-KEY` header: it was not signed, or the header name is misspelled. A request without the `SPLASH-ACCESS-KEY` header is not an API request and is refused with this message. The same message answers a `user_id` query parameter on `GET /orders`, `GET /orders/{id}`, `GET /positions`, `GET /positions/{id}` and `GET /transactions`. |

`API key revoked` is reported only when the signature is correct, so a wrong secret
always reads `Invalid signature`.

A failed check happens before anything else, so on `POST /trading/orders` nothing is recorded: fix the signing and send the
order again with the same `request_id`.

## Rate limits

Requests are limited to 10 per second per IP address; see [Rate limits](https://docs.splashsports.com/predictions/rate-limits.md) for handling a
`429 Too Many Requests` and a retrying request helper in each language.


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