10 API
This chapter is dedicated to describe the web API to interact with the Sen2Cube.AT backend.
The API follows the JSON:API specification. This document will only describe the basics with examples. For full documentation see JSON:API specification documentation. The implementation used in the Sen2Cube.AT backend is flask-rest-jsonapi. Especially the secion on filtering is important as this is to a certain degree implementation specific and not completely described within the standard.
10.1 Authentication
All API endpoints need authentication with OAuth2. The OAuth2 server is https://auth.sen2cube.at. We recommend using an
OAuth client library provided for your language environment / framework and not handling the authentication by yourself.
The following snippets only illustrate the steps needed for authentication from the commandline. They assume a
Linux / Bash environment and use jq
to extract data from JSON.
NOTE To keep your password out of the command history create a file s2c_pwd.txt containing your password without trailing line break and store it in a save location. Don’t forget to delete the file when you’re done!
For convenience we create a variable with our username.
10.1.1 Requesting a token
To request a session token we send our credentials to the OAuth token endpoint and store the result in a file called
.token.json
.
NOTE Make sure to delete this file when you’re done.
curl -X POST "https://auth.sen2cube.at/realms/sen2cube-at/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=${S2C_UN}" \
-d "password=$(cat ./s2c_pwd.txt)" \
-d 'grant_type=password' \
-d "client_id=iq-web-client" > .token.json
This returns a JSON structure containing the token and a refresh token cat .token.json
:
{
"access_token": "<token string>",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "<refresh token string>",
"token_type": "bearer",
"not-before-policy": 1597324965,
"session_state": "86c402ad-aff5-4e95-aa0b-f04fdbad7f09",
"scope": "email profile"
}
We can extract the token and refresh token with jq
and save them into files for convenience.
NOTE Make sure to delete those files when you’re done.
10.1.2 Using the token / getting user info
The token must be added as Authorization
header to the http requests. For example to get the user info of the
current user:
curl -X GET \
"https://auth.sen2cube.at/realms/sen2cube-at/protocol/openid-connect/userinfo" \
-H "Authorization: Bearer $(cat .access_token.txt)"
This returns a JSON of the following format:
{
"sub": "<SNIP>",
"email_verified": true,
"name": "User Name",
"preferred_username": "user.name",
"given_name": "User",
"family_name": "Name",
"email": "user.name@mail.invalid"
}
The preferred_username
field should match the username used for requesting the token.
10.1.3 Refreshing tokens
By default tokens are valid for 5 minutes (300s). To refresh the session without using
username / password the refresh token can be used. The request is simliar to the initial
token request but with grant_type
set to refresh_token
and the refresh token instead
of username and password. Refresh tokens are valid for 30 minutes (1800s).
curl -X POST "https://auth.sen2cube.at/realms/sen2cube-at/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=$(cat .refresh_token.txt)" \
-d "client_id=iq-web-client" > .token.json
This returns a new set of tokens that can be extracted as shown above.
cat .token.json | jq -r '.access_token' > .access_token.txt
cat .token.json | jq -r '.refresh_token' > .refresh_token.txt
NOTE Most client libraries should provide a mechanism for automatic token refresh. This should be used whenever possible.
10.2 JSON:API Basics
This section shows very basic examples of how to fetch and create entities. For more complex examples please check JSON:API specification and flask-rest-jsonapi documentation.
10.2.1 Fetching entities
Every resource has a unique URI. A resource is fetched by using an HTTP GET request.
Note Every request against https://api.sen2cube.at needs the Authorization: Bearer <valid access token>
header
to be set or it will fail with a 401 Unauthorized error.
Get single entity with known ID (example ID 10424):
curl -X GET "https://api.sen2cube.at/v1/inference/10424" \
-H "Authorization: Bearer $(cat .access_token.txt)"
Get multiple entities using paging / filters etc. This example fetches all inferences in pages of size 1:
curl -X GET "https://api.sen2cube.at/v1/inference?page[size]=1" \
-H "Authorization: Bearer $(cat .access_token.txt)"
This returns a JSON simliar to this (snipped the inference data for brevity). See below for data structure.
{
"data": [ // array containing the fetched entities.
// in case of getting a single entity via ID data is not an array
// but just the object.
{
// snipped inference data for brevity (see below for format)
}
],
"links": {
"self": "http://api.sen2cube.at/v1/inference?page%5Bsize%5D=1&filter%5Bowner%5D=user.name",
"first": "http://api.sen2cube.at/v1/inference?page%5Bsize%5D=1&filter%5Bowner%5D=user.name",
"last": "http://api.sen2cube.at/v1/inference?page%5Bsize%5D=1&filter%5Bowner%5D=user.name&page%5Bnumber%5D=23",
"next": "http://api.sen2cube.at/v1/inference?page%5Bsize%5D=1&filter%5Bowner%5D=user.name&page%5Bnumber%5D=2"
},
"meta": {
"count": 23
},
"jsonapi": {
"version": "1.0"
}
}
All results follow this structure. The links
can be used to step through pages, the meta/count
field shows the
total number of results matching the query and the data
array contains the requested data.
10.2.2 Creating entities
To create an entity - currently only inferences and models can be created via API - the entity has to be send via POST
request to the corresponding endpoint. This will example will create an inference that will be executed by the backend.
We create a file inference.json
and POST
it to th endpoint https://api.sen2cube.at/v1/inference
Example inference.json
:
{
"data": {
"attributes": {
"owner": "user.name", // username of current user.
// **NOTE** Because of a known BUG at the moment
// this will not be autofilled and needs
// to be the correct preferred username!
"comment": "Inference started via CURL",
// temporal range. Both need to be ISO timestamps and SHOULD be UTC
"temp_range_start": "2021-03-01T00:00:00.000Z",
"temp_range_end": "2021-07-31T23:59:59.999Z",
// area of interet. Needs to be a GeoJSON provided as STRING!
"area_of_interest": "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"name\":\"Area-of-interest 00\"},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[14.07188,47.768556],[14.07188,47.865185],[14.237462,47.865185],[14.237462,47.768556],[14.07188,47.768556]]]}}]}",
// downsample factbase resolution by this factor. Can be used
// for quick preview. This resamples on data load, not on
// result generation!
// 1 = factbase resolution
"output_scale_factor": 10,
"favourite": false
},
"relationships": {
// ID of the knowledgebase model to execute
"knowledgebase": {
"data": {
"type": "knowledgebase",
"id": "218"
}
},
// ID of factbase to execute against.
"factbase": {
"data": {
"type": "factbase",
"id": "1"
}
}
},
"type": "inference" // For inference endpoint always 'inference'
}
}
curl -X POST "https://api.sen2cube.at/v1/inference" \
-H "Authorization: Bearer $(cat .access_token.txt)" \
-H "Content-Type: application/json" \
-d "$(cat ./inference.json)"
On successful creation the server will return a 201 CREATED
with the following body (containing all optional fields). The field id
contains the object ID.
{
"data": {
"type": "inference",
"attributes": {
// Object attributes are snipped for brewety
},
"id": 10424,
"relationships": {
"factbase": {
"links": {
"self": "/v1/inference/10424/relationships/factbase",
"related": "/v1/inference/10424/factbase"
}
},
"knowledgebase": {
"links": {
"self": "/v1/inference/10424/relationships/knowledgebase",
"related": "/v1/inference/10424/knowledgebase"
}
}
},
"links": {
"self": "/v1/inference/10424"
}
},
"links": {
"self": "/v1/inference/10424"
},
"jsonapi": {
"version": "1.0"
}
}
10.3 Endpoints and object structures
10.3.1 Inference
Inferences are the central object for interacting with the backend. They schedule models from the knowledgebase to be executed against a factbase.
Endpoint: /v1/inference
The basic flow for running an inference is
- Select a model from the knowledgebase (see below)
- create a GeoJSON with the area of interest
- pick a temporal range
- create JSON and POST to inference endpoint
- Then wait for the backend to schedule and execute the inference (need polling at the moment)
- retrieve results when status changes to SUCCESS
Datamodel
{
"data": {
"type": "inference", // always "inference"
"attributes": {
"owner": "user.name", // owner
"comment": " ", // description or comment on inference
"status": "SUCCEEDED", // status ABORTED|CREATED|FAILED|SCHEDULED|STARTED|SUCCEEDED
"status_message": "The inference was successfully processed",
"timestamp_created": "2021-09-24T11:41:30.498496+00:00", // timestamp of creation
"timestamp_started": "2021-09-24T12:13:17.527622+00:00", // timestamp when processing started
"timestamp_finished": "2021-09-24T12:15:48.661768+00:00", // timestamp when processing finished
"factbase_id": 1, // id of used factbase (see relationships for link)
"knowledgebase_id": 218, // id of used model in knowledgebase (see relationships for link)
// area of interest to run model on. GeoJSON as STRING
"area_of_interest": "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"name\":\"Area-of-interest 00\"},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[14.07188,47.768556],[14.07188,47.865185],[14.237462,47.865185],[14.237462,47.768556],[14.07188,47.768556]]]}}]}",
// temporal range to query data from
"temp_range_start": "2021-03-01T00:00:00+00:00",
"temp_range_end": "2021-07-31T23:59:59.999000+00:00",
// scalingfactor for resolution. 1 = original factbase resolution.
// rescaling happens on data load - used for creating quick previews
"output_scale_factor": 10,
// array of outputs as JSON string (see below for more information)
"output": "[{\"name\": \"Cloud_free_composite\", \"inference_id\": 10424, \"value_type\": \"numerical\", \"value_range\": [354.0, 4463.0], \"dims\": [\"band\", \"y\", \"x\"], \"file_type\": \"geotiff\", \"vis_type\": \"composite\", \"data\": \"/output/sen2cube/user.name/Cloud_free_composite_id10424_3035.tiff\", \"bytes\": 106111, \"band_value_ranges\": [[354.0, 4463.0], [602.0, 3601.0], [764.0, 3229.0]]}]",
"qgis_project_location": "/output/sen2cube/user.name/qgis-project-id10424.zip",
"favourite": false, // mark inference as favourite (for UI)
// internal
"status_timestamp": null,
"status_progress": null,
},
"relationships": {
"knowledgebase": {
"links": {
"self": "/v1/inference/10424/relationships/knowledgebase",
"related": "/v1/inference/10424/knowledgebase"
}
},
"factbase": {
"links": {
"self": "/v1/inference/10424/relationships/factbase",
"related": "/v1/inference/10424/factbase"
}
}
},
"id": 10424,
"links": {
"self": "/v1/inference/10424"
}
},
"links": {
"self": "/v1/inference/10424"
},
"jsonapi": {
"version": "1.0"
}
}
The output
field contains a JSON string with an array of all results contained within the inference. The results are
defined in the knowledgebase model and vary from model to model. The basic structure is:
[
// example of GeoTIFF result
{
"name": "Cloud_free_composite", // name of the result
"inference_id": 10424, // ID of the inference
"value_type": "numerical", // datatype (numerical|categorical)
"value_range": [ // min and max value over all bands
354,
4463
],
"dims": [ // dimensions
"band",
"y",
"x"
],
"file_type": "geotiff", // geotiff|csv
"vis_type": "composite", // for UI purposes
// path to download. This needs to be prefixed with URI from the factbase (see below)
// that this inference was executed agains. In this case https://demo.sen2cube.at.
"data": "/output/sen2cube/user.name/Cloud_free_composite_id10424_3035.tiff",
"bytes": 106111, // filesize
"band_value_ranges": [ // min/max values per band
[
354,
4463
],
[
602,
3601
],
[
764,
3229
]
]
},
// example of a simple timeseries on one polygon
// resulting CSV has two columns: time and cloud_percentage_aoi
// containing the value
{
"name": "cloud_percentage_aoi",
"inference_id": 10424,
"value_type": "numerical",
"value_range": [
0,
100
],
"dims": [
"time"
],
"file_type": "csv",
"vis_type": "single_line_graph",
"data": "/output/sen2cube/user.name/cloud_percentage_aoi_id10424.csv",
"bytes": 1053,
"x-axis": "time",
"y-axis": "cloud_percentage_aoi"
},
// example with multiple polygons
// resulting CSV contains 3 columns.
// space contrains the polygonname (set by property 'name' in GeoJSON)
// time and value.
{
"name": "pre_event_mowing_cloudless",
"inference_id": 10424,
"value_type": "numerical",
"value_range": [
0,
100
],
"dims": [
"space",
"time"
],
"file_type": "csv",
"vis_type": "multi_line_graph",
"data": "/output/sen2cube/user.name/pre_event_mowing_cloudless_id10424.csv",
"bytes": 21891,
"x-axis": "time",
"y-axis": "pre_event_mowing_cloudless",
"lines": "space"
},
]
Downloading result
To download a result the URI in data
has to be prefixed with the uri
of the factbase it was executed against.
10.3.2 Factbase
Factbases contain the image data and semantic information.
Endpoint: /v1/factbase
Datamodel
{
"data": {
"type": "factbase", // always "factbase"
"attributes": {
"title": "Austria", // title
"description": "Sen2Cube.at data cube for Austria",
"owner": "Martin Sudmanns", // owner
"project": "sen2cube", // project (internal)
"owner_email": "info@sen2cube.at", // contact email
"uri": "https://demo.sen2cube.at", // URI of the factbase - prefix infrence output location with this URI to download data
"status": "OK", // status of factbase. OK|BUSY|MAINTENANCE
"sensor": "sentinel-2", // name of sensor
"dateStart": "2015-07-04", // start of temporal extent
"dateEnd": "2021-08-10", // end of temporal extent
// SRS of factbase
"srs": "PROJCS[\"ETRS89 / LAEA Europe\",GEOGCS[\"ETRS89\",DATUM[\"European_Terrestrial_Reference_System_1989\",SPHEROID[\"GRS 1980\",6378137,298.257222101,AUTHORITY[\"EPSG\",\"7019\"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY[\"EPSG\",\"6258\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4258\"]],PROJECTION[\"Lambert_Azimuthal_Equal_Area\"],PARAMETER[\"latitude_of_center\",52],PARAMETER[\"longitude_of_center\",10],PARAMETER[\"false_easting\",4321000],PARAMETER[\"false_northing\",3210000],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AUTHORITY[\"EPSG\",\"3035\"]]",
"resolution": [ // native resolution (x/y) in m
10,
10
],
"layout": { // description of layers available in this factbase
"reflectance": [], // snipped for brevity
"appearance": [],
"topography": [],
"atmosphere": [],
"artifacts": []
},
"footprint_bbox": { //GeoJSON polygon of BBOX
"type": "Polygon",
"coordinates": [
[
[
9.63365349495478,
46.24702482514619
],
[
9.356662993407214,
48.882262709970156
],
[
17.203411418707493,
48.99948448145476
],
[
17.094709857897673,
46.35394653581438
],
[
9.63365349495478,
46.24702482514619
]
]
]
},
"footprint": { // snipped GeoJSON feature collection representing the area(s) of data
},
"basemaps": [ //array of available / recommended basemaps for the factbase (for UIs)
{
"name": "cartocdn",
"label": "Carto Light",
"label_colour": "dark",
"url": "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
"thumbnail": "", //string with base64 encoded thumbnail image - this will be directly loaded in <img>
"attribution": "© <a href=\"http://www.openstreetmap.org/copyright\" target=\"_blank\" rel=\"noopener\">OpenStreetMap</a> contributors, © <a href=\"https://www.maptiler.com/copyright/\" target=\"_blank\" rel=\"noopener\">MapTiler</a>, © <a href=\"https://carto.com/attributions\">light_all</a>"
}
],
// internal use
"busy_worker_ping": "2021-09-25T15:51:32.359023+00:00",
"free_worker_ping": "2021-09-27T13:44:21.625625+00:00",
"location": null,
"preview_factor": 10
},
"id": 1,
"relationships": {
"inferences_fb": {
"links": {
"self": "/v1/factbase/1/relationships/inferences_fb",
"related": "/v1/factbase/1/inferences_fb"
}
}
},
"links": {
"self": "/v1/factbase/1"
}
},
"links": {
"self": "/v1/factbase/1"
},
"jsonapi": {
"version": "1.0"
}
}
10.3.3 Knowledgebase
The knowledgebase contains models (queries) that can be executed agains a factbase.
{
"data": {
"type": "knowledgebase",
"attributes": {
"title": "04 - Cloud-Free Composite",
"description": "This is a demo model.",
"owner": "user.name",
"date": "2020-06-17",
"favourite": true,
"blockdefs": "<snipped for brevity>" // contains the model as XML. Snipped here for brevity.
},
"relationships": {
"inferences_kb": {
"links": {
"self": "/v1/knowledgebase/218/relationships/inferences_kb",
"related": "/v1/knowledgebase/218/inferences_kb"
}
}
},
"id": 218,
"links": {
"self": "/v1/knowledgebase/218"
}
},
"links": {
"self": "/v1/knowledgebase/218"
},
"jsonapi": {
"version": "1.0"
}
}