REST API
The REST API allows devices to access the TAG data.
All API paths (other than OAuth2):
MUST start with
/api/v{version}, where{version}at this stage must be5MUST end with
.json
We use a short notation of the API endpoints in this documentation, to get the actual URI of a resource just prefix the
domain name, the /api path and the version, like the examples below. This is a general rule that might change
depending on the project.
The current version of the API is v5. If a specific API or feature is available only for some versions, they will be
listed against it.
URI
Short Notation
/delegate/list
/delegate/{idType}:{id}
/delegate/new
https://example.satellite.co.nz/api/v5/delegate/send-comm.json
/delegate/send-comm
/device/list
/device/{idType}:{id}
/device/new
/device/ping
/device-config
https://example.satellite.co.nz/api/v5/interaction/list.json
/interaction/list
https://example.satellite.co.nz/api/v5/interaction/new/join.json
/interaction/new/{interactionType}
https://example.satellite.co.nz/api/v5/interaction/test/join.json
/interaction/test/{interactionType}
All APIs are available in our Insomnia Workspace.
Request Format
The system understand requests sent in JSON format, so you must always set the Content-type to application/json
in your request headers.
Images must be sent in base 64 format.
User Agent
The User-Agent should follow the convention below:
{app name}/{app version}/{device code} ({physical device user agent string})
app name: the name of the app, like “EventScanner”
app version: the version of the app, like “1.0.3”
device code: this is the code entered by the user during the device discovery phase, usually it’s the code printed on the back of the physical device
physical device user agent: this is the user agent that identifies the type of the physical device used (Android, iOS, Windows, etc.), the OS version, and other relevant information
Note
This User-Agent should be sent with all calls to the REST API.
Examples of valid User-Agent strings:
EventScanner/1.0.3/1799_LAB (SM-G965F; Android 10; Scale/2.63)
EventScanner/2.4/5002 (com.satellitemedia.SatelliteTagEventScanner; build:9; iOS 16.6.0)
Response Format
When the response is not empty, the server will return a JSON encoded response.
The response will always be an object with a data property and a meta property. This does not apply to our
OAuth2 endpoints, they will retain the standard behaviour and format for requests and responses. See
Authentication.
The data property contains the payload of the response, like a delegate, a list of devices, an interaction, or in
general the information requested. Each main object (delegate, interaction, device, etc.) will always have an id
and a _type property to uniquely identify it.
The main objects will always look the same, no matter what API endpoint has been called. They will always have the same properties, and this will depend only on the “scopes” defined for the client sending the request.
The only exception to this is when an object is “embedded” into another, in that case the embedded object will include
only a subset of the exposed properties. It will always provide id and _type. This is necessary to avoid
circular dependencies and to prevent a “response blow out” due to recursively embedding too many objects in the
returned data.
The meta property contains additional information. Some examples:
pagination: current page URI, next page URI, total object count
server errors: see below
validation errors: depending on the type of the request, this could be a list of errors that are preventing the posted data from being saved or processed correctly. Could be anything from missing fields, to wrong formats, to unexpected values.
{
"data": {
"id": 782,
"_type": "delegate",
"firstName": "John",
"lastName": "Smith",
"publicId": "d78790a6-0bb3-4ef7-8626-788bccd1ccd5",
"barcode": "5VSXNMQNGLDNRYBVBL",
"data": {
"Event": {
"area": "Zone 3",
"language": "en"
}
},
"parent": {
"id": 781,
"_type": "delegate"
}
},
"meta": {}
}
Response Status Code
The response status code is possibly the most important bit of a response because it can tell you at a glance if what
you asked for has been processed correctly or not. All API methods will return an appropriate HTTP status code ranging
between 200 and 431, but if something unforeseeable happens they might return an HTTP status code ranging
between 500 and 510.
Apart from the 500s errors, that sometimes cannot be handled correctly, all other error codes should be accompanied by
some more information in the meta property of the response.
Error Responses
An error response will look like this:
{
"data": {},
"meta": {
"error": {
"code": 404,
"message": "Not Found",
"internalCode": 601
}
}
}
- code
This matches the HTTP status code.
- message
Brief description of the error.
- internalCode
Some API methods return a specific internal code that uniquely identify a particular scenario. Sometime an error code is guarantee to be returned if a determined set of circumstances does happen.
Internal Codes Internal
HTTP
Name
Description
1001
503
Device Discovery Not Accepting
Device discovery is not accepting requests at this time. Access the control room or contact an administrator for help.
1002
409
Device Discovery Code Conflict
This code has been discovered already. Use a different code, or append something to it.
1301
404
Delegate Not Found
Cannot find the delegate for the specified ID (id, barcode, rfid, etc).
1401
404
Device Not Found
Cannot find the device for the specified ID (id, code, slug, etc).
1501
404
Interaction Not Found
Cannot find the interaction for the specified ID.
1502
403
Interaction Rejected on ACL
This interaction doesn’t satisfy the ACL rules. {rule description}
1503
400
Interaction Rejected on Received
This interaction was rejected before it was processed. {error message}
0
500
Unknown
An error occurred, please try again later. Contact an administrator if the problem persists.
Handling Errors
Shall I do something about an error?
In a nutshell, HTTP error codes will tell you if you have to take action to fix an error or if it’s a problem on the server side. All errors in the 400 range will require you to handle the error on your end, all errors in the 500 range tells you that something is not right on the server side, no matter what you do as a client.
The most common 400 are related to data validation, or conditions that are not satisfied. In most cases, prompting the app user to re-enter some details, re-scan a code, or check the validity of the data being sent will do the trick.
For most 500 errors, you will have to access the control room to make some changes, check the settings, or you will have to contact an administrator, producer or developer to investigate the issue.
When the unexpected does happen…
While it’s always a good idea to parse the response looking for the data you’ve requested or for an error message,
when the status code is a 5xx you might get back an HTML body or an empty body. This mainly happens when your call
does not even get to be processed because something unexpected happens before. As a rule of thumb you can try and parse
a 5xx response body, but be prepared to fail gracefully if that fails.
Common 5xx HTTP status codes are:
500 Internal Server Error
501 Not Implemented
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
505 HTTP Version Not Supported
Tip
The best way of dealing with an unexpected error is to back off for a while and then try again. The back off timeout should be tuned depending on the method you are calling. If it’s vital that your call gets processed ASAP 500ms or 1s could be good choices, if your call is not high priority you can put it at the end of your queue for later processing or wait a few seconds before trying again.
Lists
You can retrieve a list of delegates, devices or interactions. All lists can be filtered to fit your needs. You can
also pass the If-modified-since header to only download updates.
Lists always return deleted objects too.
Searching lists
You can filter the list of delegates, devices and interactions using crafted JSON object that you can include in the query string of your get request.
Each endpoint has a predefined set of properties you can search on. And all list endpoints have the same set of tests you can execute against those properties. Note that all string checks are case insensitive.
Test |
Symbol |
Operands |
Description |
|---|---|---|---|
Exact Match |
|
2 (p,v) |
The property must be an exact match against the given value. |
Partial Match |
|
2 (p,v) |
The property must contain the given value. |
Starts With |
|
2 (p,v) |
The property must start with the given value. |
Ends With |
|
2 (p,v) |
The property must end with the given value. |
Greater Than |
|
2 (p,v) |
The property must be greater than the given value. |
Greater Than or Equal |
|
2 (p,v) |
The property must be greater than or equal to the given value. |
Less Than |
|
2 (p,v) |
The property must be less than the given value. |
Less Than or Equal |
|
2 (p,v) |
The property must be less than or equal to the given value. |
In Set |
|
2 (p,v) |
The property must be one of the given values, or the value must be in the given property. |
Empty |
|
1 (p) |
The property must be null or an empty string. |
Not |
|
1 (t) |
Negates the test results. |
Or |
|
2+ (t,t,*) |
This test succeeds if any of the included tests is successful. |
And |
|
2+ (t,t,*) |
This test succeeds only if all included tests are successful. |
The tests above can be combined to achieve the desired filter.
Each test is a JSON object with a single property named to match the symbol of the test to perform, and its value is the list of operands. You can include a comment for reference, the comment will be ignored by the system but could be useful for debugging or logging purposes.
Generic search example:
{ "*=": ["firstName","john"] }
Same Generic search, but including a brief comment:
{
"*=": ["firstName","john"],
"comment": "Search all delegates with \"john\" in their first name"
}
Delegate type match example, to find all delegates of type “Crew”:
{ "==": ["delegateType","Crew"] }
Device type and parent match example, to find all devices with type “session”, children of the room with ID 89:
{ "and": [ { "==": ["deviceType","session"] }, { "==": ["parent.id",89] } ] }
All seats joined to a given delegate example, to get all “join” interactions against “seat” devices for the delegate with ID 32:
{ "and": [ { "==": ["deviceType","seat"] }, { "fi": ["join", {"firstDelegate": 32}, ">0"] } ] }
Complex query example:
{
"and": [
{">=": ["createdAt","2023-08-21 00:00:00"]},
{"<": ["createdAt","2023-08-22 00:00:00"]},
{
"or": [
{"^=": ["delegateTye","Crew "]},
{"^=": ["delegateTye","Delegate "]}
]
},
{"fi": ["join", {"firstDevice": 89}, ">="]}
],
"comment": "Fetch all delegate created within the given timeframe and joined to device 89, if their delegate type starts with \"Crew \" or with \"Delegate \""
}
Note
For most of the tests, see table above, the system expect the first operand to be the property name of the device,
delegate or interaction and the second property to be the value to compare it against. If that’s not the case, for
example the first operand is the value and the second is the property, you can define another property called ops
as an ordered array of strings where p indicates the corresponding operand is a property or v if the
corresponding operand is a value.
This is useful for example if you want to find all delegates where the last name could have been mistakenly stored against their first name too:
{"==":["firstName","lastName"],"ops":["p","p"]}
Pulling updates
If you want to pull only updates that happened from a certain point in time, you can add the If-modified-since
header to your GET request. The If-modified-since header should be the exact copy of the Date header in the
first response received when you started downloading the list.
For example, if you had to send more than one call to download the list of delegates, because it was paginated, the next
time you want to ask for updates, you should send the content of the Date header included in the response of your
very first call, or first page of results.
Note
This is a different interpretation of the If-modified-since header, it’s now part of our TAG HTTP dialect.
The response may contain entities that have been deleted “since”, and if you are filtering by interactions (for example delegates joined to a particular session), the response may contain also entities that haven’t been deleted but that don’t match your interaction filters any more.
In a nutshell, always look for the deletedAt property set to a date/time string, or for the filterMatch property set to false. If you find at least one of those two, you have to remove that record from your local database.
Example of a deleted delegate:
{
"id": 710,
"_type": "delegate",
"delegateType": "Delegate",
"deletedAt": "2023-07-07T08:54:13+12:00"
}
Example of a delegate that does not match the interaction filter any more:
{
"id": 710,
"_type": "delegate",
"delegateType": "Delegate",
"filterMatch": false
}
Barcode
A delegate barcode is generated according to a custom algorithm created to guarantee:
uniqueness: if all the guidelines and conditions are met, codes might repeat after 5.6 years from the epoch start. This should give plenty of time since all our projects so far ran over a period of few hours to 6 months maximum.
order: the generated codes are ordered based on the time they were generated with an accuracy of few milliseconds if all clocks on the clients are in sync, if they aren’t a few seconds of accuracy or more has to be expected.
readability: codes related to the same group (or purchase in case of tickets) are easily identified because the share the same base (the first 12 characters of the barcode). Also, codes that belong to a sequence can be ordered or counted using the last two characters.
strength: codes are protected by a check-sum that can detect (but not correct) tampering. The chances of generating a valid barcode without knowing the secret are negligible for our use cases.
information rich: by analysing the barcode we can extrapolate meaningful data like:
time of the barcode generation, with an error of ±3 minutes
if it’s been generated by the server or a client
which client generated it (if generated by a client)
validity
position in a sequence (if the barcode belongs to a group or sequence)
Barcode Parts
Each barcode can be seen as made of three components:
base: the first 12 chars. The base doesn’t change within the same group or sequence or purchase;
index: the last 3 chars, or 15 bits. They represent the position of this barcode in the sequence;
check: the 3 chars between the base and the index, 15 bits. This is a check-sum that signs the “base + index” using a secret shared only among the server and clients.
The base is composed of (in order):
days: 11 bits (2 chars + 1 bit)
minutes: 9 bits (1 char and 4 bits)
milliseconds: 15 bits (3 chars)
use epoch: 1 bit
random: 9 bits (1 char and 4 bits)
server: 1 bit
seed: 14 bits (2 chars and 4 bits)
See the below table for more details on the barcode parts.
Char |
Bits |
Part |
Description |
|---|---|---|---|
0 |
0-4 |
Days |
Number of days since the epoch. Because of the 10 bit size, this will repeat approximately every 5.6 years. |
1 |
5-9 |
||
2 |
10 |
||
11-14 |
Minutes |
Number of minutes today, 3 minutes approximation. |
|
3 |
15-19 |
||
4 |
20-24 |
Milliseconds |
Number of milliseconds in the current 3 minutes frame, 5ms approximation. This helps keeping the barcodes ordered by creation time and allows generating 182 “ordered” barcodes per second. The number of barcodes that can be generated per second is actually much higher (in the order of 10¹⁸/s with the maximum number of clients generating barcodes simultaneously). |
5 |
25-29 |
||
6 |
30-34 |
||
7 |
35 |
Use Epoch |
This is 0 if the barcode has been generated without an epoch. The epoch is not stored in the barcode. |
36-39 |
Random |
This is a random number between 0 and 512. |
|
8 |
40-44 |
||
9 |
45 |
Server |
This is 1 if the barcode has been generated on the server. |
46-49 |
Seed |
This is the seed that identifies the device that generated the barcode. |
|
10 |
50-54 |
||
11 |
55-59 |
||
12 |
60-64 |
Check |
These are 15 check bits to prevent tampering or third party barcode generation. The system secret is required to generate a valid “check”. |
13 |
65-69 |
||
14 |
70-74 |
||
15 |
75-79 |
Index |
When grouping barcodes, for example tickets of the same purchase, this field allow to index them. This can index from 1 to 803. First 99 barcodes are suffixed with 00-99, after that the entire pool of character is used and the following indexes will look like: A0, A1, A2, … , Z9, ZA, ZB, … , ZX, ZY, ZZ. |
16 |
80-84 |
||
17 |
85-89 |
Barcode Generation
Using a common interface for the barcode generation across devices and programming language can help keeping the documentation consistent and improve the understanding of the internals.
The current API can be summarised as follows, using pseudo code:
/**
* Generate a new base for this client or server.
*
* @Parameter "seed" The client seed, must be unique per client
* @Parameter "epochStart" Unix timestamp of the midnight of the first day for the barcode generation, or -1 for a random epoch
* @Parameter "history" Previously generated bases, it's recommended that these are at least those generated in the past 3 minutes
* @Parameter "serverGenerated" Should be true only on the server
* @Return The array of bits for the base just generated
*/
function generateBase(int seed, int epochStart = -1, bit[][] history = [], bool serverGenerated = false): bit[]
/**
* Generate a new barcode for the given index and base, signed with the given secret.
*
* @Parameter "index" The position of this barcode in a sequence, or zero if the barcode doesn't belong to one
* @Parameter "secret" The shared secret to sign the barcode check-sum with
* @Parameter "base" The base to use for the barcode generation, or a random one if not provided
* @Return The array of bits for the barcode just generated
*/
function generateBarcode(int index = 0, string secret = '', bit[] base = []): bit[]
/**
* Generate a new barcode signed with the given secret.
*
* @Parameter "secret" The shared secret to sign the barcode check-sum with
* @Return The array of bits for the barcode just generated
*/
function generateSecure(string secret = '')
The reference implementation is in PHP, but we have a JavaScript implementation too.
Upgrading from v2 to v5
If you are migrating your app from v2 to v5, this is a quick reference to find the equivalent endpoint:
v2 |
v5 |
Notes |
Methods |
|---|---|---|---|
/delegate/cache |
/delegate/list |
can filter |
GET |
/delegate/list |
/delegate/list |
can filter |
GET |
/delegate/new |
/delegate/new |
POST |
|
/delegate/{idType}:{id} |
/delegate/{idType}:{id} |
GET, PATCH, DELETE |
|
/delegate/send-comm |
/delegate/send-comm |
POST |
|
/{deviceType}/cache |
/device/list |
can filter |
GET |
/{deviceType}/list |
/device/list |
can filter |
GET |
/{deviceType}/new |
/device/new |
deviceType in the body |
POST |
/{deviceType}/{idType}:{id} |
/device/{idType}:{id} |
GET, PATCH, DELETE |
|
/questionnaire/list |
/device/{idType}:{id} |
questionnaire in data |
GET |
/device/{idType}:{id}/ping |
/device/ping |
POST |
|
/config/summary |
/device/ping |
GET v2 / POST v5 |
|
/device-interaction/list |
/interaction/list |
GET |
|
/{deviceType}/log/{interactionType} |
/interaction/new/{interactionType} |
POST |
|
/device/log/{interactionType} |
/interaction/new/{interactionType} |
POST |
|
/device/seen |
/interaction/new/seen |
POST |
|
/lead/new |
/interaction/new/lead |
POST |
|
/device/log-batch |
/interaction/batch |
POST |
As you can notice, just from this incomplete list of v2 endpoints, they have been reduced in number (in this list they change from 18 to 11).
Notable differences are:
the “cache” calls for delegates and devices have been removed, the equivalent “list” APIs have to be used instead. The “cache” APIs have caused problems like code duplication, incompatible sets of features between “cache” and “list” calls, and inconsistent output;
the “config summary” call has been dropped in favour of including the configuration object in the output of the “ping” API, thus allowing an admin to update the configuration of our apps remotely, each time an app pings the server.
In v5 we are trying to guarantee the shape of the main objects (delegates, devices, interactions, discovery and configuration) so that it’s easier to maintain and upgrade them.