homedocsapi
API | putilov_denis - stock.adobe.com

Introducing the Puzzle API

An introduction to the interface of the Puzzle backend for developers.

Dec. 21, 2020|Bernhard Kauer
developerwork-in-progress

1. Overview

The Puzzle API allows one to interface with the Puzzle backend directly without going through the Puzzle App. In fact, this is the very same programming interface the App is using. In the following I will introduce the basic concepts. More details will be covered in follow-up articles.

Be aware that neither the backend, its interface, nor this documentation are in a final state, yet. Instead they are actively worked on and modified every other day. Any consistency between implementation and documenation should therefore not be assumed.

It is up to you, the reader, to improve this documentation by reporting back on discrepancies, omissions, errors or even hard to comprehend language, so that every other developer will benefit from your insights. If something should be additionally covered in this document let us know as well. Thank you for your support!

1.1 Recent Changes

The following parts of this document have been updated recently:

2. REST API

The API design follows the REST principles. Especially it assumes:

2.1 URL Prefix

All URLs start with the same prefix:

This value could change when incompatible versions of the API are published in the future. However, using an accept header to indicate the supported API version is currently the prefered choice.

There is no need to hard-code any URL in a program except this prefix, since all other URLs can be extracted from the responses of the service.

2.2 HTTP/2

The API is accessed over HTTP/2 with transport encryption provided by TLS. Using HTTP/2 only enables:

Accessing the service with an older HTTP version leads to a 400 Bad Request error:

$ curl --http1.1 -s -w "%{http_code} HTTP/%{http_version}\n"  https://$HOST/pzl

{"reason": "not accessed via http/2"}
400 HTTP/1.1

2.3 Transport encryption

Transport encryption is provided by TLS. The server supports TLS v1.3. Older versions below 1.2 are unsupported:

$ curl -s -S --tls-max 1.1 https://$HOST 2>&1

curl: (35) error:141E70BF:SSL routines:tls_construct_client_hello:no protocols available

Note that accessing the backend over TLSv1.2 is deprecated. It will be disabled when more tools are converted to the newer and more secure versions.

The server certificate is issued by Let’s Encrypt:

$ curl -v https://$HOST  2>&1 | grep issuer

*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3

The CAA DNS records points to Let’s Encrypt as well.

$ host -t CAA $HOST | cut -d' ' -f 3-

CAA record 128 issue "letsencrypt.org"

To avoid man-in-the-middle attacks, it is up to the client to correctly verify the server certificate.

2.4 Authentication

Digital signatures are employed to authenticate the client against the server. We utilize the ed25519 public-key signature scheme for this purpose since it is:

Dozens of libraries can calculate ed25519 signatures, for instance:

Since ed25519 is a public-key scheme, the private key must never leave the device it was generated on. The backend supports multiple public-keys per user account to enable access from multiple devices and for backup purposes.

A detailed description of the Puzzle authentication scheme is available in another article.

3. HTTP Requests

3.1 Methods

The following HTTP methods are used for the API:

If another HTTP method, like PUT is called, a 501 error will be returned.

$ curl -X PUT -s -w "%{http_code}\n"  https://$HOST/pzl

501

Not all methods are implemented for all URLs. A 404 error will be returned if no handler is found for the given URL.

$ curl -s -w "%{http_code}\n"  https://$HOST/pzl

404

3.2 Headers

The following HTTP request headers are understood:

The following HTTP headers may be send in a reply:

Request parameters must be given in the body in JSON format encoded as UTF-8.

Creation

To create new objects all mandatory parameters must be given. These are the ones that cannot be changed afterwards or that are needed to continue. Adding a new main account for instance just requires a currency, as this value is fixed during the lifetime of the object.

{"currency": "EUR"}

Registering a new user needs the name of an environment where this user should be placed. Furthermore a public key to authorize further requests is required.

{"env": "sandbox", "keytype": "ed25519", "pubkey": "ugx7f8f2JIqXjlxyhZcPk_Tgkc1reR_YBrKijRzAaHg="}

Updates

Updates have only optional parameters. To change the name and the description of an account one sends something like:

{"name": "Shoes", "description": "for buying red shoes at various internet shops"}

Retrievals

Object retrievals with GET and HEAD do not need parameters and therefore have no body. Similarly the OPTIONS calls are without any parameter.

3.4 Options and Choices

Some parameters vary through time and even between users. The OPTIONS method should be used to achieve a certain degree of forward-compatibility.

One example is the currency of an account which might depend on the country the user is located in. To get a list of these choices, one may utilize the OPTIONS method before performing the creation or update call.

$ puzzle-client  -q options /pzl/s3e8/accounts

{
 "choice":{
  "currency":[
   "EUR",
   "GBP",
   "JPY",
   "PLN",
   "PP",
   "USD"
  ]
 },
 "mandatory":[
  "currency"
 ],
 "optional":[
  "description",
  "icon",
  "name"
 ]
}

The reply also lists the mandatory and optional parameters. For the user-info the result would be:

$ puzzle-client  -q options /pzl/s3e8/info

{
 "optional":[
  "description",
  "icon",
  "name"
 ]
}

There might be optional services available to the user. An OPTIONS call reveals them as well:

$ puzzle-client  -q options /pzl/s3e8

{
 "service":[]
}

3.5 Long Polling

Polling for asynchronous updates is an inefficient approach. One either wastes resources by checking to often or updates might be seen relatively late. To optimize this task, the backend supports long-polling.

An request that would normally directly answered with an empty 304 reply, now includes a small body. The headers are still send immediately, to make the client aware the server has accepted the request, but the body will be delayed upto 60 seconds. The body will be send earlier if the underlying user object is modified. If the 60 second timeout occurs the whole connection is closed. The client is expected to repeat the operation in any case, as spurious wakeups can happen.

A client needs to provide the following to enable the long-polling feature:

  1. The request must address a current link.
  2. Specify the etag of the current version in the if-none-match header.
  3. Include a prefer header with the value wait.

If one violates the first requirement, a 400 error is returned:

$ ETAG=$(puzzle-client get /pzl/s3e8  | grep etag | cut -c 9-); puzzle-client -q get /pzl/s3e8.$ETAG -H "if-none-match: $ETAG" -H "prefer: wait"

{
 "reason":"not a current link"
}

3.6 Error Codes

The following HTTP error codes will be returned on a request.

code method reason
200 GET ok
200 HEAD ok
200 POST object updated or created
200 DELETE object deleted
201 POST attachment created
204 POST object was not modified
304 GET object still unmodified
304 HEAD object still unmodified
400 * invalid parameters
401 * authentication failed
404 * object or handler not found
412 DELETE object not current anymore
412 POST object not current anymore
500 * bug in the backend
501 * method is not implemented

A reply to a GET or POST request with return code 200 has as body the object in JSON format. It may also return the requested file, if an attachment was addressed.

The errors 400 and 401 return a reason string as JSON body. Examples are:

400: {"reason": "invalid JSON"}
400: {"reason": "invalid currency"}
401: {"reason": "expired"}
401: {"reason": "invalid signature"}

All errors at 400 and above might return a numeric code as well. This can be used during debugging sessions for fine-grained error matching in the backend.

401 {"code": 198, "reason": "authorization missing"}
404 {"code": 118}

4. Object Representation

Objects are retrieved in JSON format. Getting a user returns something like:

$ puzzle-client -q get  /pzl/s3e8

{
 "ancient":"/pzl/s3e8.g19jx9fU_xQSfh37WOAemMW2Evod3nWb6HRbH71d_5W2",
 "canonical":"/pzl/s3e8.g9iXKsn0EheFtGmGLFkgbJSagKqGvxCy_LhbwJ62ZJNY",
 "children":{
  "accounts":"gyAmcPE0D0cR0hpfZ62Z-V7STyvn1XUO344L4oS0FhmF",
  "auths":"g4t-iMZaQNvSAV2FqptiKlkaFxFLPKggH_nPoamd85Wx",
  "info":"g1ZB1MD5TXNnRkZJ3DKUV2UboMJJ7GswSia_GTjaJ-Ec"
 },
 "current":"/pzl/s3e8.AK1aUhxbqNr-dL_HKGfxIFU7GHMuB6Ug",
 "event":{
  "author":"puzzle-service api <267976@n18-1>",
  "category":"user",
  "date":1608582480.928915,
  "name":"s:3e8",
  "operation":"merge",
  "subcategory":"account",
  "versions":"0-0"
 },
 "prev":"/pzl/s3e8.g19jx9fU_xQSfh37WOAemMW2Evod3nWb6HRbH71d_5W2",
 "type":"user",
 "version":1
}

The canonical and current fields are present in all objects, as they enable versioning. The type field is also mandatory. All other fields are optional. For instance the first version of an object has no prev link and a leaf in the object tree will have no children.

The children, prev and ancient links are used for navigation in the tree of objects. They are not always present. The version field can be used to sort versions of a certain object in time.

The event fields are only present in user and account objects.

4.1 Object Types

The type attribute allows to distinguish different object classes. The following types are currently defined.

Name Usage
user highest level object
user-info details like the name of the user
auth authentication information like the public key
auth-list list of auths
account accounts holding money
account-list list of accounts
account-info details like the name of the account
outgoing outgoing transaction
outgoing-list list of outgoing transactions
incoming incoming transaction
incoming-list list of incoming transactions
file-list list of attachments

4.2 Versions

All objects are versioned. Whenever any detail changes, a new version is created. The version attribute gives an monotonically increasing, albeit sparse, number that can be used to sort object versions in time.

The canonical attribute defines a path that never changes. The tag in the canonical link is a hash over the object state and unique over time. This is similar to the commit ID in a GIT repository. Requesting the canonical path returns exactly the very same object in the future, even if the object is marked as deleted in a newer version.

The prev attribute, if present, links to the previous version of this object. This enables backward iteration of the versions. The ancient attribute, if present, points to some older version of the object. This enables a binary search for a particular version. The initial version of the aforementioned user object is:

$ puzzle-client -q get --version=0 /pzl/s3e8

{
 "canonical":"/pzl/s3e8.g19jx9fU_xQSfh37WOAemMW2Evod3nWb6HRbH71d_5W2",
 "children":{
  "accounts":"g4Krr2gT1hqmVBirI5tCGY-9f3yULCRpIrJhd54Pga8G",
  "auths":"g4t-iMZaQNvSAV2FqptiKlkaFxFLPKggH_nPoamd85Wx",
  "info":"g1ZB1MD5TXNnRkZJ3DKUV2UboMJJ7GswSia_GTjaJ-Ec"
 },
 "current":"/pzl/s3e8.AK1aUhxbqNr-dL_HKGfxIFU7GHMuB6Ug",
 "event":{
  "author":"puzzle-service api <267976@n18-1>",
  "category":"user",
  "date":1608582204.209815,
  "name":"s3e8",
  "operation":"init"
 },
 "type":"user"
}

The current attribute can be used to retrieve the newest version of an object. One can use this path together with an if-none-match header to implement efficient long-polling.

4.3 Children

An object may have children. The attribute defines a mapping between child names to their etag. By comparing two different versions of an object, one can therefore deceide whether a child was modified or not.

To retrieve the child, one combines the name of the child with the canonical or current path of the parent. The former gives a consistent view on the whole state at a certain version, whereas the later includes newer updates.

The list of accounts for instance looks like:

$ puzzle-client -q get /pzl/s3e8/accounts

{
 "canonical":"/pzl/s3e8/accounts.gyAmcPE0D0cR0hpfZ62Z-V7STyvn1XUO344L4oS0FhmF",
 "children":{
  "s:3e8":"g6osu_EUBRpNut6QQeIfGhnZ0wMl0ueFFMD2koHz8TXz"
 },
 "current":"/pzl/s3e8.AK1aUhxbqNr-dL_HKGfxIFU7GHMuB6Ug/accounts",
 "prev":"/pzl/s3e8.g19jx9fU_xQSfh37WOAemMW2Evod3nWb6HRbH71d_5W2/accounts",
 "type":"account-list",
 "version":1
}

4.4 Events

The event attribute of an object looks like:

$ puzzle-client -q get --version=1 /pzl/s3e8 | jshon -S -e event

{
 "author": "puzzle-service api <267976@n18-1>",
 "category": "user",
 "date": 1608582480.928915,
 "name": "s:3e8",
 "operation": "merge",
 "subcategory": "account",
 "versions": "0-0"
}

One can see that at a certain date, the versions 0-0 from the account named s:3e8 were merged into this user repository. The merge was performed by a certain author.

Not all objects have event attributes. Currently, only the users and the accounts have them.

4.5 Properties

Certain objects have properties. Every main account for instance has a balance:

$ puzzle-client -q get  /pzl/s3e8/accounts/s:3e8

{
 "canonical":"/pzl/s3e8/accounts/s:3e8.g6osu_EUBRpNut6QQeIfGhnZ0wMl0ueFFMD2koHz8TXz",
 "children":{
  "incoming":"g6ZlJeFf6JEMCv6HI0lc5gRsgpyhppwkOMxV9JXTMcvZ",
  "info":"g3wcaW-G37dJBi_kSPBElBoBkwCLNKNgLuBzxKdE7K9K",
  "outgoing":"g8QOHE-Ob21ZPbS5jZax07Jjg3ARMq88ALyN3CRH8tDA"
 },
 "current":"/pzl/s3e8.AK1aUhxbqNr-dL_HKGfxIFU7GHMuB6Ug/accounts/s:3e8",
 "event":{
  "author":"puzzle-service api <267976@n18-1>",
  "category":"account",
  "date":1608582480.926723,
  "name":"s:3e8",
  "operation":"init"
 },
 "properties":{
  "balance":{
   "available":"0",
   "fee":"0",
   "reserve":"0",
   "volume":"0"
  }
 },
 "type":"account"
}

A property can be a plain value or may have sub-fields. These are the sub-fields for the account->balance:

5. Traces

This section contains several traces of HTTP request and responses. Note that any JSON is pretty printed, so neither the content-length fields nor the signatures are valid anymore.

5.1 GET request

GET requests need an authorization.

$ puzzle-client -d get  /pzl/s3e8/auths

> HTTP/2 200
> cache-control: no-cache
> content-length: 230
> content-type: application/json
> date: Mon, 21 Dec 2020 20:28:23 GMT
> etag: g4t-iMZaQNvSAV2FqptiKlkaFxFLPKggH_nPoamd85Wx
> 
> {
>  "canonical": "/pzl/s3e8/auths.g4t-iMZaQNvSAV2FqptiKlkaFxFLPKggH_nPoamd85Wx",
>  "children": {
>   "x1": "gwIreKIDYGsoAZW4DbAmKwZNhh-zMyXTqWmKqaQxRn-v"
>  },
>  "current": "/pzl/s3e8.AK1aUhxbqNr-dL_HKGfxIFU7GHMuB6Ug/auths",
>  "type": "auth-list"
> }

5.2 OPTIONS request

OPTIONS need a signature as well.

$ puzzle-client -d options  /pzl/s3e8/info

> HTTP/2 200
> content-length: 46
> content-type: application/json
> date: Mon, 21 Dec 2020 20:28:23 GMT
> 
> {
>  "optional": [
>   "description",
>   "icon",
>   "name"
>  ]
> }

Without authorization a 401 is returned.

$ puzzle-client  -d -q options /pzl/s38/auths

{
 "reason":"authorization missing"
}

The only exception to this rule is the OPTIONS call to the entry point.

$ curl -X OPTIONS -s https://$HOST/pzl

{"choice": {"keytype": ["ed25519"]}, "mandatory": ["env", "keytype", "pubkey"], "optional": ["description", "icon", "name"]}

5.3 UPDATE request

Unknown parameters of an UPDATE are ignored.

$ echo "info foo=bar" | puzzle-client  -d update /pzl/s3e8 2>&1

< POST /pzl/s3e8.AK1aUhxbqNr-dL_HKGfxIFU7GHMuB6Ug/info HTTP/2
< authorization: pzl time=1608582504+60,sig=1R3SmbT6pLDEOUsseLAZTUsSR6t25RCFEYKXsPHmMayemeyUa996jZixDBBmDbHHsSIia4SEYf4TfBFAG21MDQ
< content-type: application/json
< 
< {"foo":"bar"} 

> HTTP/2 204
> date: Mon, 21 Dec 2020 20:28:24 GMT
>
Interest?
tell@puzzle2pay.com