API documentation · version 2.2.0

Convat API reference

Everything you need to convert booking codes from your own backend: authentication, endpoints, safe retries, every error code and real responses from the live service.

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
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"}'
Node.js 18+
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.

200 OK
{
  "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.
  • sourceOddsTotal and targetOddsTotal: the legs that were BOOKED, at the source and target prices. Compare these two with each other.
  • targetOddsTotal is null when the target returns no price.
  • /preview returns 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 for Retry-After and 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

HeaderDescription
Idempotency-KeyrequiredOne new value per intended conversion (a UUID is recommended), reused on every retry of it. 1 to 128 printable characters, no spaces.

Request body

FieldTypeDescription
sourcerequiredBookmakerSourceA bookmaker that can be read as a source. The definitive list of directions for your key is returned on 422 unsupported_direction.
targetrequiredBookmakerTargetA bookmaker that can be written as a destination.
coderequiredstringThe booking code as the source bookmaker issued it. Spaces anywhere in it are removed for you.
exactLinesbooleanOptional. 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.
Example request
{
  "source": "bet9ja",
  "target": "sportybet",
  "code": "5VV8VYM"
}

Responses

StatusMeaning
200Converted. Compare matched with total to detect a partial. A replay carries Idempotency-Replayed: true.
400Malformed request, missing or invalid Idempotency-Key, or the same bookmaker twice.
401Missing, unknown or revoked key, or a key used on the other environment's host.
402No credits left. Nothing was booked.
403The key is not active: not yet activated, suspended or expired.
409This Idempotency-Key is still being processed.
413Request body too large.
422The request was understood and cannot be converted. See the error code.
429A limit was reached, or a bookmaker throttled us.
500Failed on our side. Nothing was charged.
501The bookmaker cannot do this yet.
502A bookmaker could not be reached.
503At capacity, or the request could not be verified. Nothing was booked.

A 200 returns ConvertResponse. Errors return Error.

Example 200 response
{
  "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

FieldTypeDescription
sourcerequiredBookmakerSourceA bookmaker that can be read as a source. The definitive list of directions for your key is returned on 422 unsupported_direction.
coderequiredstring
Example request
{
  "source": "sportybet",
  "code": "H6FHCF"
}

Responses

StatusMeaning
200The slip as the source bookmaker holds it.
400Malformed request.
401Missing or invalid key.
403The key is not active.
422The code cannot be read, or the source is not available on your key.
429A limit was reached, or the bookmaker throttled us.
500Failed on our side.
502The source bookmaker could not be reached (sourceUnavailable: true).
503At capacity, or the request could not be verified.

A 200 returns PreviewResponse. Errors return Error.

Example 200 response
{
  "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

StatusMeaning
200The API is answering.
Example 200 response
{
  "status": "ok"
}

ConvertRequest

FieldTypeDescription
sourcerequiredBookmakerSourceA bookmaker that can be read as a source. The definitive list of directions for your key is returned on 422 unsupported_direction.
targetrequiredBookmakerTargetA bookmaker that can be written as a destination.
coderequiredstringThe booking code as the source bookmaker issued it. Spaces anywhere in it are removed for you.
exactLinesbooleanOptional. 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

FieldTypeDescription
successrequiredboolean
true
sourceBookierequiredstring
sourceCoderequiredstring
targetBookierequiredstring
targetCoderequiredstringThe live booking code created on the target bookmaker.
matchedrequiredintegerLegs on the created coupon.
totalrequiredintegerLegs on the source slip. matched < total is a partial.
slipOddsTotalrequirednumber or nullThe whole source slip at the source prices. Null when no leg is priced.
sourceOddsTotalrequirednumber or nullThe booked legs at the source prices.
targetOddsTotalrequirednumber or nullThe booked legs at the target prices, read back after creation. Null when the target returns no price.
matchedSelectionsrequiredarray of MatchedSelection
unmatchedrequiredarray of UnmatchedSelectionEvery source leg that is not on the created coupon.
processingTimeMsrequiredinteger
selectionsOmittedbooleanOnly 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

FieldTypeDescription
eventNamerequiredstring or null
competitionrequiredstring or null
marketrequiredstring or nullUniversal market code, e.g. MATCH_RESULT, OVER_UNDER.
selectionrequiredstring or nullUniversal selection code, e.g. HOME, OVER.
linerequiredstring or nullThe line on the source slip, for markets that have one.
startDaterequiredstring or nullYYYY-MM-DD HH:MM:SS, UTC.
sourceOddsrequiredstring or null
targetOddsrequiredstring or nullThe price the leg was booked at on the target.
lineAdjustedrequiredbooleanTrue 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.
bookedLinerequiredstring or nullThe line actually booked, when lineAdjusted is true.
inPlayrequiredbooleanTrue when the game had already started and the target books it in play.

UnmatchedSelection

FieldTypeDescription
eventNamerequiredstring or null
marketrequiredstring or null
selectionrequiredstring or null
linerequiredstring or null
startDaterequiredstring or null
reasonrequiredstringWhy this leg is not on the coupon.
event_startedevent_not_foundselection_not_offeredmarket_not_offeredunsupported_marketplayer_propsimulated_eventodds_mismatchrejected_at_creationdestination_timeoutnot_converted
reasonTextrequiredstringThe same, as one plain sentence that is safe to show to your users.

PreviewRequest

FieldTypeDescription
sourcerequiredBookmakerSourceA bookmaker that can be read as a source. The definitive list of directions for your key is returned on 422 unsupported_direction.
coderequiredstring

PreviewResponse

FieldTypeDescription
sourceBookierequiredstring
sourceCoderequiredstring
foldsrequiredintegerNumber of legs on the slip.
totalOddsrequirednumber or nullProduct of the source odds.
legsrequiredarray of PreviewLeg
convertibleLegsrequiredintegerLegs whose bet type can be converted at all.
blockerrequiredobject or nullSet only when most of the slip cannot convert to ANY target, so you can stop before calling /convert.
processingTimeMsrequiredinteger

PreviewLeg

FieldTypeDescription
eventNamerequiredstring or null
competitionrequiredstring or null
marketrequiredstring or nullUniversal market code, or null when the bet type is not supported.
selectionrequiredstring or null
linerequiredstring or null
oddsrequirednumber or nullThe source bookmaker's price.
startDaterequiredstring or nullYYYY-MM-DD HH:MM:SS, UTC.
convertiblerequiredboolean
unsupportedReasonrequiredstring or nullWhy the leg cannot be converted, e.g. simulated_event, player_prop, unmapped_market.
sourceMarketrequiredstring or nullThe bookmaker's own name for the market.

Error

FieldTypeDescription
errorrequiredstringStable machine-readable code. Branch on this.
messagerequiredstringA plain sentence, safe to show to your users.
sourceUnavailablebooleanOn upstream_error: the SOURCE bookmaker could not be read.
alternativesarray of stringOn no_matches: targets on your key that do carry these games.
supportedarray of objectOn unsupported_direction: every direction your key can use.
supportedSourcesarray of stringOn unsupported_source: every source your key can read.
creditsUsedintegerOn quota_exhausted.
creditCapintegerOn 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_converted

Errors

Branch on error, never on message. message is a plain sentence that is safe to show to your users and never names us.

StatuserrorMeaningWhat to do
400bad_requestA required field is missing or malformed, or the body is not valid JSON.Fix the request.
400idempotency_key_requiredPOST /convert was sent without an Idempotency-Key header.Fix the request.
400invalid_idempotency_keyThe Idempotency-Key is not 1 to 128 printable characters without spaces.Fix the request.
400same_bookieSource and target are the same bookmaker.Fix the request.
401unauthorizedThe key is missing, unknown, revoked, or belongs to the other environment.Check the key and the host.
402quota_exhaustedEvery credit on the key has been used. Nothing was booked.Do not retry.
403key_not_activeThe key has been issued but not yet activated.Contact support.
403key_suspendedThe key is suspended or otherwise not active.Contact support.
403key_pausedYou paused this key in your developer dashboard. Nothing was booked.Resume the key, then retry.
403key_expiredThe key has passed the end of its term.Contact support.
404not_foundNo such endpoint.Fix the path.
409request_in_progressA 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.
413payload_too_largeThe request body is larger than 32 KB.Fix the request.
422idempotency_key_reusedThis Idempotency-Key was already used with a different source, target or code. Nothing was booked.Use a new key for a new conversion.
422unsupported_directionThis source to target direction is not available on your key. The response lists every direction that is.Do not retry.
422unsupported_source/preview only: this bookmaker cannot be read as a source on your key. The response lists those that can.Do not retry.
422invalid_or_expired_codeThe booking code does not exist on the source bookmaker, or has expired.Do not retry.
422events_startedMost of the games on the slip have already started.Do not retry.
422no_matchesThe code was read, but the target bookmaker carries none of its games. May list alternatives.Do not retry this direction.
422unsupported_marketMost of the slip is made of bet types that cannot be converted.Do not retry.
422market_unavailableThe target does not offer these markets on these games.Do not retry this direction.
422simulated_eventsThe slip is made of simulated (virtual) games, which bookmakers do not carry.Do not retry.
422player_propThe slip is made of player bets, which cannot be converted yet.Do not retry.
422empty_slipThe code resolved to a slip with no selections.Do not retry.
422slip_creation_failedThe target bookmaker refused to create the coupon, or returned no code.Retrying later with a NEW Idempotency-Key may work.
429rate_limitedYour per-minute limit is used up, or a bookmaker throttled us.Retry after Retry-After (when present) with the SAME key.
429concurrency_limit_exceededMore requests in flight at once than your key allows.Retry when one finishes, with the SAME key.
500internal_errorSomething failed on our side. Nothing was charged.Contact support with the ids.
500conversion_failedThe conversion failed for a reason we could not classify. Nothing was charged.Contact support with the ids.
501not_implementedThat bookmaker cannot do this yet.Do not retry.
502upstream_errorA bookmaker could not be reached or did not answer in time. sourceUnavailable says when it was the source.Retry with the SAME key.
503server_busyThe service is briefly at capacity.Retry after Retry-After with the SAME key.
503service_unavailableWe could not verify the key, the credit or the Idempotency-Key, so nothing was booked.Retry after Retry-After with the SAME key.
503target_unavailableThe 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

1win 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 xparibet

Source only (codes can be read from them)

Accessbet accessbetBetano betanoBetCorrect betcorrectBetika betikaiLotBet ilotbetMeridianbet meridianMozzart mozzartPariPulse paripulsePepeta pepetaStake stake

Destination only (country stores; send the id as the target)

1xBet Benin 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_zm

Ready to build?

Request access and we will send an evaluation key with free credits.

Request access