# GET /v1/arbitrage

> Independent. Not affiliated with, endorsed by or sponsored by Novig.

Canonical: https://novigapi.com/docs/endpoints/arbitrage
Facts checked: 2026-11-12

GET /v1/arbitrage returns two-way arbitrage across Novig, FanDuel and DK Sportsbook: both legs, the stake split for 100 units and the guaranteed margin, recomputed every 60 seconds. Novig legs are priced at the ask plus any taker fee due at that moment. type=middles returns lines that leave a window where both legs win. Production and Scale, checked 12 Nov 2026. (Source: [Novig API docs: Trading fees](https://docs.novig.com/api/concepts/fees.md))

## What it returns

An arbitrage exists when the best price for each side, on different venues, implies less than 100% together. Stakes are split so both outcomes return the same amount.

```
margin = 1 - (1 / decimal_a + 1 / decimal_b); stake_a = 100 x (1 / decimal_a) / (1 / decimal_a + 1 / decimal_b)
```
Novig legs use the ask plus 0.06 x P x (1 - P) when the fee is charged (live game markets, some futures).

Middles (type=middles) pair Over a low line with Under a higher one, or two spreads that leave a gap, across venues. They are not risk-free: the row says how wide the window is and which whole numbers land in it.

## Parameters

Query and path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| type | string | No | `arbitrage` (default) or `middles`. |
| league | string | No | League code. |
| market_type | string | No | `moneyline`, `spread`, `total` or `player_prop`. |
| min_profit | number | No | Smallest guaranteed margin, percent. |
| limit | integer | No | Rows per page, 1 to 500. Default 50. Free returns at most 100. |
| cursor | string | No | `meta.next_cursor` from the previous page. |


## Request

```bash
curl -s "https://novigapi.com/api/v1/arbitrage?league=NBA" \
  -H "Authorization: Bearer nv_..."
```

## Response

An illustrative row in the exact shape the API returns. Real two-way arbitrage between these venues is rare and short-lived; most passes find none, and a row with any stale leg is marked low confidence.

```json
{
  "data": [
    {
      "id": "01a1ce81-31b6-7933-ae0e-1e6dcfbc5a0b|total||-|221.5|nv:over|fd:under",
      "league": "NBA",
      "event_id": "01a1ce81-31b6-7933-ae0e-1e6dcfbc5a0b",
      "starts_at": "2026-11-14T00:05:00.000Z",
      "home": "Indiana Pacers",
      "away": "New York Knicks",
      "market_type": "total",
      "stat": null,
      "player": null,
      "line": 221.5,
      "legs": [
        {
          "side": "over",
          "venue": "nv",
          "american": 104,
          "decimal": 2.0408,
          "stake": 50.11,
          "all_in_probability": 0.49,
          "market_id": "01a1d1f0-bc46-77c0-9a2f-c34d0ce7ae2c",
          "book": "nv",
          "book_name": "Novig",
          "american_text": "+104"
        },
        {
          "side": "under",
          "venue": "fd",
          "american": 105,
          "decimal": 2.05,
          "stake": 49.89,
          "all_in_probability": 0.4878,
          "market_id": "734.197440601:7733046",
          "book": "fd",
          "book_name": "FanDuel",
          "american_text": "+105"
        }
      ],
      "implied_sum": 0.9778,
      "profit_pct": 2.2699,
      "stake_basis": 100,
      "oldest_age_seconds": 40,
      "confidence": "high",
      "flags": [],
      "first_seen": "2026-11-13T02:21:00.000Z",
      "observed_at": "2026-11-13T02:57:00.000Z"
    }
  ],
  "meta": {
    "observed_at": "2026-11-13T02:57:00.000Z",
    "source_age_seconds": 0,
    "edge_cached": false,
    "next_cursor": null,
    "limit": 50,
    "count": 1,
    "type": "arbitrage",
    "refresh_seconds": 60,
    "plan": "developer",
    "credits_charged": 3,
    "credits_remaining": 9874,
    "credit_policy": "credits_v1"
  }
}
```

## Fields

Response fields

| Field | Type | Description |
| --- | --- | --- |
| legs[] | object[] | Each leg: side, book, book_name, american, american_text, decimal, stake (of 100), all_in_probability (price after any fee) and market_id. |
| implied_sum | number | 1 / decimal_a + 1 / decimal_b. Under 1 is an arbitrage. |
| profit_pct | number | Guaranteed return on the total stake, percent. |
| oldest_age_seconds, confidence, flags | integer, string, string[] | Age of the older leg; `stale_price` over 360 s, `thin_exchange_depth` when the Novig leg cannot absorb much. |
| window, landing_integers (middles) | number, integer[] | Width between the two lines and the whole-number results that win both legs. |
| meta.next_cursor | string or null | Pass as `cursor` to get the next page. Null on the last page. |
| meta.limit, meta.count | integer | The page size used and the rows on this page. |
| meta.observed_at | string (ISO 8601) | When our collector read the oldest row in this answer from Novig's public catalog. Each row also carries its own `observed_at`. |
| meta.source_age_seconds | integer | Seconds between that read and our response. On paid plans this stays at about 10 or less. |
| meta.edge_cached | boolean | True when Novig's edge cache served any of the reads, so the source copy may itself be a few seconds old. |
| meta.plan, credits_charged, credits_remaining, credit_policy | string, integer | Your plan, what this call cost, the credits left this month and the credit rules version. |


## Credit cost

Weighted 3x: 3 credits per call per 100 rows requested (the `limit` you send, default 50, maximum 500; Free pages hold at most 100 rows). A default page costs 3 credits. Plans without arbitrage get 403 UPGRADE_REQUIRED, which costs nothing. Errors and 304 Not Modified cost 0; the `X-Credits-Cost` header and `meta.credits_charged` state the price of every call.

## Python

```python
import os
import requests

KEY = os.environ["NOVIGAPI_API_KEY"]  # nv_...
HEADERS = {"Authorization": f"Bearer {KEY}"}

r = requests.get("https://novigapi.com/api/v1/arbitrage", params={"min_profit": 0.5}, headers=HEADERS, timeout=10)
for a in r.json()["data"]:
    print(a["profit_pct"], [(l["book_name"], l["side"], l["american_text"], l["stake"]) for l in a["legs"]])
```

## JavaScript

```javascript
const KEY = process.env.NOVIGAPI_API_KEY; // nv_...
const headers = { Authorization: "Bearer " + KEY };

const res = await fetch("https://novigapi.com/api/v1/arbitrage?type=middles&league=NFL", { headers });
console.log((await res.json()).data);
```

## Notes and edge cases

- Prices move. Place the leg on the slower venue first, and confirm both before you commit.
- A Novig leg fills only up to the depth on the other side's best bid; check the [order book](https://novigapi.com/docs/endpoints/orderbook-snapshot) for size.
- Use the [arbitrage calculator](https://novigapi.com/arbitrage-calculator) and the [middle calculator](https://novigapi.com/middle-calculator) to rework a split by hand.

## Sources

- [Novig API docs: Trading fees](https://docs.novig.com/api/concepts/fees.md)
- [Novig API docs](https://docs.novig.com)

---

Get a free API key: https://novigapi.com/sign-up . Docs: https://novigapi.com/docs . MCP: https://novigapi.com/api/mcp . Operated by Stunning Couches LLC. Independent. Not affiliated with, endorsed by or sponsored by Novig.
