Introduction
Convert a sportsbook booking code issued by one bookmaker into a live booking code on another.
The API covers 51 bookmakers as a source and 57 as a destination, 2,866 directions in all. Keys are issued after a short agreement: request access and we will send an evaluation key with free credits.
Quick start
Call the API from your server, never from an app or a browser. Every POST /convert needs an Idempotency-Key: a new value per conversion, and the same value on every retry of it.
curl -X POST https://eval-api.convat.io/v1/convert \
-H "Authorization: Bearer $CONVAT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"source":"bet9ja","target":"sportybet","code":"5VV8VYM"}'import { randomUUID } from 'node:crypto';
// One key per conversion you intend. Keep it, and send the SAME
// value again if you retry this conversion.
const idempotencyKey = randomUUID();
const res = await fetch('https://eval-api.convat.io/v1/convert', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CONVAT_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({"source":"bet9ja","target":"sportybet","code":"5VV8VYM"}),
signal: AbortSignal.timeout(60_000),
});
const data = await res.json();
if (res.ok) {
console.log(data.targetCode, `${data.matched} of ${data.total} legs`);
} else {
console.log(res.status, data.error, data.message);
}A real response from the evaluation service. This one is a partial: 6 of 8 legs were booked, and unmatched says why the rest were not. Long leg lists are shortened here.
{
"success": true,
"sourceBookie": "bet9ja",
"sourceCode": "5VV8VYM",
"targetBookie": "sportybet",
"targetCode": "GTTDHC",
"matched": 6,
"total": 8,
"slipOddsTotal": 42.48,
"sourceOddsTotal": 22.84,
"targetOddsTotal": 24.4,
"matchedSelections": [
{
"eventName": "Olimpia Milano - Dubai Basketball",
"competition": "Euroleague",
"market": "WINNER",
"selection": "HOME",
"line": null,
"startDate": "2026-10-15 18:30:00",
"sourceOdds": "2.05",
"targetOdds": "2.12",
"lineAdjusted": false,
"bookedLine": null,
"inPlay": false
},
{
"eventName": "AL Hazem - AL Fateh",
"competition": "Saudi Professional League",
"market": "MATCH_RESULT",
"selection": "HOME",
"line": null,
"startDate": "2026-10-15 15:15:00",
"sourceOdds": "2.52",
"targetOdds": "2.6",
"lineAdjusted": false,
"bookedLine": null,
"inPlay": false
}
],
"unmatched": [
{
"eventName": "Brisbane Bullets - SE Melbourne Phoenix",
"market": "WINNER",
"selection": "AWAY",
"line": null,
"startDate": "2026-10-15 08:30:00",
"reason": "event_not_found",
"reasonText": "We couldn't find this game on SportyBet."
},
{
"eventName": "Kolodziej, Krystian - Jadczyk, Marcin",
"market": "WINNER",
"selection": "AWAY",
"line": null,
"startDate": "2026-10-15 03:45:00",
"reason": "event_not_found",
"reasonText": "We couldn't find this game on SportyBet."
}
],
"processingTimeMs": 733
}Hosts and keys
- Production:
https://api.convat.io/v1(production keys, issued with a paid plan) - Evaluation:
https://eval-api.convat.io/v1(evaluation keys)
Keys are issued after a short agreement. Request access at convat.io/api.
Send your key as Authorization: Bearer <key>. Keys are scoped per environment: an evaluation key works only on the evaluation host and a production key only on the production host. Call the API from your own backend. Never put a key in a mobile app, a browser or any other client.
What a conversion returns
A real coupon created in the target bookmaker's own system (targetCode), plus the odds read back after creation. Not a list of legs for you to book.
Partial conversions
Bookmakers do not all carry the same games and markets, so a conversion can succeed with fewer legs than the source slip. Every 2xx accounts for every leg: matchedSelections.length + unmatched.length == total, and every entry in unmatched carries a reason code and a reasonText sentence. Compare matched with total rather than assuming a full slip.
Lines on totals
When the destination does not list the exact line of an over/under total, the leg is booked on the nearest line it does list, never more than 0.5 away, and marked lineAdjusted: true with the bookedLine. Handicaps never move. Send "exactLines": true with /convert to refuse that: every total is booked on its own line or left out, listed in unmatched with reason selection_not_offered and a reasonText naming the closest line. We can also switch exact lines on for your whole key, so every conversion uses them without the field: ask support@convat.io.
Odds
slipOddsTotal: the whole source slip at the source bookmaker's prices.sourceOddsTotalandtargetOddsTotal: the legs that were BOOKED, at the source and target prices. Compare these two with each other.targetOddsTotalisnullwhen the target returns no price./previewreturns source odds only. It contacts no target bookmaker.
Odds are read back from the created coupon, and odds move. Show them to your users before they stake.
Credits
One credit per conversion that returns a target code (a 2xx with targetCode). Failed conversions, refusals, unsupported directions, /preview, /health and replays cost nothing. At the cap every conversion is refused with 402 quota_exhausted and nothing is booked.
Idempotency-Key (required on POST /convert)
Generate a new value (a UUID) for every conversion you intend, and send the SAME value on every retry of it.
- Completed: sending the key again within 24 hours of the result returns the original status and body, byte for byte, with
Idempotency-Replayed: true. Nothing is booked and no credit is used. - Still running:
409 request_in_progress. Nothing new is started. Wait forRetry-Afterand retry with the same key to receive the result. - Different payload: the same key with a different source, target, code or exactLines is refused with
422 idempotency_key_reused. Nothing is booked. - Only a 2xx is final. After any error the key is released, so a retry with the same key runs the conversion again, and the failed attempt cost nothing.
- A conversion keeps running if your client times out or disconnects. Retry with the same key: you receive its result when it has finished, or 409 while it is still running.
- After 24 hours a key is forgotten and the same value starts a new conversion.
- If our service restarts while your request is running, the key stays in progress for up to about two and a half minutes and then a retry runs the conversion again. In that rare case a coupon may have been created at the bookmaker that was never returned to you and was never charged.
Request ids
Every response, including every error, carries:
X-Request-Id: new for every HTTP request.X-Correlation-Id: the same for every attempt and every replay that uses one Idempotency-Key, so one intended conversion can be traced end to end.
Timeouts, retries and unknown outcomes
Most conversions answer in a few seconds. A slow bookmaker can stretch one to about a minute, and a few destinations occasionally take longer. Set your client timeout to 60 seconds; if it fires, retry with the same Idempotency-Key and you receive the result once the conversion has finished.
Retry only 429, 502 and 503, with backoff and always with the SAME Idempotency-Key. On 409, wait and retry with the same key. Never retry a 2xx. Other 4xx responses are final for that request.
A 429, 502, 503 or a timeout alone does not prove that no coupon was created. Retrying with the same key is always safe. If you still need help, email support@convat.io with the X-Request-Id, X-Correlation-Id, Idempotency-Key, source, target, source code and the time in WAT.
Limits
Per key: a number of requests per minute (/convert and /preview together) and a number of conversions in flight at once. /preview has its own in-flight allowance of the same size. Over a limit: 429 with Retry-After. The limits on your key are confirmed when it is issued.
Markets, selections and dates
market and selection are universal codes, the same for every bookmaker (for example MATCH_RESULT with HOME, or OVER_UNDER with OVER and a line). On /preview, sourceMarket is the bookmaker's own name for the market. startDate is YYYY-MM-DD HH:MM:SS in UTC, as the source bookmaker reports it, or null.
POST/convert
Convert a booking code to another bookmaker
Reads the source code, matches every selection on the target and creates a live coupon there. One credit when a target code is returned. Requires an Idempotency-Key.
Requires your key: Authorization: Bearer <key>.
Headers
| Header | Description |
|---|---|
Idempotency-Keyrequired | One new value per intended conversion (a UUID is recommended), reused on every retry of it. 1 to 128 printable characters, no spaces. |
Request body
| Field | Type | Description |
|---|---|---|
sourcerequired | BookmakerSource | A bookmaker that can be read as a source. The definitive list of directions for your key is returned on 422 unsupported_direction. |
targetrequired | BookmakerTarget | A bookmaker that can be written as a destination. |
coderequired | string | The booking code as the source bookmaker issued it. Spaces anywhere in it are removed for you. |
exactLines | boolean | Optional. true: book every over/under total on the exact line of the source slip, or leave that leg out (reason selection_not_offered); never a nearest line. Part of the request for Idempotency-Key purposes. Default false: a missing line may be booked on the nearest one, marked lineAdjusted. |
{
"source": "bet9ja",
"target": "sportybet",
"code": "5VV8VYM"
}Responses
| Status | Meaning |
|---|---|
200 | Converted. Compare matched with total to detect a partial. A replay carries Idempotency-Replayed: true. |
400 | Malformed request, missing or invalid Idempotency-Key, or the same bookmaker twice. |
401 | Missing, unknown or revoked key, or a key used on the other environment's host. |
402 | No credits left. Nothing was booked. |
403 | The key is not active: not yet activated, suspended or expired. |
409 | This Idempotency-Key is still being processed. |
413 | Request body too large. |
422 | The request was understood and cannot be converted. See the error code. |
429 | A limit was reached, or a bookmaker throttled us. |
500 | Failed on our side. Nothing was charged. |
501 | The bookmaker cannot do this yet. |
502 | A bookmaker could not be reached. |
503 | At capacity, or the request could not be verified. Nothing was booked. |
A 200 returns ConvertResponse. Errors return Error.
{
"success": true,
"sourceBookie": "bet9ja",
"sourceCode": "5VV8VYM",
"targetBookie": "sportybet",
"targetCode": "GTTDHC",
"matched": 6,
"total": 8,
"slipOddsTotal": 42.48,
"sourceOddsTotal": 22.84,
"targetOddsTotal": 24.4,
"matchedSelections": [
{
"eventName": "Olimpia Milano - Dubai Basketball",
"competition": "Euroleague",
"market": "WINNER",
"selection": "HOME",
"line": null,
"startDate": "2026-10-15 18:30:00",
"sourceOdds": "2.05",
"targetOdds": "2.12",
"lineAdjusted": false,
"bookedLine": null,
"inPlay": false
},
{
"eventName": "AL Hazem - AL Fateh",
"competition": "Saudi Professional League",
"market": "MATCH_RESULT",
"selection": "HOME",
"line": null,
"startDate": "2026-10-15 15:15:00",
"sourceOdds": "2.52",
"targetOdds": "2.6",
"lineAdjusted": false,
"bookedLine": null,
"inPlay": false
}
],
"unmatched": [
{
"eventName": "Brisbane Bullets - SE Melbourne Phoenix",
"market": "WINNER",
"selection": "AWAY",
"line": null,
"startDate": "2026-10-15 08:30:00",
"reason": "event_not_found",
"reasonText": "We couldn't find this game on SportyBet."
},
{
"eventName": "Kolodziej, Krystian - Jadczyk, Marcin",
"market": "WINNER",
"selection": "AWAY",
"line": null,
"startDate": "2026-10-15 03:45:00",
"reason": "event_not_found",
"reasonText": "We couldn't find this game on SportyBet."
}
],
"processingTimeMs": 733
}POST/preview
Read a code into its legs without booking anything
Reads the source slip so you can show it before a target is chosen. Source odds only; no target is contacted. Never charged. No Idempotency-Key needed.
Requires your key: Authorization: Bearer <key>.
Request body
| Field | Type | Description |
|---|---|---|
sourcerequired | BookmakerSource | A bookmaker that can be read as a source. The definitive list of directions for your key is returned on 422 unsupported_direction. |
coderequired | string |
{
"source": "sportybet",
"code": "H6FHCF"
}Responses
| Status | Meaning |
|---|---|
200 | The slip as the source bookmaker holds it. |
400 | Malformed request. |
401 | Missing or invalid key. |
403 | The key is not active. |
422 | The code cannot be read, or the source is not available on your key. |
429 | A limit was reached, or the bookmaker throttled us. |
500 | Failed on our side. |
502 | The source bookmaker could not be reached (sourceUnavailable: true). |
503 | At capacity, or the request could not be verified. |
A 200 returns PreviewResponse. Errors return Error.
{
"sourceBookie": "sportybet",
"sourceCode": "H6FHCF",
"folds": 10,
"totalOdds": 14.49,
"legs": [
{
"eventName": "Suwon Bluewings - Hwaseong FC",
"competition": "K-League 2",
"market": "DOUBLE_CHANCE",
"selection": "HOME_DRAW",
"line": null,
"odds": 1.19,
"startDate": "2026-10-18 05:00:00",
"convertible": true,
"unsupportedReason": null,
"sourceMarket": "Double Chance"
},
{
"eventName": "Podravka Koprivnica - Nykoebing Falster HB",
"competition": "EHF Champions League, Women",
"market": "MATCH_RESULT",
"selection": "AWAY",
"line": null,
"odds": 1.65,
"startDate": "2026-10-18 12:00:00",
"convertible": true,
"unsupportedReason": null,
"sourceMarket": "1X2"
}
],
"convertibleLegs": 10,
"blocker": null,
"processingTimeMs": 1
}GET/health
Liveness probe
No key needed. Never charged.
No key needed.
Responses
| Status | Meaning |
|---|---|
200 | The API is answering. |
{
"status": "ok"
}ConvertRequest
| Field | Type | Description |
|---|---|---|
sourcerequired | BookmakerSource | A bookmaker that can be read as a source. The definitive list of directions for your key is returned on 422 unsupported_direction. |
targetrequired | BookmakerTarget | A bookmaker that can be written as a destination. |
coderequired | string | The booking code as the source bookmaker issued it. Spaces anywhere in it are removed for you. |
exactLines | boolean | Optional. true: book every over/under total on the exact line of the source slip, or leave that leg out (reason selection_not_offered); never a nearest line. Part of the request for Idempotency-Key purposes. Default false: a missing line may be booked on the nearest one, marked lineAdjusted. |
ConvertResponse
| Field | Type | Description |
|---|---|---|
successrequired | boolean | true |
sourceBookierequired | string | |
sourceCoderequired | string | |
targetBookierequired | string | |
targetCoderequired | string | The live booking code created on the target bookmaker. |
matchedrequired | integer | Legs on the created coupon. |
totalrequired | integer | Legs on the source slip. matched < total is a partial. |
slipOddsTotalrequired | number or null | The whole source slip at the source prices. Null when no leg is priced. |
sourceOddsTotalrequired | number or null | The booked legs at the source prices. |
targetOddsTotalrequired | number or null | The booked legs at the target prices, read back after creation. Null when the target returns no price. |
matchedSelectionsrequired | array of MatchedSelection | |
unmatchedrequired | array of UnmatchedSelection | Every source leg that is not on the created coupon. |
processingTimeMsrequired | integer | |
selectionsOmitted | boolean | Only on a replay of a response larger than 256 KB, whose stored copy keeps every top-level field but not the two leg lists. No real slip reaches that size.true |
MatchedSelection
| Field | Type | Description |
|---|---|---|
eventNamerequired | string or null | |
competitionrequired | string or null | |
marketrequired | string or null | Universal market code, e.g. MATCH_RESULT, OVER_UNDER. |
selectionrequired | string or null | Universal selection code, e.g. HOME, OVER. |
linerequired | string or null | The line on the source slip, for markets that have one. |
startDaterequired | string or null | YYYY-MM-DD HH:MM:SS, UTC. |
sourceOddsrequired | string or null | |
targetOddsrequired | string or null | The price the leg was booked at on the target. |
lineAdjustedrequired | boolean | True when the target did not list the exact line and the leg was booked on the nearest one. This is a different bet from the source leg. Never true when the request sent exactLines: true. |
bookedLinerequired | string or null | The line actually booked, when lineAdjusted is true. |
inPlayrequired | boolean | True when the game had already started and the target books it in play. |
UnmatchedSelection
| Field | Type | Description |
|---|---|---|
eventNamerequired | string or null | |
marketrequired | string or null | |
selectionrequired | string or null | |
linerequired | string or null | |
startDaterequired | string or null | |
reasonrequired | string | Why this leg is not on the coupon.event_startedevent_not_foundselection_not_offeredmarket_not_offeredunsupported_marketplayer_propsimulated_eventodds_mismatchrejected_at_creationdestination_timeoutnot_converted |
reasonTextrequired | string | The same, as one plain sentence that is safe to show to your users. |
PreviewRequest
| Field | Type | Description |
|---|---|---|
sourcerequired | BookmakerSource | A bookmaker that can be read as a source. The definitive list of directions for your key is returned on 422 unsupported_direction. |
coderequired | string |
PreviewResponse
| Field | Type | Description |
|---|---|---|
sourceBookierequired | string | |
sourceCoderequired | string | |
foldsrequired | integer | Number of legs on the slip. |
totalOddsrequired | number or null | Product of the source odds. |
legsrequired | array of PreviewLeg | |
convertibleLegsrequired | integer | Legs whose bet type can be converted at all. |
blockerrequired | object or null | Set only when most of the slip cannot convert to ANY target, so you can stop before calling /convert. |
processingTimeMsrequired | integer |
PreviewLeg
| Field | Type | Description |
|---|---|---|
eventNamerequired | string or null | |
competitionrequired | string or null | |
marketrequired | string or null | Universal market code, or null when the bet type is not supported. |
selectionrequired | string or null | |
linerequired | string or null | |
oddsrequired | number or null | The source bookmaker's price. |
startDaterequired | string or null | YYYY-MM-DD HH:MM:SS, UTC. |
convertiblerequired | boolean | |
unsupportedReasonrequired | string or null | Why the leg cannot be converted, e.g. simulated_event, player_prop, unmapped_market. |
sourceMarketrequired | string or null | The bookmaker's own name for the market. |
Error
| Field | Type | Description |
|---|---|---|
errorrequired | string | Stable machine-readable code. Branch on this. |
messagerequired | string | A plain sentence, safe to show to your users. |
sourceUnavailable | boolean | On upstream_error: the SOURCE bookmaker could not be read. |
alternatives | array of string | On no_matches: targets on your key that do carry these games. |
supported | array of object | On unsupported_direction: every direction your key can use. |
supportedSources | array of string | On unsupported_source: every source your key can read. |
creditsUsed | integer | On quota_exhausted. |
creditCap | integer | On quota_exhausted. |
Leg reasons
Every entry in unmatched carries one of these codes in reason, plus a plain reasonText sentence that is safe to show to your users. Branch on the code.
event_startedevent_not_foundselection_not_offeredmarket_not_offeredunsupported_marketplayer_propsimulated_eventodds_mismatchrejected_at_creationdestination_timeoutnot_convertedErrors
Branch on error, never on message. message is a plain sentence that is safe to show to your users and never names us.
| Status | error | Meaning | What to do |
|---|---|---|---|
| 400 | bad_request | A required field is missing or malformed, or the body is not valid JSON. | Fix the request. |
| 400 | idempotency_key_required | POST /convert was sent without an Idempotency-Key header. | Fix the request. |
| 400 | invalid_idempotency_key | The Idempotency-Key is not 1 to 128 printable characters without spaces. | Fix the request. |
| 400 | same_bookie | Source and target are the same bookmaker. | Fix the request. |
| 401 | unauthorized | The key is missing, unknown, revoked, or belongs to the other environment. | Check the key and the host. |
| 402 | quota_exhausted | Every credit on the key has been used. Nothing was booked. | Do not retry. |
| 403 | key_not_active | The key has been issued but not yet activated. | Contact support. |
| 403 | key_suspended | The key is suspended or otherwise not active. | Contact support. |
| 403 | key_paused | You paused this key in your developer dashboard. Nothing was booked. | Resume the key, then retry. |
| 403 | key_expired | The key has passed the end of its term. | Contact support. |
| 404 | not_found | No such endpoint. | Fix the path. |
| 409 | request_in_progress | A request with this Idempotency-Key is still running. Nothing new was started. | Wait for Retry-After, then retry with the SAME key to receive its result. |
| 413 | payload_too_large | The request body is larger than 32 KB. | Fix the request. |
| 422 | idempotency_key_reused | This Idempotency-Key was already used with a different source, target or code. Nothing was booked. | Use a new key for a new conversion. |
| 422 | unsupported_direction | This source to target direction is not available on your key. The response lists every direction that is. | Do not retry. |
| 422 | unsupported_source | /preview only: this bookmaker cannot be read as a source on your key. The response lists those that can. | Do not retry. |
| 422 | invalid_or_expired_code | The booking code does not exist on the source bookmaker, or has expired. | Do not retry. |
| 422 | events_started | Most of the games on the slip have already started. | Do not retry. |
| 422 | no_matches | The code was read, but the target bookmaker carries none of its games. May list alternatives. | Do not retry this direction. |
| 422 | unsupported_market | Most of the slip is made of bet types that cannot be converted. | Do not retry. |
| 422 | market_unavailable | The target does not offer these markets on these games. | Do not retry this direction. |
| 422 | simulated_events | The slip is made of simulated (virtual) games, which bookmakers do not carry. | Do not retry. |
| 422 | player_prop | The slip is made of player bets, which cannot be converted yet. | Do not retry. |
| 422 | empty_slip | The code resolved to a slip with no selections. | Do not retry. |
| 422 | slip_creation_failed | The target bookmaker refused to create the coupon, or returned no code. | Retrying later with a NEW Idempotency-Key may work. |
| 429 | rate_limited | Your per-minute limit is used up, or a bookmaker throttled us. | Retry after Retry-After (when present) with the SAME key. |
| 429 | concurrency_limit_exceeded | More requests in flight at once than your key allows. | Retry when one finishes, with the SAME key. |
| 500 | internal_error | Something failed on our side. Nothing was charged. | Contact support with the ids. |
| 500 | conversion_failed | The conversion failed for a reason we could not classify. Nothing was charged. | Contact support with the ids. |
| 501 | not_implemented | That bookmaker cannot do this yet. | Do not retry. |
| 502 | upstream_error | A bookmaker could not be reached or did not answer in time. sourceUnavailable says when it was the source. | Retry with the SAME key. |
| 503 | server_busy | The service is briefly at capacity. | Retry after Retry-After with the SAME key. |
| 503 | service_unavailable | We could not verify the key, the credit or the Idempotency-Key, so nothing was booked. | Retry after Retry-After with the SAME key. |
| 503 | target_unavailable | The target bookmaker is not issuing booking codes at the moment. | Retry later, or use another target. |
Bookmakers
Keys cover every bookmaker the service supports: 51 can be read as a source and 57 can be written as a destination, 2,866 directions in all. Every one of them is listed below.
betano, goka, ngbet, stake restrict access from many locations, so reading their slips is occasionally unavailable. When it is, /convert and /preview answer 502 upstream_error with sourceUnavailable: true. Nothing is booked and nothing is charged.
A request for a direction your key does not cover is refused with 422 unsupported_direction, and that response lists every direction your key can use, so you never need to hardcode the list. New bookmakers are added to keys that cover every bookmaker as soon as the service supports them.
Source and destination
1win1xBet 1xbet1xBit 1xbit22Bet 22bet888Starz 888starzAfropari afropariBangBet bangbetBC.GAME bcgameBet9ja bet9jaBetJam betjamBetkadio betkadioBetKing betkingbetPawa betpawaBetpikr betpikrBetway (Nigeria) betwayBetwinner betwinnerBet&You betyouDB Bet dbbetEasybet easybetFastpari fastpariFootball.com footballGOKA gokaGoldpari goldpariHelabet helabetLinebet linebetLiveScore Bet livescorebetLuckypari luckypariMegapari megapariMelbet melbetMSport msportNairaBet nairabetNGBET ngbetOdibets odibetsParipesa paripesaPlanbet planbetRazed razedSpade spadeSportPesa sportpesaSportyBet (Nigeria) sportybetWinwinbet winwinbetXparibet xparibetSource only (codes can be read from them)
accessbetBetano betanoBetCorrect betcorrectBetika betikaiLotBet ilotbetMeridianbet meridianMozzart mozzartPariPulse paripulsePepeta pepetaStake stakeDestination only (country stores; send the id as the target)
1xbet_ben1xBet Burkina Faso 1xbet_bf1xBet Cote d'Ivoire 1xbet_civ1xBet Cameroon 1xbet_cmr1xBet Ghana 1xbet_gh1xBet Global 1xbet_global1xBet Kenya 1xbet_ken1xBet Malawi 1xbet_mw1xBet Nigeria 1xbet_ng1xBet Uganda 1xbet_ug1xBet Zambia 1xbet_zmSportyBet Cameroon sportybet_cmSportyBet Ghana sportybet_ghSportyBet Kenya sportybet_keSportyBet Tanzania sportybet_tzSportyBet Zambia sportybet_zmReady to build?
Request access and we will send an evaluation key with free credits.
Request access