API Access

Overview

X-Assist allows integration with 3rd party software through its GraphQL API. It provides access to all features and data available in X-Assist.

The GraphQL API is served from a single endpoint:

https://x-assist.exop-group.com/graphql

Getting started

For first time users, we recommend using the interactive query tool available at https://x-assist.exop-group.com/devtools. The tool offers a query editor with autocompletion and a documentation browser which allows you to explore the GraphQL schema of X-Assist.

For example, to retrieve the three most recent events, you would perform the following query:

query {
  events(first: 3, orderBy: { field: CREATED_AT, direction: DESC }) {
    nodes {
      id
      name
      createdAt
    }
  }
}

The API then responds with a JSON object that looks like this:

{
  "data": {
    "events": {
      "nodes": [
        {
          "id": "RXZlbnQtMTg2NjY5NzU=",
          "name": "Sindh: At Least Three Killed and Seven Injured in a Gas Cylinder Explosion in an Eatery in City of Karachi",
          "createdAt": "2021-04-07T09:14:01Z"
        },
        {
          "id": "RXZlbnQtMTg2NjY5NzI=",
          "name": "Shan: Reportedly 1 Person Killed and 10 Others Injured After Security Forces Open Fire on Civilians Within the Context of a Protest in Township of Nyaungshwe",
          "createdAt": "2021-04-07T09:10:07Z"
        },
        {
          "id": "RXZlbnQtMTg2NjY5NDU=",
          "name": "Gauteng: 14 Injured in a Taxi Road Traffic Accident in Location of Florida",
          "createdAt": "2021-04-07T08:43:10Z"
        }
      ]
    }
  }
}

Authentication

The X-Assist API uses OAuth 2.0 Bearer Tokens to authenticate requests.

These tokens are issued by the EXOP IAM module, which implements a standards-compliant OAuth 2.0 authorization server and OpenID Connect provider. You can access its configuration data at https://accounts.exop-group.com/.well-known/openid-configuration.

Server-side applications should use the Client Credentials Grant to request an access token from the IAM's token endpoint.

Request an OAuth access token from the IAM

POST https://accounts.exop-group.com/oauth/token

Request Body

NameTypeDescription

client_id

string

The OAuth client ID of your application.

client_secret

string

The OAuth client secret of your application.

grant_type

string

The OAuth grant type to use. Must be set to "client_credentials".

{
    "access_token": "-sc-na0LtIghflDJ8rH8wBDdmRBSyd5iPF0znsVegeY",
    "token_type": "Bearer",
    "expires_in": 600,
    "scope": "public",
    "created_at": 1612882203
}

Note that access tokens have a finite lifetime and expire at created_at + expires_in seconds after the Unix epoch. After this time, you must request a new token from the IAM.

If you do not want to implement the token management yourself, you can use one of the existing client libraries for your programming language/environment instead.

Accessing the GraphQL endpoint

To query the GraphQL endpoint, the request must include an access token in the HTTP Authorization header (preferred) or the access_token query parameter.

GraphQL query

POST https://x-assist.exop-group.com/graphql

Headers

NameTypeDescription

Authorization

string

The OAuth access token used to authenticate the request. Example value: "Bearer -sc-na0LtIghflDJ8rH8wBDdmRBSyd5iPF0znsVegeY"

Request Body

NameTypeDescription

query

string

The GraphQL operation to execute. Can be a query, mutation or a subscription. See https://graphql.org/learn/queries/.

operationName

string

Only required if multiple operations are present in the query. See https://graphql.org/learn/queries/#operation-name.

variables

object

A dictionary of dynamic query arguments. See https://graphql.org/learn/queries/#variables.

{
  "data": { ... },
  "errors": [ ... ],
  "extensions": { ... }
}

GraphQL does not use HTTP status codes, but instead uses an errors array in the response.

The reason for this is that complex queries (e.g. ones that use multiplexing or are only partially executed) cannot be reasonably mapped to a single status code.

Therefore, API clients must always check the errors array in the response. In addition to that, our GraphQL mutation payloads have a separate array of UserError objects.

  • Entries in the top-level errors array are developer-facing errors (e.g. for syntax errors in the GraphQL query).

  • Mutation errors are used to communicate user-facing errors, e.g. when an input field exceeds its maximum length.

Here is a full example showing how to retrieve the three most recent events from X-Assist:

POST https://x-assist.exop-group.com/graphql
Authorization: Bearer -sc-na0LtIghflDJ8rH8wBDdmRBSyd5iPF0znsVegeY
Content-Type: application/json

{
    "query": "query { events(first: 3, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id name createdAt }}}",
    "variables": null,
    "operationName": null
}

Date and time

Timestamps and other time-related objects are crucial concepts in the X-Assist API, and it is critical that API clients handle them in the right way.

Timestamps

Fields with the scalar type DateTime are the most basic ones. A DateTime value is an ISO 8601 formatted string that represents an instant on the global timeline. DateTime values in API responses are usually in UTC (indicated by a trailing Z), but API clients must be able to handle values with other offsets as well. Unqualified DateTime inputs (with an unspecified offset) are assumed to be in UTC.

A DateTime field on an object is frequently accompanied by a timeZone field that contains an IANA Time Zone ID that API clients can use to convert the UTC value back into local time. The time zone field can be on the same object as the timestamp itself (as is the case with HotelSegment objects, for example), or within a nested object (e.g. FlightLeg objects do not have timeZone fields, but the associated airports do).

Time intervals

DateRange objects combine two DateTime fields to represent the time interval between the two points in time. The lower bound of the date range is always inclusive, while the upper bound is always exclusive.

Durations

Durations that do not refer to a fixed point in time are represented either as integers or as values of the type Duration. For integers, the value is given in seconds. Duration fields use ISO 8601 formatting.

ISO 8601 formatted durations can also be used in the DateTimeOrDuration scalar, which is commonly used in the DateFilter type. Here, durations are interpreted relative to the current time and negative values are permitted. For example, a date filter that covers the last 24 hours looks like this:

{
  gte: "-PT24H",
  lt: "P0D"
}

Geographical data

The X-Assist GraphQL schema uses the following types to represent geographical data:

  • GeoPoint / GeoPointInput

  • GeoJSON

These types always use WGS 84 / SRID 4326 as spatial reference system.

Internationalization (I18n)

The content of X-Assist is available in the following languages:

  • Chinese

  • English

  • French

  • German

  • Italian

  • Japanese

  • Portuguese

  • Spanish

The HTTP Accept-Language header determines the language in which the content of an API response is returned. Additionally, some fields in the GraphQL schema support a language argument that allows API clients to explicitly request content in a specific language.

For example, clients may wish to retrieve the original English headline of an event in addition to machine translated headline:

query {
  event(id: "RXZlbnQtMTk5NTYxOTU=") {
    name # Language is auto-selected by X-Assist (German in this example)
    originalName: name(language: EN) # Explicitly request the original English text
    latinName: name(language: LA) # X-Assist does not support Latin yet, so this will return null
  }
}
{
  "data": {
    "event": {
      "name": "Zhytomyr: Mindestens fünf getötete Zivilisten bei Luftangriff auf die Stadt Malyn",
      "originalName": "Zhytomyr: At Least Five Civilians Killed in Air Strike on the City of Malyn",
      "latinName": null
    }
  }
}

Pagination

Collections of objects are exposed in the X-Assist API in two different ways. Collections with only a few items are exposed as plain lists (i.e. arrays) for simplicity:

type Event {
  # Each event typically only involves a few militant groups (if any).
  militantGroups: [String!]!
}

Large collections use Relay Connections, a cursor-based pagination mechanism and the de-facto standard used by many GraphQL APIs.

Let's re-visit the earlier example query that retrieves the three most recent events:

query {
  events(first: 3, orderBy: { field: CREATED_AT, direction: DESC }) {
    nodes {
      id
      name
      createdAt
    }
  }
}

The events field in this query is a connection of type EventConnectionWithTotalCount. Every connection has the fields pageInfo, nodes and edges, and some connections (including the event connection) also provide additional fields such as totalCount.

The number of items that can be returned in a single query is limited. Clients must be prepared to receive fewer items than requested.

Connections can expose additional data about the relationship between the objects on their edges.

For example, the edges of the affectedPeople connection on the IncidentAlert type have a distanceFromIncident field that contains the distance in meters between the person and the event at the time of the alert.

If you do not need this information, you can select the nodes field instead of edges to remove one level of nesting from the API response.

To support pagination, the query must be changed and some fields from the PageInfo object must be added to the selections:

query {
  events(first: 3, orderBy: { field: CREATED_AT, direction: DESC }) {
    pageInfo {
      endCursor
      hasNextPage
    }
    totalCount
    nodes {
      id
      name
      createdAt
    }
  }
}

The new query result now contains a cursor that can be used to fetch the next page:

{
  "data": {
    "events": {
      "pageInfo": {
        "endCursor": "ed3qIy2/n4k=",
        "hasNextPage": true
      },
      "totalCount": 160724,
      "nodes": [...]
    }
  }
}

If there are more items in the connection (i.e. hasNextPage is true), the next three events can be fetched by passing the cursor value in the after argument:

query {
  events(first: 3, after: "ed3qIy2/n4k=", orderBy: { field: CREATED_AT, direction: DESC }) {
    pageInfo {
      endCursor
      hasNextPage
    }
    totalCount
    nodes {
      id
      name
      createdAt
    }
  }
}

This process is repeated until hasNextPage is false and the entire collection has been traversed.

Do not change filter predicates or sorting halfway through the pagination process, as this will lead to undefined results.

Object identification

Some types in X-Assist's GraphQL schema implement the Node interface which allows API clients to retrieve one or more objects by ID. Refer to the Global Object Identification chapter in the GraphQL documentation for further information.

All applications must treat object IDs as opaque strings and make no assumptions about their content or length.

The encoding scheme of these IDs may also change over time. These changes are backwards compatible (X-Assist will continue to accept IDs in previous formats), but applications that store references to X-Assist objects are encouraged to include the id field in each query and update the IDs in their own data stores when a change is detected.

Optimistic Concurrency Control

Many mutations use optimistic locking to prevent lost updates when multiple users make changes to existing data concurrently. The input objects for these mutations have a mandatory version field.

When performing a mutation, pass the expected version of the object as input and add the version field to your selections to get the new version of the object after the update. As an example, the mutation to update a site looks as follows:

mutation {
  updateSite(input: {
    id: "U2l0ZS0yMTcx",
    version: "1",
    attributes: { ... }
  }) {
    site {
      version
    }
    errors: {
      message
      path
    }
  }
}

The response contains the new version of the site:

{
  "data": {
    "updateSite": {
      "site": {
        "version": "2"
      },
      "errors": []
    }
  }
}

If a version mismatch occurs, you will receive an error instead:

{
  "data": {
    "updateSite": {
      "site": null,
      "errors": [
        {
          "message": "Attempted to update a stale object: Site.",
          "path": [
            "input",
            "version"
          ]
        }
      ]
    }
  }
}

API clients can handle such errors by re-fetching the object (see Object identification) to get the current version, and then retrying the mutation.

Do not make assumptions about the content and the behavior of the version field.

In particular, do not assume that the version of new objects is equal to 1 or that updates only increment the version by one. Be aware that background processes or other events can also change the version of an object without any requests being sent to X-Assist from an API client.

For maximum robustness, treat the version as an opaque string.

Data consistency

The X-Assist API provides different data consistency guarantees for different kinds of operations.

Mutations

GraphQL mutation payloads provide strong consistency. When you perform a mutation, all fields in the response are guaranteed to return the most recent data and reflect your changes.

However, this does not apply to changes caused indirectly by a mutation. For instance, when you register a new trip in X-Assist, the trip itself will be visible immediately, but the person's itinerary stops (expected locations) will only be updated after a delay because they are re-computed by an asynchronous background job.

Subscriptions

GraphQL subscriptions offer the same guarantees as mutations.

Queries

Unlike mutations, regular GraphQL queries only offer eventual consistency. This means that changes may not immediately visible to all clients, and that a query can return stale data.

Rate limiting

Regular users and OAuth applications can make up to 1000 requests per hour, with a maximum number of 25 + 1 burst requests in rapid succession. This limit can be raised once your solution/integration has been validated by EXOP.

The HTTP response headers will show your current rate limit status:

HTTP/2 200
Date: Tue, 20 Apr 2021 10:56:29 GMT
X-RateLimit-Limit: 26
X-RateLimit-Remaining: 18
X-RateLimit-Reset: 50.5414

We loosely follow the IETF draft for rate limiting headers, but we do not use fixed-length time windows.

When a request is blocked due to rate limiting, X-Assist will respond with HTTP status 429 ("Too Many Requests"):

HTTP/2 429
Date: Tue, 20 Apr 2021 11:10:12 GMT
X-RateLimit-Limit: 26
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 183.986299
Retry-After: 3.986299

You are rate limited, try again later.
Response HeaderDescription

X-RateLimit-Limit

The maximum number of available burst requests when no previous requests have been made, or the previous requests occurred long enough ago.

X-RateLimit-Remaining

The number of requests that can be made immediately (i.e. the remaining burst request count).

X-RateLimit-Reset

The time in seconds until the full number of burst requests will be available again.

Retry-After

Only present when the request was blocked to due rate limiting. For allowed requests that can be retried later (see note below), this header will contain the duration to wait in seconds before the request will be allowed.

When the request is forbidden due to complexity constraints, this header will have no value (i.e. an empty string).

Not all requests are equal. For example, a request with a very complex GraphQL operation may consume multiple "logical requests" at once.

It is therefore possible to create requests that will never succeed because they would consume more logical requests than the maximum number of available burst requests.

Last updated