Agrimetrics GraphQL API Introduction

Agrimetrics GraphQL API Introduction

Table of Contents

  1. Introduction To GraphQL
  2. Agrimetrics Graph API
  3. Advanced GraphQL Concepts
  4. Examples

Introduction To GraphQL {#introduction-to-graphql}

What is GraphQL?

GraphQL (Graph Query Language) is a query language for graph-like data. You can use the Agrimetrics GraphQL API to query Agrimetrics data.

GraphQL APIs are backed by a machine-readable schema and human-readable documentation which can be used by user interfaces and tools to help explore and explain the graph and its data.

You can find a general introduction to GraphQL at https://graphql.org/learn/.

You can explore our data, and try out queries, using our Graph Explorer application — note that this application requires that you hold an Agrimetrics Field Explorer license (full or trial).

If you have any questions or feedback, please contact us.

Agrimetrics Graph API {#agrimetrics-graph-api}

A simple example

A GraphQL query is sent to a server via an HTTP POST request; here's a simple example - asking for the area of a field.

Here is the query:

query {
  fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
    area {
      value
      unit
    } 
  }
}

Here's how we would send it to the GraphQL API using curl (assuming your API key is stored in a shell variable called API_KEY):

curl -XPOST -H 'content-type: application/json' -H "ocp-apim-subscription-key: ${API\_KEY}" https://api.agrimetrics.co.uk/graphql/v1 -d '{"query": "query { fields(where: {id: {EQ: \\"agfd:C6BgTxUxhMG\_OCGrLGW8qw\\"}}) { area { value unit }}}"}'

Here is the result:

  "data": {
    "fields": [{
      "id": "https://data.agrimetrics.co.uk/fields/C6BgTxUxhMG\_OCGrLGW8qw",
      "area": {
        "value": 9.9364,
        "unit": "http://data.agrimetrics.co.uk/units/hectares"
      }
    }]
  }
}

See below for more detail, see Examples for more examples.

Licensing{#licensing}

The GraphQL API (and the Graph Explorer application) requires a Field Explorer subscription. Trial subscriptions may access the Graph Explorer and API, but functionality will be limited. Some data sets (e.g. Field Boundaries and Crop Observations) require additional subscriptions or payments beyond a standard Field Explorer subscription.

The API will return an informative error if you try to access something beyond your current subscription.

Calling the API{#calling-the-api}

Every GraphQL request is a POST request to the same endpoint, https://api.agrimetrics.co.uk/graphql/v1, with the GraphQL query text contained in the body of the request. Your subscription key should be passed in the Ocp-APIM-Subscription-Key header.

Here is the simple example above in more detail:

  fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
    area {
      value
      unit
    }
  }
}

We are querying for a field by its known id, and requesting the area of the field. The concept area itself contains multiple properties, including the value and unit which are requested.

The where parameter uses a comparator expression to filter the results. You will find the where pattern throughout the Agrimetrics GraphQL API. Depending on what is being queried, different properties will be available for filtering, with various comparator operators available. In this example we filter the id property using the equality operator, EQ.

This could be sent to the API using the following curl command line (assuming your API key is stored in a shell variable called API_KEY):

curl -XPOST -H 'content-type: application/json' -H "ocp-apim-subscription-key: ${API\_KEY}" https://api.agrimetrics.co.uk/graphql/v1 -d '{"query": "query { fields(where: {id: {EQ: \\"agfd:C6BgTxUxhMG\_OCGrLGW8qw\\"}}) { area { value unit }}}"}'

Note that the GraphQL query text is encoded as a string in the query property of a JSON object, with quotes in the query text escaped using back-slashes. White-space in the query text is not considered significant, thus the query may be expressed in one line without line-feeds or indentation.

The easiest way to export a query for use in curl, Python or other tools is to use the Graph Explorer's "Export Code" feature.

Response

A request made with a valid GraphQL syntax will always return a 200 OK response code, with a JSON object as the body (unless instructed otherwise).

The request above returns the following JSON object response body:

  "data": {
    "fields": [
      {
        "area": {
          "value": 9.9364,
          "unit": "http://data.agrimetrics.co.uk/units/hectares"
        }
      }
    ]
  }
}

All the data we requested is returned under the data property, in a similar structure to that of the query.

Because the underlying schema declares the value of fields as a list, the API returns an array of field data objects, in this case, with only one value. If no fields matched your query, the fields array would be empty.

Errors

In case of syntax errors in the GraphQL query, the server will return a 400 Bad Request response, otherwise the server will always return a 200 OK response, even if errors happen when actually evaluating the query; both responses will have a JSON object body.

In case of either type of error, there will be an errors property (a JSON array) at the top level of the JSON object. There may still be a data property if any part of the query evaluated correctly, otherwise data will not be present.

Depending on your use case, you may choose to discard the data if there were any errors at all, or you may choose to use any data that was returned, and merely report, or even ignore the errors.

Here's an example of a response to a request for the area and shape (boundary) of a field, when the subscription doesn't have sufficient credits to retrieve boundaries:

{
  "errors": [
    {
      "message":"Insufficient credits to complete request",
      "locations":[{"line":1,"column":69}],
      "path": ["fields",0,"shape"],
      "extensions":{"code":"PAYMENT\_REQUIRED"}
    }
  ],
  "data": {
    "fields": [
      {
        "area": {
          "value": 9.9364,
    "unit": "http://data.agrimetrics.co.uk/units/hectares"
        },
        "shape":null
      }
    ]
  }
}

The area property is returned successfully as before, but shape is null, and there is now an errors array at the top level, with details about the error, including the path which shows on which property of the query the error occurred, and an informative message and code.

Output Formats{#output-formats}

JSON

By default the GraphQL API returns results as JSON.

All URIs returned from the GraphQL API are compatible with the Agrimetrics JSON-LD REST APIs.

If you are interested in JSON-LD output, please contact us for details.

Tabular

By specifying an Accept header of text/csv in your query, the API can return data in tabular format. This is currently an experimental feature.

If returning tabular, we strongly recommend that you keep your queries simple, in particular these queries should not contain two or more data series at the same level.

If there are errors when evaluating a query that is requested in tabular format, a non-200 HTTP error status will be returned with a JSON body containing error information.

Data Sets{#data-sets}

The data returned by the GraphQL API comes from a variety of Data Sets. By specifying the dataSetId property in your query you can see the source Data Set for a specific type of data such as a weather observation:

  fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
    id
    weatherObservations(where: {date: {GE: "2020-01-01", LT: "2020-02-01"}}) {
      rainfallTotalDaily {
        value
        dataSetId
      }
    }
  }
}

returns

{
  "data": {
    "fields": [
      {
        "id": "https://data.agrimetrics.co.uk/fields/C6BgTxUxhMG\_OCGrLGW8qw",
        "weatherObservations": {
          "rainfallTotalDaily": [
            {
              "value": 0,
              "dataSetId": "https://data.agrimetrics.co.uk/data-sets/83a5fc48-182a-4933-9a3a-a453c102b2be"
            }
          ]
        },
        ...
      }
    ]
}

Note that in this example, and others throughout this documentation, the inclusion of ... in the JSON response simply indicates this is a list of results and there would be more data in a real response.

Where there are multiple possible Data Sets for a given type of data, the API will return data from a default. It is also possible to specify the Data Set you wish to use by specifying dataSetId in a where clause:

query {
  fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
    id
    weatherObservations(where: {date: {GE: "2020-01-01", LT:"2020-02-01"}}) {
      rainfallTotalDaily(where: {dataSetId:{EQ: "https://data.agrimetrics.co.uk/data-sets/83a5fc48-182a-4933-9a3a-a453c102b2be"}}) {
        value
        dataSetId
      }
    }
  }
}

returns

{
  "data": {
    "fields": [
      {
        "id": "https://data.agrimetrics.co.uk/fields/C6BgTxUxhMG\_OCGrLGW8qw",
        "weatherObservations": {
          "rainfallTotalDaily": [
            {
              "value": 0,
              "dataSetId": "https://data.agrimetrics.co.uk/data-sets/83a5fc48-182a-4933-9a3a-a453c102b2be"
            },
            ...
          ]
        }
      }
    ]
  }
}

Note that when you limit the Data Set using where.dataSetId, if there is no data available, e.g. for your given date range, you will receive an empty list of results.

You can find out information about a Data Set via its Id using a node query:

query {
  node(id: "https://data.agrimetrics.co.uk/data-sets/83a5fc48-182a-4933-9a3a-a453c102b2be") {
    id
    ... on DataSet {
      id
      title
      description
    }
  }
}

Full information for all Data Sets is also available in the Data Catalogue.

A Note on Ids{#a-note-on-ids}

All node entities in the graph have an id property, which will be returned as a full URI, for example https://data.agrimetrics.co.uk/fields/C6BgTxUxhMG_OCGrLGW8qw.

When querying by id the full URI can be provided, or alternatively, there exist convenience short-hand prefixes for many entities.

The above field can be resolved equivalently using agfd:C6BgTxUxhMG_OCGrLGW8qw as the id.

Below is a list of prefixes, the concept they represent, and the full URI they will evaluate to:

Advanced GraphQL Concepts{#advanced-graphql-concepts}

The following information will help you to make full use of the power of GraphQL in complex queries and when asking for large amounts of data.

Parameterizing Queries{#parameterizing-queries}

Instead of substituting values such as coordinates or dates directly into a query text, you can use variables in your query, and pass the values separately. This is more secure and less error-prone. It is strongly recommended to use parameterized queries instead of attempting to achieve this with either string concatenation or interpolated strings.

For more information, see the Variables documentation in the GraphQL learning resource.

Cursors{#cursors}

To obtain all the results of a large data set, you will need to use cursors.

Cursors allow you to efficiently retrieve a page of results with each request, and to progress through subsequent pages until all data has been returned, even if the total data to be retrieved is very large.

See our documentation on cursors for details of how to apply them in your queries.

Query Fragments{#query-fragments}

Sometimes a query will include lots of repetition, for instance getting the same properties from multiple nodes, where the underlying type of the node is the same; using query fragments can minimise how much duplication these queries need.

To see how you can use fragments to make complex queries easier to manage, read our documentation on query fragments, and the Fragments section in the offical GraphQL learning resource.

Interfaces and Union Types{#interfaces-and-union-types}

GraphQL schemas allow defining interfaces and union types.

Interfaces are abstract types that define a set of properties. Any type that includes these properties can then implement that interface.

Union types are effectively a set of distinct types. If a property is a union-type, data returned will be one of the set of types defined.

We use these concepts in our GraphQL API, and they can be used with Query Fragments and the special node top-level query.

More information on interfaces and union types can be found in the GraphQL documentation.

Examples{#examples}

Simple Examples{#simple-examples}

The examples shown earlier as an introduction to GraphQL show querying a field by its known ID. Fields can also be found by geo-location, using either a Point, where the point must lie within a field:

query {
  fields(geoFilter: {location: {type: Point, coordinates: [0.089372, 52.245591]}}) {
      id
  }
}

Note that Point is [lon, lat] as per GeoJSON.

or a Point with a distance parameter to specify a radius (in metres) of a circle that the field must intersect with:

  fields(geoFilter: {distance: {LE: 5000}, location: {type: Point, coordinates: [0.089372, 52.245591]}}) {
      id
  }
}

or a Polygon to specify a shape that the field must intersect with:

query {
  fields(geoFilter: {location: {type: Polygon, coordinates: [[[\-0.63720703125, 51.09144802136697], [\-0.582275390625, 51.057796900048594], [\-0.53009033203125, 51.076782592536524], [\-0.509490966796875, 51.11473061746101], [\-0.53558349609375, 51.13972478986592], [\-0.6042480468749999, 51.128522178293224], [\-0.63720703125, 51.09144802136697]]]}}) {
    id
  }
}

The location parameter here is a GeoJSON Polygon.

or, finally, a list of field ids, using the where parameter, instead of a geoFilter:

query {
  fields(where: {id: {EQ: ["agfd:C6BgTxUxhMG\_OCGrLGW8qw", "agfd:-8Ux5v46seUPmyjqH3-5Sw"]}}) {
    id
  }
}

Return the Weather Forecast, for a given field, generated on a given date.

{
query {
  fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
    weatherForecast(where: {generatedAt: {EQ: "2019-01-01"}}) {
      temperatureMeanDaily {
        dateTime
        value
      }
      rainfallTotalDaily {
        dateTime
        value
      }
      windSpeedMeanDaily {
        dateTime
        value
      }
    }
  }
}

returns

{
  "data": {
    "fields": [
      {
        "weatherForecast": {
          "temperatureMeanDaily": [
            {
              "dateTime": "2019-01-01T00:00:00.000Z",
              "value": 5.88
            },
            ...
          ],
          "rainfallTotalDaily": [
            {
              "dateTime": "2019-01-01T00:00:00.000Z",
              "value": 0
            },
            ...
          ],
          "windSpeedMeanDaily": [
            {
              "dateTime": "2019-01-01T00:00:00.000Z",
              "value": 4.364
            },
            ...
          ]
        }
      }
    ]
  }
}

Return a specific 3 months of total rainfall for a given field

query {
  fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
    weatherObservations(where: {date: {GE: "2019-03-01", LT: "2019-06-01"}}) {
      rainfallTotalDaily {
        value
        dateTime
      }
    }
  }
}

returns

{
  "data": {
    "fields": [
      {
        "weatherObservations": {
          "rainfallTotalDaily": [
            {
              "value": 0,
              "dateTime": "2019-03-01T00:00:00.000Z"
            },
            {
              "value": 0.2,
              "dateTime": "2019-03-02T00:00:00.000Z"
            },
            ...
          ]
        }
      }
    ]
  }
}

Advanced Examples{#advanced-examples}

Cursoring over large result-sets{#cursoring-over-large-result-sets}

Retrieving a large set of data requires using a cursor. This example uses parameterization, however this is optional.

query($cursor: String) {
  fields(where: {id: {EQ: "agfd:C6BgTxUxhMG\_OCGrLGW8qw"}}) {
    weatherObservations(
      where: {
        date: {
          GE: "2018-03-01",
          LE: "2019-01-01",
        }
      }
      after: $cursor
    ) {
      cursor
      rainfallTotalDaily {
        value
        dateTime
      }
    }
  }
}

A first request, with cursor set to null returns the first set of data:

{
  "data": {
    "fields": [
      {
        "weatherObservations": {
          "cursor": "MjAxOC0wOC0wMw==",
          "rainfallTotalDaily": [
            {
              "value": 1,
              "dateTime": "2018-03-01T00:00:00.000Z"
            },
            {
              "value": 2.7,
              "dateTime": "2018-03-02T00:00:00.000Z"
            },
            ...,
            {
              "value": 0,
              "dateTime": "2018-08-03T00:00:00.000Z"
            }
          ]
        }
      }
    ]
  }
}

Note that the returned cursor, MjAxOC0wOC0wMw==, is not null. Thus there is additional data which can be retrieved with subsequent requests.

Repeating the query, but this time with the variable cursor set to MjAxOC0wOC0wMw== returns:

{
  "data": {
    "fields": [
      {
        "weatherObservations": {
          "cursor": null,
          "rainfallTotalDaily": [
            {
              "value": 0,
              "dateTime": "2018-08-04T00:00:00.000Z"
            },
            {
              "value": 0,
              "dateTime": "2018-08-05T00:00:00.000Z"
            },
            ...
          ]
        }
      }
    ]
  }
}

These results begin where the last request left off.

In the original request, the last dateTime provided was '2018-08-03T00:00:00.000Z', the first result of the second request has a dateTime of '2018-08-04T00:00:00.000Z'.

An implementation of this can be seen in the examples.

Return the Soil Texture for every field in a given area{#return-the-soil-texture-for-every-field-in-a-given-area}

In this example, we obtain the top-soil and sub-soil texture of all the fields within 10,000 m of a point specified by a latitude and longitude.

If there are a large number of fields, it is important to request the cursor to ensure you get all the data, for example the following query:

{
  fields(geoFilter: {distance: {LE: 10000}, location: {type: Point, coordinates: [0.089372, 52.245591]}}) {
    cursor
    id
    soil {
      subSoil {
        texture {
          clayPercentage
          sandPercentage
          siltPercentage
          type
        }
      }
      topSoil {
        texture {
          clayPercentage
          sandPercentage
          siltPercentage
          type
        }
      }
    }
  }
}

returns

{
  "data": {
    "fields": [
      {
        "cursor": "MQ==|aHR0cDovL2RhdGEuYWdyaW1ldHJpY3MuY28udWsvZmllbGRzLy0wVmRaZHlvMnQya0dhRUlYWlM2Z0E=",
        "id": "https://data.agrimetrics.co.uk/fields/-0VdZdyo2t2kGaEIXZS6gA",
        "soil": {
          "subSoil": {
            "texture": {
              "clayPercentage": 24.67,
              "sandPercentage": 49.67,
              "siltPercentage": 25.67,
              "type": "SANDY\_CLAY\_LOAM"
            }
          },
          "topSoil": {
            "texture": {
              "clayPercentage": 24.69,
              "sandPercentage": 47.88,
              "siltPercentage": 27.43,
              "type": "SANDY\_CLAY\_LOAM"
            }
          }
        }
      },
      {
        "cursor": "NTAw|aHR0cDovL2RhdGEuYWdyaW1ldHJpY3MuY28udWsvZmllbGRzLzl6MC1ibmZRNmlaTF8yV2FxRXdrRXc=",
        "id": "https://data.agrimetrics.co.uk/fields/9z0-bnfQ6iZL\_2WaqEwkEw",
        "soil": {
          "subSoil": {
            "texture": {
              "clayPercentage": 24.75,
              "sandPercentage": 47.49,
              "siltPercentage": 27.76,
              "type": "SANDY\_CLAY\_LOAM"
            }
          },
          "topSoil": {
            "texture": {
              "clayPercentage": 23.69,
              "sandPercentage": 44.64,
              "siltPercentage": 31.67,
              "type": "LOAM"
            }
          }
        }
      },
      ...
    ]
  }
}

Note in this request every field has a cursor. You can continue a query starting from any of these fields.

If the last cursor is null, there are no more fields to cursor over.