Rowlog
Developers

Resources and documentation for the Rowlog API.

Welcome to the Rowlog API!

As we are soon deprecating our old SOAP API (which is actually very limited), we are providing a better alternative for our 3rd-party developers.

The Rowlog API is partially RESTful - we do not implement HATEOAS, as we feel it is unnecessary.

You can see the changelog here if you are interested.


Minimal .NET example

To show just how easy it is to get started from scratch, heres a quick example in .NET, using Basic Authentication. Requires JSON.NET for converting JSON to a .NET class.

using System.IO;
using System.Net;
using System.Text;
using Newtonsoft.Json;

namespace RowlogMinimalSample
{
    public class RowlogMember
    {
        public int Id { get; set; }
        public string Name { get; set; }
        // ... basic properties of a member.
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Create a request for the Members API.        
            WebRequest request = WebRequest.Create("https://rowlog.com/api/members");
            var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("admin@rokort.dk:admin"));
            // Add required headers.
            request.Headers.Add("Authorization","Basic "+ credentials);
            request.Headers.Add("X-ClubId", "100"); // demo club

            // Get the response.
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            // Display the status.
            Console.WriteLine(response.StatusDescription);
            // Get the stream containing content returned by the API. This is raw JSON.
            Stream dataStream = response.GetResponseStream();
            // Open the stream using a StreamReader for easy access.
            StreamReader reader = new StreamReader(dataStream);
            // Read the content. 
            string responseFromServer = reader.ReadToEnd();
            // Deserialize to a list of rowlog members. Requires JSON.NET.
            var members = JsonConvert.DeserializeObject<List<RowlogMember>>(responseFromServer);
            // Print each member to the screen.
            foreach (var rowlogMember in members)
            {
                Console.WriteLine("ID: {0}, Name: {1}", rowlogMember.Id, rowlogMember.Name);
            }
            // Cleanup the streams and the response.
            reader.Close();
            dataStream.Close();
            response.Close();
            Console.ReadLine();
        }
    }
}

SDK's

To make life easier for you, as well as for ourselves, we are working on creating native language bindings that can be plugged in to whatever stack you are using.

.NET SDK

The .NET SDK is a Portable Class Library using a custom build of PortableRest under the covers, and it is the exact same code that powers our own apps, as well as any integrations from MemberLink.

We will provide a NuGet as soon as we can.


Getting started

This section describes how the API works, and what you must provide in your HTTP request in order to access it. If you are using an SDK, you can probably skip ahead to your SDK's documentation. We do, however, recommend reading the Common Traits & Parameters section.

API root

The API root is currently https://rowlow.com/api.

When an endpoint is referred to as e.g. /api/members, the absolute address would be https://rowlow.com/api/members.

REST 101

Without going into too much detail, here is a quick rundown of what we mean when we say our API is RESTful.

All objects/entities in Rowlog have their own endpoint. Although not all endpoints support it, they can be invoked with these four HTTP verbs: GET, POST, PUT and DELETE.

Lets use the Member API as an example. Here is how each verb is mapped to an action (and this is common for most, if not all REST API's out there):

  • GET /api/members: Gets a list of members. Returns an array of Member objects.
  • GET /api/members/1: Get the member with ID 1. Returns that one member object.
  • POST /api/members: Creates a new member. Returns the new member object.
  • PUT /api/members/1: Updates member 1. Returns the updated member object.
  • DELETE /api/members/1: Deletes member 1. Returns nothing.

This looks an awful lot like CRUD (Create, Read, Update, Delete). Thats probably because it is CRUD.

Not only is this simple, but since pretty much any language worth mentioning has a reliable HTTP client library, everyone can integrate with it.

General request requirements

There are a few things each request must contain in the HTTP headers, unless otherwise stated.

  • Preferably, make sure you use a content type. For JSON:

    Content-Type: application/json; charset=utf8
    
  • Authentication information. Please see the documentation on Authentication for details.

Authentication

As mentioned above, please see the dedicated documentation for authentication

Request/response formats

The API backend is powered by ASP.NET Web Api, which natively supports XML and JSON. Personally we prefer JSON, and as of now we only provide examples with JSON.

All JSON is case-insensitive. The only difference between the following 2 snippets are semantics.

// This...
{
    "id": 123,
    "name": "Rowdie"
}
// .. is the same as this
{
    "Id": 123,
    "Name": "Rowdie"
}

Omitted JSON fields

When a value is null, it is not returned in the response. This shaves off a few bytes on the response, and we see no harm in doing this.

CORS / JSONP

Cross Origin Resource Sharing (CORS) is supported on all endpoints, unless noted otherwise.

JSONP is also supported, but at the moment only for public API's, since Basic Auth headers cannot be passed along in script tags.

Since JSONP does not permit custom headers, you should use the querystring variant of passing the club ID.

Example (using Trips endpoint, a currently public API):

GET /api/trips/?callback=?&clubId=103

Note: The callback=? is for jQuery's implementation.


Working with an endpoint

As mentioned earlier, all endpoints share some common actions/functionality.

In the following sections, we will use the Members endpoint as an example.

  • Listing entities (Get All) - GET /api/members. Returns an array of entities.
  • Fetching a single entity (Get One) - GET /api/members/1. Returns a single entity.
  • Creating an entity (Create) - POST /api/members. Returns the created entity.
  • Updating an existing entity (Update) - PUT /api/members/1. Returns the updated entity.
  • Deleting an entity (Delete) - DELETE /api/members/1. Returns no response body.

Listing entities (Get All)

To get a list (array) of entities:

GET /api/members

This returns an HTTP 200 OK with the following response:

[
    {
        "id": 1,
        "name": "Rowdie Rower",
        "birthDate": "1994-09-06T00:00:00",
        ... removed for brevity
    },
    {
        "id": 2,
        "name": "Kaya Kayak",
        "birthDate": "1995-06-09T00:00:00",
        ... removed for brevity
    },
]

Fetching a single entity (Get One)

To get a single entity:

GET /api/members/1

Where 1 is the ID of the entity you want to get. This returns an HTTP 200 OK with the following response:

{
    "id": 1,
    "name": "Rowdie Rower",
    "birthDate": "1994-09-06T00:00:00",
    ... removed for brevity
}

Creating an entity (Create)

To create a new entity,

POST /api/members/

with the following request body:

{
    "name": "Rowdina Rower",
    "birthDate": "1995-09-06T00:00:00",
    ... removed for brevity
}

This returns an HTTP 200 OK with the following response:

{
    "id": 2,
    "name": "Rowdina Rower",
    "birthDate": "1995-09-06T00:00:00",
    ... removed for brevity
}

Updating an existing entity (Update)

To update an existing entity,

PUT /api/members/2

with the following request body:

{
    "name": "Rowdeena Rower",
    "birthDate": "1996-10-07T00:00:00",
    ... removed for brevity
}

Where 2 is the ID of the entity you want to update, this returns an HTTP 200 OK with the following response:

{
    "id": 2,
    "name": "Rowdeena Rower",
    "birthDate": "1996-10-07T00:00:00",
    ... removed for brevity
}

Deleting an entity (Delete)

To get a single entity:

DELETE /api/members/2

Where 2 is the ID of the entity you want to delete. This returns an HTTP 200 OK with an empty response body.


Common Traits & Parameters

All endpoints (unless stated otherwise) share some common traits and parameters. If an endpoint does not support something being listed here, we will state so in the respective endpoint's documentation.

When an action is marked as public, it means authentication is not necessary. If an action is marked as private, it means you, the client making the request, must have access to the private API's. Each authentication method specifies how to provide this.

Pagination / Skipping, limiting

Any Get All request can be paginated.

GET /api/members/?limit=10&offset=10

The above will return 10 members, starting at member 11 (inclusive) and ending at member 20 (inclusive).

The total count is returned in the response header, as X-TotalCount.

To prevent overload and extremely large responses, each endpoint limits the max amount of entities that can be returned. This is typically 100.

Example: If you request Get /api/members/?limit=200, you will only get 100 members back in the response.

Enveloping

If parsing HTTP headers is not your thing, or if you are using JSONP, you can get your response wrapped in an envelope. Only applicable for Get All requests.

Example request:

GET /api/members/?envelope=true

Example response:

{
    "totalCount": 100,
    "data": [
        .. array of members
    ]
}

Filtering

An endpoint may support filtering, and if it does it will say so in the respective endpoint's documentation.

Filters are added to the querystring of a Get All request.

Again, using the Members endpoint as an example, we can search for members using the searchText filter.

GET /api/members/?searchText=row

In the Members endpoint's case, the search text is applied to the member.name (does the name contain searchText?) field, so it would return Rowdie Rower, Rowdeena Kayak and John Rower, but not Bob Kayak.

Filtering, by it's very definition, will reduce the amount of entities being returned. When combined with paging, filters are applied first, and the totalCount, reflects the total count after filters have been applied.

Example: Your club has 200 members, and the max limit for the Members endpoint is 100. If you were to Get All members without any filtering or pagination, you would get the first 100 members, and a totalCount of 200.

Now, if you call

GET /api/members/?searchText=r

If you have 150 members that contain the letter r, you would get the first 100 members, and a totalCount of 150.


Error Handling

Part of being human involves causing errors.

When an error is being returned along with a non-successful status code, the response payload will contain alteast the following:

{
    "message": "Something went wrong somewhere, good luck to you."    
}

We do our best to map the error type to the appropriate HTTP status code, however there are a few cases we sort of.. redefine what they mean.

Here is a quick rundown of what we return, and when.

  • HTTP 200 OK: Everything went as smooth as it could, as in no severe input/processing errors.
  • HTTP 400 Bad Request: Forgetting parameters/fields, incorrect format, etc, trigger these. See validation
  • HTTP 401 Unauthorized: Per standard, a 401 means unauthenticated or unauthorized. In our case, it only means unauthenticated - we use 403 as unauthorized, as we felt this made more sense.
  • HTTP 403 Forbidden: We use this for unauthorized requests. Example, when attempting to create a new member without the memberAdmin privilege.
  • HTTP 404 Not Found: When you, in one way or another, attempt to resolve an entity that does not exist.

    Example: requesting GET /api/members/123, but a member with ID 123 does not exist.

    Example: Creating a new member with the following payload:

    {
        "name": "Bob Rower",
        ...
        "memberTypeId": 5
    }
    

    but a MemberType with ID 5 does not exist.

    The response message will contain a string detailing the ID + type of entity that was not found. In this case, something like A MemberType with ID "5" was not found..

  • HTTP 500 Internal Server Error: These are the ones we are all scared of. If you get one of these, the response does not really provide anything of value, and if this problem persists, please contact us with the details of your request, what SDK you are using (if any), your club ID, basically anything that you think will help us.

  • HTTP 503 Service Unavailable: You get these when we are currently updating things.


Validation

If validation rules are violated, a HTTP 400 Bad Request is usually returned, with a response body similar to this example:

{
    "message": "One or more validation errors have occured.",
    "validationErrors": [
        {
            "fieldName": "name",
            "rule": "Required",
            "message": "'Name' must not be empty.",
        },
        {
            "attemptedValue": "someinvalidmail@.com",
            "fieldName": "emailAddress",
            "rule": "Email",
            "message": "Not a valid email address",
            "state": [
                // any additional state that may be of use goes into this array.
            ]
        }
    ]
}

Entities

These are the entity types we expose in the API.

Each field is displayed in propertyName: type format.

If the type is suffixed with a ?, it can be set to null (even if it is a value type).

Example: billToId: int?, means that billToId is an int, but it can be set to null.

Each page describes the entity's fields, as well as any additional notes.


Endpoints

These are the endpoints we currently provide.

Each page describes what the endpoint supports.