This document describes the basic principles of the SRXP API, and how you can get started using it. Please note our API is still in beta, and while we do not expect major changes there might be some minor breaking changes in the near future.
Most API operations require an authenticated user. These credentials can be provided
in the HTTP Authorization
header, using either Basic
authentication, or using an access key.
While it is in theory possible to use basic authentication for every request, we strongly suggest only using basic auth to obtain an access key. To obtain the access key, you thus provide an SRXP e-mail and password
in a Basic Authorization
header. For example, using cURL:
curl -X POST -H "Content-Type: application/json" -d "{}" --user test@test.com:password https://portal.srxp.com/api/1/access_keys
Provided the e-mail and password are correct, this will return a HTTP 201 Created
with a JSON response like:
{
"access_key": "access_key_40_characters_long",
"expires": 123467890,
"user_id": 1,
"id": 6
}
The access_key
returned from this request can be used to authenticate consecutive requests by specifying the HTTP Authorization header:
curl -H "Authorization: SRXPAPI accesskey=access_key_40_characters_long" http://portal.srxp.com/api/1/accounts/1
Below a resource is defined as "an item with an 'id' field".
Generally resources are created by sending a POST
request to the plural version of its resource name with the desired
data. However, most resources in our system are subresources of another resource. When this is the case, and the
resource cannot exist outside of its parent resource, its creation URI can be generated by appending the standard
URI format to its parent resource identifier. To clarify: an expense is always a subresource of an account, and must
therefore be created with a POST to /accounts/{account_id}/expenses
.
We try to be as REST as possible here, but with a few exceptions. Resource listings are generally available at
the plural of their resource name, like /currencies
. Subresources often (also) have shortcut URLs, e.g. you could use
/customers/{customer_id}/categories
to retrieve all categories for the customer with ID customer_id
.
Single resources are only available at the plural of their resource name plus an ID. For instance, an expense
with ID 32 can be accessed at the URI /expenses/32
. We don't nest these resource identifiers - so this is true
even for resources that are created as subresources. That means that /accounts/{account_id}/expenses/{id}
does not exists.
A resource can be updated with a PUT
or PATCH
to its request URI. Note that due to the mediocre browser support for
PATCH
, PUT
and PATCH
are not differentiated by our API and both partially update a resource (so they're both
PATCH
!). See "Data format" for more info on how we deal with deleting fields.
Resource relations are both specified and updated using {resource_name}_id
fields on the child resource. If the
relation is optional, it might be possible to delete the relation by sending a null
value (see "Creating and
updating resources"). Because we do not nest resource URLs, it is always straightforward to construct the resource
URI from the {resource_name}_id
field; simply pluralize the relation name and append the ID to get the URI. Most
resources must have a parent - e.g. a Category
always belongs to a Customer
. These resources are created using
parent URLs (in this example POST /customers/{customer_id}/categories
), which will set the parent ID automatically.
It is not possible to update these parent relations.
Resources are created and updated by PUT
ing or POST
ing a JSON object with their properties to the appropriate
URL. The details of these objects can be found in our API specification. Because we interpret both PUT
and PATCH
as PATCH
, optional fields cannot be deleted by simply PUT
ing the resource without the field. We therefore
employ a different mechanism to do this: if you want to delete an optional field from a resource, you can PUT
or
PATCH
to the resource with a null
value for the field. This is interpreted as removing the field from the resource,
so you might get validation errors regarding the field being required if that action is not supported / allowed.
Resources are retrieved in a standard format similar to what is proposed by JSONAPI. The only difference is the way we specify our resource relations.
Let's say we do GET /expenses/1
. Assuming the resource is found, we get something like this:
{
"expenses": [{"id": 1, ...}],
"categories": [{"id": 1, ...}, ...],
...
}
The expenses
top level key will contain the requested expense. Other top-level keys will contain directly related
models of this expense, which are specified using {related}_id
fields on the expense or its amounts.
The return format of resource lists is very similar to that of single resources. Say we perform GET /currencies
,
we'll get something like this:
{
"meta": {"count": 115},
"currencies": [{id: 1, ...}, ...]
}
The only difference being the meta
top level key, which includes information about how many items could be returned
in total using this request. By default, resource lists are limited to 25 items at a time. You can specify a min
parameter in the query string to indicate the offset from which you want to retrieve items in the total list. A limit
parameter lets you specify the number of items you want to retrieve. This value has an upper limit, which is set at
200 unless specified otherwise.
Resources can be sorted by using the sort
query parameter. The syntax of this parameter is as follows:
field:direction,field2:direction,field3:direction,....
For instance
GET /accounts/99/expenses?sort=date:desc,id:asc
which sorts expenses by date descending. Different resources support different sorting fields. Specifying an
unsupported sorting order results in a 400 Bad Request
response.
Most list calls support some kind of filtering. Filters are specified using a filter string in a query parameter, for instance:
GET /accounts/99/expenses?filter=filterString
A filter string can consist of field/value pairs, free searches, logical conditions or both. In its most simple form, a filter string is just a search term
term
which will try to return relevant documents corresponding to that term. Most resource support searching through specific fields or with specific conditions, with the syntax:
field:value
At the top level, the filter string is split on whitespace. If you want to include whitespace in your filter strings, you can quote the values:
field:"value with spaces"
Logical conditions are also supported. This means you can either combine multiple fields,
field:("value 1" OR "value 2")
or combine field conditions:
field1:value OR field2:value
For a lot of fields, matching is fuzzy for values. If you want a field value to be matched exactly, you can prefix it with a plus sign:
field:+"exactly this"
You can also negate an entire search term:
field:-("value 1" OR "value 2")
To conclude, a filter string which uses a combination of the above looks like:
(field_a:-("value 1" OR "value 2") OR field_b:(+"value 1" OR "value 2")) "some other value"
Which means: look for the resources which match the search string "some other value", where field_a
does
not contain the term "value 1" or "value 2" and field_b
either equals "value 1" or contains "value 2".
Different resources use different fields / indicator terms, this is specified with the resource. Specifying
a malformed filter string, or a filter with unsupported fields results in a 400 Bad Request
status.
Successful requests result either in a 200 Ok
or 201 Created
status code. Unless specified otherwise, POST
requests return a 201, whereas any other request results in a 200.
If your requests contains malformed JSON you will receive a 400 Bad Request
, whereas posting with a different
content-type than "application/json" will result in a 415 Unsupported Media Type
error.
Requesting or posting to a resource that doesn't exist will return a 404 Not Found
. If you request a resource
that requires authorization without specifying a valid access key this will result in a 401 Unauthorized
.
If you have insufficient permissions to view the resource you requested this could result in either a
403 Forbidden
or 404 Not Found
- the latter of which can be returned instead of a 403 to prevent information leakage.
When creating or updating a resource with data invalid for that resource you will get a 422 Unprocessable Entity
,
accompanied with information about the errors in the request body.
The API call limit operates using a "leaky bucket" algorithm as a controller. This allows for infrequent bursts of calls, and allows your app to continue to make an unlimited amount of calls over time. The default bucket size is 40 calls (which cannot be exceeded at any given time), with a "leak rate" of 2 calls per second that continually empties the bucket. If your app averages 2 calls per second, it will never trip a 429 error ("Too Many Requests"). To learn more about the algorithm in general, click here.
Your API calls will be processed almost instantly if there is room in your "bucket". Unlike some integrations of the leaky bucket algorithm that aim to "smooth out" (slow down) operations, you can make quick bursts of API calls that exceed the leak rate. The bucket analogy is still a limit that we are tracking, but your processing speed for API calls is not directly limited to the leak rate of 2 calls per second.
Some things to remember:
You can check how many calls you've already made using the headers sent in response to your API call:
X-RateLimit-Limit
: The size of the bucketX-RateLimit-Remaining
: The number of API calls you can make before hitting the limitX-Retry-After
: The time in seconds before you can make another API requestKeep in mind that the number of remaining API call will increase over time. If you see you only have 10 calls remaining, and wait 10 seconds, you'll be up to 30 calls.