Generate a bet slip
Available for: Free API · Concierge API · Enterprise API. Results remain within your API tier's league and sportsbook coverage. Free API content partner requests are limited to included books and carry partner attribution.
Turn a text description or image into matched sportsbook bet slips. Generation is asynchronous: submit content, keep the returned request ID, and poll for the result. A successful submission does not mean every requested leg has matched.
1. Submit content
POST /api/v1/bet/generate
For a text request, send type: "text" and content.text. The example prompt describes the kind of bet to find; its results depend on current availability.
Set UNABATED_API_BASE_URL explicitly to the API base URL, https://data.unabated.com. The examples stop if it is missing.
curl --fail-with-body --connect-timeout 5 --max-time 30 \
--request POST \
--header "X-Api-Key: ${UNABATED_API_KEY}" \
--header "Content-Type: application/json" \
--data '{"type":"text","generateMobileLinks":true,"content":{"text":"NFL three-game moneyline parlay"}}' \
"${UNABATED_API_BASE_URL:?Set UNABATED_API_BASE_URL to https://data.unabated.com}/api/v1/bet/generate"
The response is a direct object, not the data/success/messages envelope used by odds endpoints. A representative accepted response looks like:
{
"status": "pending",
"details": "text: NFL three-game moneyline parlay",
"requestId": "00000000-0000-4000-8000-000000000001"
}
The illustrated ID is a placeholder. Use your response's requestId. If the returned status indicates an error, stop and read details; do not poll the example ID or assume an HTTP 200 means generation succeeded.
Image input
Use type: "image" with one image representation:
{
"type": "image",
"generateMobileLinks": true,
"content": { "imageUrl": "https://your-site.example/bet-slip.png" }
}
Alternatively, send content.base64String containing the image data. A compression field is available for base64 input; follow the request schema for supported usage. Only submit an image you are authorized to process. generateMobileLinks defaults to true; set it to false when requesting desktop-oriented links.
2. Poll the request status
GET /api/v1/bet/status/{guid}
curl --fail-with-body --connect-timeout 5 --max-time 20 \
--header "X-Api-Key: ${UNABATED_API_KEY}" \
"${UNABATED_API_BASE_URL:?Set UNABATED_API_BASE_URL to https://data.unabated.com}/api/v1/bet/status/${UNABATED_REQUEST_ID}"
| Status | What to do |
|---|---|
Processing |
Wait briefly and poll again, within your application's deadline |
Complete |
Read betSlips and inspect matched selections |
Error |
Stop, read details/message, and report the failure |
Status responses also use a direct object. A request remains associated with its owner; use the same account's key when polling.
Complete lifecycle example
These server-side examples use a two-second poll interval as a client recommendation, not an endpoint limit. They stop after a bounded wait and do not automatically resubmit a generation request after a timeout, which could create duplicate work.
Node.js 20 or later; save as bet-slip.mjs and run on your server with UNABATED_API_KEY set.
const key = process.env.UNABATED_API_KEY;
if (!key) throw new Error('Set UNABATED_API_KEY');
const base = process.env.UNABATED_API_BASE_URL;
if (!base) throw new Error('Set UNABATED_API_BASE_URL to https://data.unabated.com');
const headers = { 'X-Api-Key': key, 'Content-Type': 'application/json' };
async function request(path, options = {}) {
const response = await fetch(`${base}${path}`, {
...options, headers, signal: AbortSignal.timeout(20_000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
return response.json();
}
const submitted = await request('/api/v1/bet/generate', {
method: 'POST',
body: JSON.stringify({
type: 'text', generateMobileLinks: true,
content: { text: 'NFL three-game moneyline parlay' },
}),
});
if (submitted.status?.toLowerCase() === 'error' || !submitted.requestId) {
throw new Error(submitted.details ?? 'Generation was not accepted');
}
const deadline = Date.now() + 60_000;
let completed;
while (Date.now() < deadline) {
const result = await request(`/api/v1/bet/status/${submitted.requestId}`);
const status = result.status?.toLowerCase();
if (status === 'complete') { completed = result; break; }
if (status === 'error') throw new Error(result.message ?? result.details ?? 'Generation failed');
if (status !== 'processing') throw new Error(`Unexpected status: ${result.status}`);
await new Promise(resolve => setTimeout(resolve, 2_000));
}
if (!completed) throw new Error(`Still processing; retain requestId ${submitted.requestId} to resume later`);
for (const slip of completed.betSlips ?? []) {
console.log({
book: slip.book, odds: slip.overallOdds,
matched: slip.legsMatched, requested: slip.legsRequested,
deepLink: slip.deepLink,
});
}
Python 3.10 or later; install requests with python -m pip install requests.
import os
import time
import requests
base = os.environ['UNABATED_API_BASE_URL']
session = requests.Session()
session.headers.update({'X-Api-Key': os.environ['UNABATED_API_KEY']})
def request(method, path, payload=None):
response = session.request(method, f'{base}{path}', json=payload, timeout=(5, 20))
response.raise_for_status()
return response.json()
submitted = request('POST', '/api/v1/bet/generate', {
'type': 'text', 'generateMobileLinks': True,
'content': {'text': 'NFL three-game moneyline parlay'},
})
if submitted.get('status', '').lower() == 'error' or not submitted.get('requestId'):
raise RuntimeError(submitted.get('details', 'Generation was not accepted'))
deadline = time.monotonic() + 60
completed = None
while time.monotonic() < deadline:
result = request('GET', f"/api/v1/bet/status/{submitted['requestId']}")
status = result.get('status', '').lower()
if status == 'complete':
completed = result
break
if status == 'error':
raise RuntimeError(result.get('message') or result.get('details') or 'Generation failed')
if status != 'processing':
raise RuntimeError(f"Unexpected status: {result.get('status')}")
time.sleep(2)
if completed is None:
raise TimeoutError(f"Still processing; retain requestId {submitted['requestId']} to resume later")
for slip in completed.get('betSlips') or []:
print(slip['book'], slip.get('overallOdds'),
slip.get('legsMatched'), slip.get('legsRequested'), slip.get('deepLink'))
session.close()
3. Inspect the matched result
Each item in betSlips describes one sportsbook:
book,overallOdds, anddeepLinkdescribe the full slip.legsRequestedandlegsMatchedtell you how much of the request was matched.betslipBetscontains the individual selections, including event/player descriptions, bet type, period,odds, and individualdeepLink.urlvalues.sharedBetSlipIdandshareUrlon the status response may provide a shareable slip when one was created.
Show matched-leg counts and actual returned selections to your user. Do not present a partially matched slip as if every requested leg was found. Odds and availability can change before the sportsbook opens, and generation does not place a bet.
Free requests carry your partner attribution into returned bet-slip links. Publish those links as returned; the manual suffix workflow for generic odds links is explained separately in tracked deep links.