💟 Introduction to the API documentation

The Booking Experts API is organised around REST and it follows the JSON API specification. The API has predictable, resource-oriented URLs, and uses HTTP response codes to indicate API errors.

Documentation is standardized by using the Open API 3 (OAS3) specification, this allows you to inspect the API using other clients like for example Swagger UI and Postman. The specification is hosted here: https://api.bookingexperts.nl/v3/oas3.json


Responses

The Booking Experts API will always respond with a HTTP status code. The API can return the following codes:

CodeSemanticMeaning
200OKRequest was successful
400Bad RequestParameters for the request are missing or malformed. Body contains the errors.
401UnauthorizedYour API key is wrong
403ForbiddenIP is blacklisted for API usage, see Throttling information
404Not FoundEntity not found
422Unprocessable entitySaving the entity in the database failed due to validation errors. Body contains the errors.
429Too Many RequestsYou're requesting too many kittens! Slow down!
5XXServer ErrorsSomething went wrong on Booking Experts's end. We are probably already busy solving the issue. It's your responsibility to retry the request at a later point.
//Might produce the following output
{
  "errors": [
    {
      "status": 401,
      "code": "RESOURCE_NOT_FOUND",
      "title": "Unauthorized error",
      "detail": "Please make sure to set the Authorization HTTP header"
    }
  ]
}

Error Codes

Error codes are custom to give you more information. The list below contains the most common errors that can occur.

  • APPLICATION_HALTED: Your app is halted and you cannot perform any requests anymore
  • CONFLICT: A conflict error occurred during mutation
  • FORBIDDEN_INCLUDE: An include was specified for which you don't have any permission
  • FORBIDDEN_INPUT: Invalid input was specified. See the error message for more details
  • INTERNAL_SERVER_ERROR: An error occurred on our side. We are notified automatically of this.
  • INVALID_ATTRIBUTE: The attribute value specified is invalid
  • INVALID_FIELDSET: The fieldset specified is incorrect
  • INVALID_FILTER: The filter specified is incorrect
  • INVALID_INCLUDE: The include specified is incorrect
  • INVALID_PAGINATION: Incorrect pagination parameters
  • INVALID_RELATIONSHIP: The relationship specified is invalid
  • INVALID_SORTING: The sorting parameters specified is incorrect
  • INVALID_TOKEN: The token is invalid er revoked
  • NO_ADMINISTRATION_ACCESS: You don't have access to the given administration
  • NO_CHANNEL_ACCESS: You don't have access to the given channel
  • NO_VALID_SCOPE: No valid scope was found for the called endpoint
  • RATE_LIMITED: Your request was rate limited
  • RESOURCE_NOT_FOUND: The requested resource could not be found
  • SUBSCRIPTION_CANCELLED: The current subscription subscription is cancelled
  • UNKNOWN_ATTRIBUTE: An unknown attribute was specified
  • UNKNOWN_FORMAT: The response format requested is unknown
  • UNKNOWN_RELATIONSHIP: An unknown relationship was specified
  • UNKNOWN_VERSION: The resource could not be serialized for the given serializer version
  • UNSUPPORTED_MEDIA_TYPE: The media type specified is not supported

Accepted Language

You can always pass an Accept-Language header containing a comma separated list of locales. This will limit the result of 'localized' attributes to the locales specified.

x-be-env header

When your application receives a request from Booking Experts, for example when a webhook or command is called, the x-be-env header is passed. Usually, the value of this header will be 'production', denoting that the request originated from our production environment. For testing purposes however, it might be possible that your app will receive requests from a different environment, for example 'staging'. You can check this header if you want to handle these requests differently.

x-be-signature header

When your application receives a request from Booking Experts, for example when a webhook or command is called, the x-be-signature header is passed to allow you to verify that the request was sent by our systems. It uses a HMAC hexdigest to compute the hash based on your Client Secret.

To verify a request, your code should look something like this:

  • No matter which implementation you use, the hash signature starts with sha256=, using the key of your Client Secret and your payload body.
  • Using a plain == operator is not advised. A method like secure_compare performs a "constant time" string comparison, which renders it safe from certain timing attacks against regular equality operators.
  • Note that this approach has been taken from the Github webhook docs. There might be more examples on the web for this solution that can be used as inspiration.
def verify_signature(payload_body)
  signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['OAUTH_CLIENT_SECRET'], payload_body)
  return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_BE_SIGNATURE'])
end

Rate limiting

Usage of the Booking Experts API is virtually unlimited. However, to prevent fraud and abuse, requests to the API are rate limited. By default a leaky bucket limiter with a bucket size of 100 that drains in 180 seconds (i.e., a 'drain rate' of 1.8 per second) is used to implement our rate limiting. This means that bursts of up to 100 calls are allowed, but further requests will then be limited to one call per 1.8 seconds because bucket capacity has been reached. After 3 minutes, the bucket is 'empty' again and a new burst of 100 additional calls is possible. Nonetheless, it's best to space out requests, to prevent reaching bucket capacity too quickly and running into the rate limit.

While within the limit, each response contains a X-RateLimit-Limit and a X-RateLimit-Remaining header containing the set limit & the remaining allowance in the window. If you exceed the limit (i.e., the bucket capacity has been reached), the API will respond with a 429 Too many requests response. This response contains a Retry-After header containing the time (in seconds) after which a new call is allowed.

If your use case requires more lenient rate limits, please contact us at [email protected] to request a higher limit. We will also request a explanation as to why your limit needs to be increased.

More information about the working of leaky bucket limiters can be found at Wikipedia.


Pagination

All collection responses include pagination. In the response body you will find a links node that contains links to first, self, next, prev, last pages. Most responses have 30 records per page.

{
  "links": {
    "self": "https://api.bookingexperts.nl/v3/administrations/1/reservations?page%5Bnumber%5D=2",
    "first": "https://api.bookingexperts.nl/v3/administrations/1/reservations?page%5Bnumber%5D=1",
    "last": "https://api.bookingexperts.nl/v3/administrations/1/reservations?page%5Bnumber%5D=14",
    "prev": "https://api.bookingexperts.nl/v3/administrations/1/reservations?page%5Bnumber%5D=1",
    "next": "https://api.bookingexperts.nl/v3/administrations/1/reservations?page%5Bnumber%5D=3"
  }
  "data": [...]
}

Sparse field sets

By default every request returns a quite complete set of fields (attributes and relationships). You can limit or expand this default set however. Per record type you can specify which fields to include.

Will return only the name and description fields of every administration. Note that this will also omit defined relationships of the resource.


Includes

Includes are a standard part of the JSON:API specification. Each relationship on a resource can be included. Which relationships a resource has can be determined by looking at its Schema.

As an example: the Reservation resource defines a relationship called extra_order_items. This means that you can add extra_order_items to the includes list in the query string. In turn, the ExtraOrderItem has a relationship with an extra, so you could in that case also include the metadata of the associated extra by specifying extra_order_items.extra in your includes list. When you only need the ID of a relationship, it is not necessary to include a resource, as the ID is defined within the relationship itself.


Filters

The following attribute filters are available on GET API calls:

  • filter[attr]=term: attr = 'term'
  • filter[attr]=~term: attr ILIKE '%term%'
  • filter[attr]=term,term: attr IN ('term', 'term')
  • filter[attr]=a..b: attr1 BETWEEN 'a' AND 'b'

All expressions can be inverted by prefixing a !, this holds for the entire expression.

OR-filtering

An OR filter can be created by separating attribute names in a filter with a pipe:

  • filter[attr1|attr2]=term: attr1 = 'term' OR attr2 = 'term'
  • filter[attr1|attr2]=~term: attr1 ILIKE '%term%' OR attr2 ILIKE '%term%'
  • filter[attr1|attr2]=term,term: attr1 IN ('term', 'term') OR attr2 IN ('term', 'term')
  • filter[attr1|attr2]=a..b: attr1 BETWEEN 'a' AND 'b' OR attr2 BETWEEN 'a' AND 'b'

Security

To be able to fetch resources, either directly or through including related resources, you need the appropriate permissions. Permissions can be defined in you App's settings and they translate into OAuth2 scopes that the user needs to explicitly approve. When you are missing permissions for the call you are executing, an error will be returned like the one on the right.

Note: When you update your App's permissions, these will apply directly when using API keys. When using OAuth2 however, subscribers will need to grant permission again in order for the new permissions to be applied.

Example permissions (scopes):

  • availability|read
  • reservation|read
  • category|read
  • payment|write

There are also some channel specific permissions. This functionality is meant for tour operators who don't need to access all reservations, but only reservations and related resources that have been created through their own channel:

  • channel::reservation|read
  • channel::order|read
  • channel::customer|read
{
  "errors": [{
    "status": "403",
    "code": "NO_VALID_SCOPE",
    "title": "Forbidden",
    "detail": "The scopes for your application do not grant you permission to perform this action. One of the following scopes is required: payment|write."
  }]
}