Pinnacle Sports: How to implement a REST client using .NET

.NET in conjunction with the ASP.NET Web API has the necessary features to build web services and clients. In this article we will implement a REST client for the Pinnacle Sports API.

What is REST?

REST has been around for some long time, but lately it has been associated with web services using HTTP and the POST, GET, PUT and DELETE methods. The data can be in several formats but generally JSON or XML is used.

What is Pinnacle Sports?

Pinnacle Sports is as bookmaker who provides an API for reading the available odds. You will need to make an account with Pinnacle Sports to be able to use their API.

We will implement their most recent API as of this date. They also have a legacy API, so I suppose you could call it the second version, although the URL to the API mentions version 1.

Terms of use

The API is free to use for bettors or affiliates. If you want to use it for commercial purposes you need to contact them.

Read the terms and conditions and the fair use policy before using the API.

You can make requests for the feed command once every 60 seconds or once every 5 seconds when you specify the last parameter.

General concepts

The API has several available commands to fetch the sports, leagues and events. At the time of writing this article there were no commands to place bets directly, but now there are. This article will only cover the read commands.

You use HTTPS and Basic Access Authentication to access the API. The API response is in XML format.

Considerations

I will not be providing further updates in the article, so any changes to the API will not be taken into account.

Also, I will not be held responsible in any way by how you decide to use the API, so please use it responsibly. What I provide in this article is not software, but an implementation of a client to communicate with the API.

Using the client

First you initialize the client, where you specify your ID and password.

var client = new PinnacleClient();
client.ClientId = "Your client ID";
client.Password = "Your password";
client.DefaultOddsFormat = OddsFormat.Decimal;
client.DefaultCurrency = "EUR";

You can obtain a list of available currencies to use when requesting feeds:

var currencies = client.GetCurrencies();

foreach (var currency in currencies)
{
    Console.WriteLine("{0}\t{1}", currency.Code, currency.Name);
}

How to get a list of sports:

var sports = client.GetSports();

foreach (var sport in sports)
{
    Console.WriteLine("{0}\t{1}", sport.Id, sport.Title);
}

Getting the leagues from a specific sport:

var sportId = 29;
var leagues = client.GetLeagues(sportId);

foreach (var league in leagues)
{
    Console.WriteLine("{0}\t{1}", league.Id, league.Title);
}

Obtaining a feed with event information:

var leagueIds = new[] { 2627 };
var feed = client.GetFeed(sportId, leagueIds);

foreach (var sport in feed.Sports)
{
    foreach (var league in sport.Leagues)
    {
        foreach (var match in league.Events)
        {
            Console.WriteLine("{0:g}\t{1} vs {2}",
                match.StartDateTime,
                match.HomeTeam.Name,
                match.AwayTeam.Name);
        }
    }
}

Remember to respect the fair use policy: one call every 60 seconds.

The best practice is to use the last timestamp which allows one call every 5 seconds:

var last = feed.Time;
feed = client.GetFeed(sportId, leagueIds, last);

Requirements

To be able to build the client you need to add the following assemblies:

  • System.Net.Http
  • System.Net.Http.WebRequest
Add references to project
Add references to project

Besides that, you also need to install the Microsoft ASP.NET Web API 2.1 Client Libraries. You can do it through NuGet.

Install Web API Client Libraries through NuGet
Install Web API Client Libraries through NuGet

How to implement the API

You can use the following approach to implement any REST API:

  • Check the available commands and responses from the web service
  • Map the entities and provide serialization features
  • Create a general client to communicate with the web service
    • Call commands by building request URIs
    • Parse responses

Entity structure

  • Response
    • ResponseError
    • Currency
    • Sport
    • League
    • Feed
      • FeedSport
        • FeedLeague
          • Event
            • Team
            • Period
              • MoneyLine
              • Spread
              • BetAmount

The reason we have two different sport and league entities is because they serve different purposes.

When we want to get sports information we make a request for sports. When we want to get events we request a feed, which will return detailed event data but only include sport and league IDs.

The Pinnacle Sports API returns data in XML format. We will make use of the XML serialization features in .NET and decorate our entities with attributes.

Mapping the Sport entity

To obtain the sports you make a GET call to the following URL: https://api.pinnaclesports.com/v1/sports.

This will return the following response:

<rsp status="ok">
    <sports>
        <sport id="1" feedContents="1">Badminton</sport>
        <sport id="2" feedContents="1">Bandy</sport>
        <sport id="3" feedContents="1">Baseball</sport>
        <sport id="4" feedContents="1">Basketball</sport>
        <sport id="6" feedContents="0">Boxing</sport>
        <!-- etc. -->
    </sports>
</rsp>

So how do we map this to our class?

[Serializable]
[XmlRoot("sport")]
public class Sport
{
    [XmlAttribute("id")]
    public int Id { get; set; }
 
    [XmlAttribute("feedContents")]
    public int FeedContents { get; set; }
 
    [XmlText]
    public string Title { get; set; }
}

We just place the necessary attributes and the serialization will be done automatically:

  • XmlRoot specifies the root element name.
  • XmlAttribute for attributes.
  • XmlText for the containing text.

Mapping the Event entity

Let’s look at a more complex example, an event:

<event>
    <startDateTime>2014-03-07T09:15:00Z</startDateTime>
    <id>357948005</id>
    <IsLive>No</IsLive>
    <status>I</status>
    <homeTeam type="Team1">
        <name>Trap</name>
        <rotNum>4863</rotNum>
    </homeTeam>
    <awayTeam type="Team2">
        <name>Soo</name>
        <rotNum>4864</rotNum>
    </awayTeam>
    <periods>
        <period lineId="125334260">
            <number>0</number>
            <description>Match</description>
            <cutoffDateTime>2014-03-07T09:15:00Z</cutoffDateTime>
            <spreads />
            <moneyLine>
                <awayPrice>1.943</awayPrice>
                <homePrice>1.909</homePrice>
            </moneyLine>
            <maxBetAmount>
                <spread>0</spread>
                <moneyLine>364.1</moneyLine>
            </maxBetAmount>
        </period>
    </periods>
</event>

As you can see, it has several different elements. The mapping is the following:

[Serializable]
[XmlRoot("event")]
public class Event
{
    [XmlElement("startDateTime")]
    public DateTime StartDateTime { get; set; }
 
    [XmlElement("id")]
    public long Id { get; set; }
 
    [XmlElement("IsLive")]
    public string IsLiveString { get; set; }
 
    [XmlIgnore]
    public bool IsLive
    {
        get
        {
            return IsLiveString.Equals("Yes", StringComparison.OrdinalIgnoreCase);
        }
        set
        {
            IsLiveString = (value ? "Yes" : "No");
        }
    }
    
    [XmlElement("status")]
    public string StatusString { get; set; }
 
    [XmlIgnore]
    public Status Status
    {
        get
        {
            if (!string.IsNullOrWhiteSpace(StatusString))
            {
                switch (StatusString.ToLower())
                {
                    case "o":
                        return Status.Open;
                    case "i":
                        return Status.LowerMaximum;
                    case "h":
                        return Status.Unavailable;
                    case "x":
                        return Status.Cancelled;
                    default:
                        throw new Exception("Unrecognized status: " + StatusString);
                }
            }
            throw new Exception("No status string");
        }
    }
 
    [XmlElement("homeTeam")]
    public Team HomeTeam { get; set; }
 
    [XmlElement("awayTeam")]
    public Team AwayTeam { get; set; }
 
    [XmlArray("periods")]
    [XmlArrayItem("period")]
    public List<Period> Periods { get; set; }
}

Of note:

  • Use XmlIgnore for properties that are not serializable.
  • We have our own custom status enumeration. To use that we build a surrogate StatusString property and make an actual Status property which does the conversion and is ignored in the serialization. We also apply this strategy to the IsLive property.
  • Use XmlElement for child elements.
  • For collections of objects you specify the array element with XmlArray and the name of each item with XmlArrayItem. We map it to a List and the serialization features will know what to do.

The Response entity

Now let’s look at the entity that is returned when you make a call to the web service.

[Serializable]
[XmlRoot("rsp")]
public class Response
{
    [XmlAttribute("status")]
    public string Status { get; set; }
 
    [XmlIgnore]
    public bool IsValid
    {
        get
        {
            return !string.IsNullOrWhiteSpace(this.Status)
                && this.Status.Equals("ok", StringComparison.OrdinalIgnoreCase);
        }
    }
 
    [XmlElement("err")]
    public ResponseError Error { get; set; }
 
    [XmlArray("sports")]
    [XmlArrayItem("sport")]
    public List<Sport> Sports { get; set; }
 
    [XmlArray("leagues")]
    [XmlArrayItem("league")]
    public List<League> Leagues { get; set; }
 
    [XmlArray("currencies")]
    [XmlArrayItem("currency")]
    public List<Currency> Currencies { get; set; }
 
    [XmlElement("fd")]
    public Feed Feed { get; set; }
}

As you may remember, there is always a response entity and its the inner payload that differs: sometimes the response is just a list of sports, other times it is a list of leagues, etc.

We implement all properties and only those that are returned in the response will have any value.

You can find the code of the remaining entities in the download.

Building the client

With all the entities done, we need to build the client that will call the web service and return the response.

How to call web service to get response

In our client constructor with initialize the HttpClient, which is what we use to make calls to the web service.

this.HttpClient = new HttpClient();
this.HttpClient.BaseAddress = new Uri("https://api.pinnaclesports.com/v1/");

This is the method we use to get a response from the web service:

protected IList<T> GetAsync<T>(bool useAuthentication, string requestUri, params object[] values)
{
    if (useAuthentication)
    {
        this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Basic",
            Convert.ToBase64String(
                ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", this.ClientId, this.Password))));
    }
    var response = this.HttpClient.GetAsync(string.Format(requestUri, values)).Result;
    response.EnsureSuccessStatusCode();
 
    var xmlFormatter = new XmlMediaTypeFormatter { UseXmlSerializer = true };
    var apiResponse = response.Content.ReadAsAsync<Response>(new[] { xmlFormatter }).Result;
 
    if (apiResponse.IsValid)
    {
        return CastResponse<T>(apiResponse);
    }
    else
    {
        throw new Exception("Pinnacle Sports API error: " + apiResponse.Error.Message);
    }
}

The use of authentication is conditional because not all API commands require authentication.

We read the response using an XML formatter and we cast and only return the containing payload.

An example of how the method is used internally:

public IList<League> GetLeagues(int sportId)
{
    return GetAsync<League>(false, "leagues?sportid={0}", sportId);
}

Requesting a feed

Now let’s look at the more complex example of requesting a feed.

protected Feed GetFeed(int sportId, int[] leagueIds, OddsFormat format, string currency, long lastTimestamp, int isLive)
{
    if (string.IsNullOrWhiteSpace(this.ClientId))
    {
        throw new Exception("Client ID is mandatory when requesting feeds");
    }
 
    if (string.IsNullOrWhiteSpace(this.Password))
    {
        throw new Exception("Password is mandatory when requesting feeds");
    }
 
    if (this.EnsureFairUse && !IsFairFeedRequest(lastTimestamp))
    {
        throw new Exception(
            string.Format("Too many feed requests. Minimum interval time between request is {0} seconds or {1} seconds when specifying the last parameter",
                this.MinFeedRefresh,
                this.MinFeedRefreshWithLast));
    }
    this.LastFeedRequest = DateTime.Now;
    string uri = GetFeedRequestUri(sportId, leagueIds, format, currency, lastTimestamp, isLive);
    
    return GetAsync<Feed>(true, uri).FirstOrDefault();
}

First we make sure that all the requirements for requesting a feed are met. This includes having a client ID and password, but also ensuring we abide by the fair use policies.

Then we build the request URI and finally we make the request.

protected string GetFeedRequestUri(int sportId, int[] leagueId, OddsFormat format, string currency, long lastTimestamp, int isLive)
{
    StringBuilder sb = new StringBuilder();
    sb.AppendFormat("feed?sportid={0}", sportId);
    sb.AppendFormat("&leagueid={0}", string.Join("-", leagueId));
    sb.AppendFormat("&oddsformat={0}", (int)format);
    sb.AppendFormat("&currencycode={0}", currency);
 
    if (lastTimestamp > 0)
    {
        sb.AppendFormat("&last={0}", lastTimestamp);
    }
 
    if (isLive == 0 || isLive == 1)
    {
        sb.AppendFormat("&islive={0}", isLive);
    }
 
    return sb.ToString();
}

The feed request URI has a more complex structure because of the several parameters it supports, with some of them being optional.

Conclusion

We talked about the most important aspects of building the API. The full code is available in the download.

I hope you have found this tutorial useful and good luck!

Nuno Freitas
Posted by Nuno Freitas on April 30, 2014

Related articles