Developer API

Table of Contents

2.1 Actors
2.2 Assets
2.7 Files
2.8 Lenses
4.1 Node
5.1 Assets
5.5 Files
5.6 Actors
6.1 Asset
6.3 Actor
6.4 Update
6.6 File
6.8 Lenses
7.1 Users
7.2 Actors
7.3 Assets
7.5 Claims
7.10 Lenses
7.12 Theming

1 Introduction

The Geora protocol is a blockchain-based system for managing and securing commodity supply chains. It provides simple tools and workflows to create solutions that provide rich data traceability, secure payments and real time finance.

This page outlines core concepts and terminology that are helpful in understanding the Protocol and the various workflows available, so you can begin transforming and growing your agri-supply chain. It also provides instructions for connecting to our developer API and using it to build applications on Geora.

2 Core concepts

2.1 Actors

An actor is an entity in the system which represents a user of the supply chain. It could be the producer of a commodity, or a logistics provider. While there may be many real people performing actions on behalf of a company, the protocol considers the company to be a single actor.

Actors span the whole supply chain:

An actor in the protocol may fit into many roles. A site operator for a grain bulk handler could take part as a custodian, certifier, and publisher at the same time.

2.2 Assets

An asset represents part of the supply chain; a real-world entity with unique data and history. While an asset most commonly represents a commodity, it can take other forms, such as:

2.3 Identifiers and attributes

From the moment of creation until end of life, an asset is identified by a unique asset ID. This ID always references the same asset across the protocol. At a lower level, each change to an asset generates a new version, identified by a *version ID*. The asset ID represents the asset and its history as a whole, while the version ID refers to a particular point in its history.

An asset's class defines its variety or commodity type, like "APPLE" or "WHEAT", which producers use when generating new entities in the system.

An asset is measured using a primary value and primary unit, which refer respectively to the amount of the asset and its unit of measurement. These can change over time. For example:

2.4 Provenance data

Assets can contain arbitrary data, reflecting their diverse purposes. Data on an asset takes the form of claims and evidence.

Claims are statements of fact about an asset. For example, a canola asset may contain claims like:

By themselves, claims aren't verified - the reputation of the actor who made the claim is all that exists to show that they're correct. *Evidence* provides a means to support claims, allowing actors to add documentation and certificates that prove or back claims. This could take the form of an organic certification from Australian Certified Organic, or a quality sensor output.

2.5 Ownership

The owner of an asset is the actor or actors who have legal title to the asset. This can be split between multiple actors proportionally (e.g. one actor has 70%, another has 30%).

The custodian of an asset is the actor who physically controls the asset. This could be different to the owner.

The manager of an asset is the owner who is delegated permission by all other owners to manage the asset, and receives full permissions on the asset.

For example, in a scenario in which a shipment of nylon is stored in a warehouse, the ownership structure might look like this:

2.6 Permissions

Control of assets is managed through a permission system. Actors can be given permissions to perform subsets of available actions on assets.

2.7 Files

Actors can upload files to use as evidence on assets. Files are immutable once uploaded, and have a similar permissioning system to assets that controls who can view and use the files.

2.8 Lenses

Actors can create lenses which provide a permissioned view for a group of assets, specifying the data that a certain user type can see. Lenses can restrict what is revealed of the asset's history, such as hiding the asset's class, quantity,and which claims and evidence are visible.

3 Using the API

Geora offers a GraphQL API which allows developers to build full applications on the protocol. GraphQL is a query language that is well suited to Geora's graph-like domain model.

The API provides features like:

It also provides a query interface to create rich queries on objects like assets, asset versions, actors, and files.

For example, the following query will retrieve a particular version of an asset and show both ownership and claims on that version:

{
  asset(id: "YXNzZXR8ODczMjEyNDIzNDMyNAo=") {
    latestVersion {
      id
      quantity {
        primaryUnit
        primaryValue
      }
    }
  }
}

The response contains all the requested information:

{
   "data": {
     "asset": {
       "latestVersion": {
         "id": "YXNzZXRWZXJzaW9ufDg3MzIxMjQyMzQzMjQK",
         "quantity": {
           "primaryValue": 56.6,
           "primaryUnit": "kg"
         }
       }
    }
  }
}

This documentation outlines the API's GraphQL schema. The schema is directly generated from this document, and you can find the generated output file here.

3.1 API status and stability

Different parts of the Geora API are classified according to their stability. Each of these stability levels have certain guarantees for the user.

We include the following as breaking changes:

We do NOT class the following as breaking changes:

Please note that we are in the process of making many fields in the API non-nullable which were previously nullable. For most users this will have no effect, but if you generate strong types from the GraphQL schema definition you should be aware of this change! Please reach out if this is a concern for you so we can keep you up to date.

The

@alpha

and

@beta

directives appear on all fields with a non-live status. You can use this during schema introspection to filter out APIs based on their stability.

directive @alpha on FIELD_DEFINITION
directive @beta on FIELD_DEFINITION

schema

3.2 Authentication and authorisation

To set up a free developer’s account to experiment with the system, follow these steps.

  1. Go to https://app.geora.io/developer and sign up with an email address.
  2. Check your inbox for an email, follow the link and set your password.
  3. Log in using your new password.
  4. Save your API headers in a safe place, like a password manager, and note the API endpoint. These uniquely identify you with the protocol!

For all requests to the API, you must supply your user ID and API key as the header variables x-api-user and x-api-key respectively.

All API calls except actorCreate require an additional authorisation header. These calls must be made from a particular actor, with the x-geora-actor key set to the actor ID. Because a user can create and use multiple actors, this ensures that the correct permissions are applied.

In the developer playground, these header keys are passed in the bottom left, under the HTTP Headers tab, as a JSON object like:

{
  "x-api-user":"50d5463a-f4f1-42e3-9ebb-c20171ea0896",
  "x-api-key":"my-secret-key",
  "x-geora-actor":"YWN0b3J8NTBkNTQ2M2EtZjRmMS00MmUzLTllYmItYzIwMTcxZWEwODk2Cg=="
}

The GraphQL schema enforces the x-geora-actor header key using the requiresActor directive.

directive @requiresActor on FIELD_DEFINITION

schema

The requiresPayingUser directive enforces that the calling user is a paying user with a required minimum subscription plan.

directive @requiresPayingUser(plan: SubscriptionPlan!) on FIELD_DEFINITION

schema

enum SubscriptionPlan {
    FREE
    STARTER
    PRO
    BUSINESS
}

domain-helpers

f, c, and id are implementation directives.

directive @f(name: String!, afterDefault: Boolean = False, nullOnNotFound: Boolean = False, transform: Boolean = False) on FIELD_DEFINITION
directive @c(name: String!, ascending: Boolean = False, defaultLimit: Int = 10, hasEdgeData: Boolean = False, transform: Boolean = False, orderableOnEdge: [Orderable!] = [], orderableOnNode: [Orderable!] = []) on FIELD_DEFINITION

directive @id(t: String) on FIELD_DEFINITION

schema

3.3 Connecting to the API

The best way to experiment with the API is using the integrated development playground here.

The integrated developer playground

On the left is an input for queries and mutations. On the right is the server's response. At the bottom of the screen is a headers section, where the user can provide authentication headers to use the API.

The playground contains a documentation explorer, which contains available queries and their parameters.

Once you're ready to start building an application, you can make calls directly to the API at the endpoint https://api.geora.io/v2.

A GraphQL request is a POST HTTP request to this url, containing a query or mutation. A query reflects a read request, while a mutation is a write request which will modify state.

In this query, the caller is requesting the asset class and quantity of a particular asset version:

{
  assetVersion(id: "YXNzZXRWZXJzaW9ufDEyMTgzNwo=") {
    id
    class
    quantity {
      primaryUnit
      primaryValue
    }
  }
}

In this mutation, the caller is changing the information on the same asset version:

mutation {
  assetUpdate(
    versionID: "YXNzZXRWZXJzaW9ufDEyMTgzNwo="
    input: {
      class: "WHEAT"
      quantity: { primaryUnit: "mt", primaryValue: 22.34 }
    }
  ) {
    id
  }
}

To send a request to the Geora API:

  1. Create a JSON string with the field query having your query or mutation as a string
  2. Add the Content-Type: application/json header
  3. Add authentication headers, as described below
  4. Send the string as a POST request to the Geora API

For example, using curl the above mutation would look like:

curl 'https://api.geora.io/v2' \
    -H 'Content-Type: application/json' \
    -H 'x-api-user: 50d5463a-f4f1-42e3-9ebb-c20171ea0896' \
    -H 'x-api-key: my-secret-key' \
    -H 'x-geora-actor: YWN0b3J8NTBkNTQ2M2EtZjRmMS00MmUzLTllYmItYzIwMTcxZWEwODk2Cg==' \
    --data-binary '{"query":"mutation { assetUpdate( versionID: \"YXNznXRWZXJzaW9ufDEyMTgzNwo=\" input: {class: \"WHEAT\" quantity: { primaryUnit: \"mt\", primaryValue: 22.34 }}) {id}}"}' \
    --compressed

The server will always return the response as a JSON string.

3.4 Client Libraries

Below are examples of calling the GraphQL API with some common languages, as well as Elm which we use in our own client software.

// Typescript

import fetch, { RequestInit, Response } from "node-fetch";

const ASSET_VERSION_QUERY: string = `
query ($id: ID!) {
    assetVersion(id: $id) {
        id
        class
        quantity {
            primaryUnit
            primaryValue
        }
    }
}
`;

async function query(): Promise<Response> {
    const url: string = "https://api.geora.io/v2";

    const headers: { [header: string]: string } = {
        "Content-Type": "application/json",
        "x-api-user": "50d5463a-f4f1-42e3-9ebb-c20171ea0896",
        "x-api-key": "my-secret-key",
        "x-geora-actor":
            "YWN0b3J8NTBkNTQ2M2EtZjRmMS00MmUzLTllYmItYzIwMTcxZWEwODk2Cg==",
    };

    const id: string = "YXNzZXRWZXJzaW9ufDI=";

    const options: RequestInit = {
        method: "POST",
        headers,
        body: JSON.stringify({
            query: ASSET_VERSION_QUERY,
            variables: { id },
        }),
    };

    return fetch(url, options);
}

async function main(): Promise<JSON> {
    const response: Response = await query();
    return await response.json();
}

main().then((res: JSON) => console.log(res["data"]));
# Python

import requests

ASSET_VERSION_QUERY = """
query ($id: ID!) {
    assetVersion(id: $id) {
        id
        class
        quantity {
            primaryUnit
            primaryValue
        }
    }
}
"""

def query():
    url = "https://api.geora.io/v2"

    headers = {
        "Content-Type": "application/json",
        "x-api-user": "50d5463a-f4f1-42e3-9ebb-c20171ea0896",
        "x-api-key": "my-secret-key",
        "x-geora-actor": "YWN0b3J8NTBkNTQ2M2EtZjRmMS00MmUzLTllYmItYzIwMTcxZWEwODk2Cg=="
    }

    id = "YXNzZXRWZXJzaW9ufDI="

    request = requests.post(
        url,
        json={
            "query": ASSET_VERSION_QUERY,
            "variables": { "id": id }
        },
        headers=headers
    )
    return request.json()

result = query()
print(f"{result['data']}")
-- Elm

type Msg = LoadedActor Actor
loadActor : Config -> Id -> Cmd Msg
loadActor cfg id = makeQuery cfg LoadedActor (selActor id)
type Actor = Actor Id String Email
selActor : Id -> SelectionSet Actor RootQuery
selActor id =
    Query.actor { id = id }
        (SelectionSet.succeed Actor
            |> with GeoraAPI.Object.Actor.id
            |> with (GeoraAPI.Object.Actor.name |> nonNullOrFail)
            |> with
                (GeoraAPI.Object.Actor.email
                    GeoraAPI.Object.EmailInfo.email
                    |> nonNullOrFail))
        |> nonNullOrFail

See our article here for a technical deep dive on Geora's GraphQL API.

4 Basics

This section explains some of the common ideas that are shared across the Geora API.

scalar TimestampUTC

domain-helpers

4.1 Node

You can think of the Geora protocol as a graph, containing objects and their links to each other. For example, an asset version is linked to actors by ownership and permissions, and files are linked to assets by evidence attachments.

The Node interface is implemented by all objects (or vertices) in this graph:

The type for each of these vertices extends Node.

interface Node {
    id: ID!
}

schema

The node ID is a globally unique identifier for the vertex. Using unique IDs permits a standardised refetch query called node, which can be used to efficiently implement UI caching and refetching.

4.2 Connection

A connection is used where a query or field returns a list of objects; it provides cursor-based pagination in an efficient way. A connection contains a list of edges, each linking to another node. For example, referring to a

interface Connection {
    # edges: [Edge!]!  -- commented because GraphQL doesn't let us write this
    pageInfo: PageInfo!
}

schema

Any type ending in Connection must provide these fields:

Queries that return a Connection support forward pagination via the first and after arguments:

The PageInfo object provides metadata that helps the client determine whether there are more pages to iterate through. It has fields startCursor and endCursor which provide the cursor values of the first and last edges in the list.

type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    totalPages: Int
    startCursor: Cursor
    endCursor: Cursor
}

schema

Cursors are opaque base64-encoded strings which uniquely identify an edge in the connection.

scalar Cursor

schema

4.2.1 Edge

An edge represents one item in the paginated list and must provide these fields:

interface Edge {
    # node: Node!  -- commented because graphql doesn't let us write this.
    cursor: Cursor!
}

schema

4.2.2 Using connections

It helps to see a concrete example of using a connection. In this case, the caller wants to see all assets that they own. They use the actor > assets query, which returns a connection.

To see the first page of assets, they request 10 items in the connection:

{
  actor(id: "YWN0b3J8NzJjN2U5YzMtOGViNC00ZjIyLWE5NDktYjljMTVkYTUzNzYw") {
    assets(first: 10) {
      pageInfo {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }

      edges {
        cursor
        node {
          id
        }
      }
    }
  }
}

This returns the ten assets under the edges key, as well as a PageInfo object that looks like:

{
    "hasNextPage":true,
    "hasPreviousPage":false,
    "startCursor":"YXNzZXRWZXJzaW9ufDg3MjM0ODk3Mjg5NDcyMwo=",
    "endCursor":"YXNzZXRWZXJzaW9ufDg3MzIxMjQyMzQzMjQK"
}

From the response, the caller can see that there is at least one more page of assets available. For the next query, they set the after argument to the value of endCursor, in order to see the next ten assets.

{
  actor(id: "YWN0b3J8NzJjN2U5YzMtOGViNC00ZjIyLWE5NDktYjljMTVkYTUzNzYw") {
    assets(first: 10, after: "YXNzZXRWZXJzaW9ufDg3MzIxMjQyMzQzMjQK") {
      pageInfo {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }

      edges {
        cursor
        node {
          id
        }
      }
    }
  }
}

When this query returns, hasNextPage is false and there are no more assets to view.

4.3 Standards

The Geora API implements the following GraphQL standards:

5 Domain model

This section describes the various objects exposed by the Geora API; in particular the top-level objects that inherit from the Node interface.

5.1 Assets

An Asset consists of an ID and a list of asset versions.

<<domain-helpers>>

type Asset implements Node {
    id: ID! @id(t: "asset")
    latestVersion: AssetVersion
    <<domain-asset-methods>>
}

schema

The versions connection returns a list of asset versions with the queried asset ID, in reverse chronological order (so the latest version will appear first). Since querying for the latest version of an asset is very common, there is already a latestVersion field which dynamically points to this version.

versions(
    first: Int
    after: Cursor
): VersionConnection @c(name: "get_asset_asset_versions_connection")

domain-asset-methods

type VersionConnection implements Connection {
    pageInfo: PageInfo!
    edges: [VersionEdge!]!
}

type VersionEdge implements Edge {
    cursor: Cursor!
    node: AssetVersion!
}

domain-helpers

assetStandards: [AssetStandard!]! @f(name: "get_asset_asset_standards")

domain-asset-methods

5.2 Asset versions

Each AssetVersion contains the state of the asset at that point in its history.

type AssetVersion implements Node {
    id: ID! @id(t: "assetVersion")
    <<domain-asset-version-properties>>
    <<domain-asset-version-methods>>
}

schema

All assets contain some basic identifying information:

asset: Asset! @f(name: "get_asset_version_asset")
class: String! @f(name: "get_asset_version_class")
quantity: Quantity! @f(name: "get_asset_version_quantity")

domain-asset-version-properties

collectedQuantity(unit: String): [Quantity!]! @f(name: "get_asset_version_collected_quantity") @beta

domain-asset-version-properties

type Quantity {
    primaryValue: Float!
    primaryUnit: String!
}

domain-helpers

If an asset version has an asset standard set, it can be accessed here.

assetStandards: [AssetStandard!]! @f(name: "get_asset_version_asset_standards") @alpha

domain-asset-version-properties

The AssetVersionStatus indicates whether an asset has been burned (made read-only). In the future, more asset statuses may be added as options in this enum, such as an IN_ESCROW status for finanical agreements.

status: AssetVersionStatus! @f(name: "get_asset_version_status")

domain-asset-version-properties

enum AssetVersionStatus {
    ACTIVE
    BURNED
}

domain-helpers

The following fields are deprecated and exist only for legacy users.

isVerified: Boolean! @f(name: "get_asset_version_is_verified") @deprecated(reason: "No longer required by privacy system")
rootHash: String @f(name: "get_asset_version_root_hash") @deprecated(reason: "No longer required by privacy system")

domain-asset-version-properties

Any actor that has a relationship of some sort will be present in the actorRelationships connection. A relationship is any of:

actorRelationships(
  first: Int
  after: Cursor
): ActorRelationshipConnection @c(name: "get_asset_version_actor_relationships_connection", hasEdgeData: true, transform: true)

domain-asset-version-methods

type ActorRelationshipConnection implements Connection {
    pageInfo: PageInfo!
    edges: [ActorRelationshipEdge!]!
}

domain-helpers

The connection contains the related actor as its node, but additionally adds relationship data to the edges, codified in the ActorRelationshipEdge type.

ownershipPercentage is a percentage value between 0 and 1, but is DEPRECATED. Due to the behaviour of the GraphQL Float type, in some scenarios you will experience rounding errors which cause ownership transfers to fail. Please use exactOwnershipPercentage instead.

scalar Decimal

domain-helpers

exactOwnershipPercentage is a percentage value between 0 and 1. This has a different internal representation to ownershipPercentage.

permissions is a list of permissions granted to the actor on the queried version.

type ActorRelationshipEdge implements Edge {
    cursor: Cursor!
    node: Actor!
    isOwner: Boolean!
    isManager: Boolean!
    isCustodian: Boolean!
    ownershipPercentage: Float!
    exactOwnershipPercentage: Decimal!
    permissions: [AssetVersionPermission!]!
}

domain-helpers

Actor permissions on asset versions are represented by the AssetVersionPermission object. Currently, this object only contains one field: the permission type. Using an object allows Geora to expand the permissioning system in the future without breaking backwards compatibility.

The supported permissions are:

enum AssetVersionPermissionType {
    SET_PERMISSION
    CHANGE_CLAIMS
    CHANGE_COLLECTION
    CHANGE_CLASS
    CHANGE_IS_BURNT
    CHANGE_PARENTS
    CHANGE_CUSTODIAN
    USE_AS_PARENT
    USE_IN_COLLECTION
    VIEW_ASSET
}

type AssetVersionPermission {
    permission: AssetVersionPermissionType
}

domain-helpers

Asset versions are related to other asset versions on different assets by relationships like collections or parenthood.

A collection is an asset which contains one or more collected assets. This represents scenarios like a grain silo; where the silo is an asset containing multiple barley assets.

A parent is an abstract "precursor" asset to the current asset. This can be used to represent processing, as in the processing of hops to make beer, or the merging of multiple assets into one.

assetVersionRelationships(
    first: Int
    after: Cursor
): AssetVersionRelationshipConnection @c(name: "get_asset_version_asset_relationships_connection", hasEdgeData: true)

domain-asset-version-methods

type AssetVersionRelationshipConnection implements Connection {
    pageInfo: PageInfo!
    edges: [AssetVersionRelationshipEdge!]!
}

type AssetVersionRelationshipEdge implements Edge {
    cursor: Cursor!
    node: AssetVersion!
    relationship: AssetVersionRelationshipType!
}

domain-helpers

Each asset relationship edge contains the relationship type. When viewing the asset, the bidirectional nature of relationships is shown; when asset A COLLECTS asset B, asset B is COLLECTED_BY asset A, and when asset C is a PARENT_OF asset D, asset D is a CHILD_OF asset C.

When creating relationships, AssetVersionRelationshipInputType is used instead.

enum AssetVersionRelationshipType {
    COLLECTS
    COLLECTED_BY
    PARENT_OF
    CHILD_OF
}

enum AssetVersionRelationshipInputType {
    COLLECTS
    CHILD_OF
}

domain-helpers

Assets may have claims made against them, which are key-value pairs asserting information about the asset. Substantiated claims are backed up by file attachments called evidence.

claims(
    first: Int
    after: Cursor
): ClaimConnection! @c(name: "get_asset_version_claims_connection", transform: true)

attachments: [Attachment!]! @f(name: "get_asset_version_attachments", transform: true)

domain-asset-version-methods

type ClaimConnection implements Connection {
    pageInfo: PageInfo!
    edges: [ClaimEdge!]!
}

type ClaimEdge implements Edge {
    cursor: Cursor!
    node: Claim!
}

domain-helpers

Updates provide the link to past versions of an asset. On UpdateEdge, the location will always be PAST, representing the update occurring backwards in time in relation to the queried version. Only successful updates are shown in this connection, and are listed in reverse chronological order.

updates(
    first: Int
    after: Cursor
): AssetUpdatesConnection
@c(name: "get_asset_version_updates_connection",
   orderableOnNode: [
     {field: "submittedAt", orderAs: DATE}
   ],
   hasEdgeData: true,
   transform: true)

domain-asset-version-methods

type AssetUpdatesConnection implements Connection {
    pageInfo: PageInfo!
    edges: [AssetUpdateEdge!]!
}

type AssetUpdateEdge implements Edge {
    cursor: Cursor!
    node: Update!
    location: UpdateTemporalLocation
}

domain-helpers

Updates in the PAST were made before (further back in time) than the currently-queried version, and assets in the FUTURE were made after. The update which created the currently-queried version will be the newest asset in the PAST.

enum UpdateTemporalLocation {
    PAST
    FUTURE
}

domain-helpers

The certifications of an asset are found in the certifications connection.

certifications(
  first: Int
  after: Cursor
): CertificationConnection @c(name: "get_asset_version_certification_connection", hasEdgeData: true)

domain-asset-version-methods

type CertificationConnection implements Connection {
    pageInfo: PageInfo!
    edges: [AssetCertificationEdge!]!
}

type AssetCertificationEdge implements Edge {
    cursor: Cursor!
    node: Registry!
    certifications: [CertificationStatus!]!
}

domain-helpers

5.3 Claims and evidence

A claim is a key-value pair asserting some property of an asset, which may be backed by evidence files. Claim labels are case-insensitive, and the API gives no guarantees on the case of the returned label. Claims differ from the other types by not implementing the Node interface.

type Claim {
    id: ID!
    label: String!
    value: String!
    evidence(first: Int, after: Cursor): EvidenceConnection @c(name: "get_claim_evidence_connection", transform: true)
}

type Attachment {
    name: String!
    url: URL!
}

schema

type EvidenceConnection implements Connection {
    pageInfo: PageInfo!
    edges: [EvidenceEdge!]!
}

type EvidenceEdge implements Edge {
    cursor: Cursor!
    node: File!
}

domain-helpers

5.4 Updates

An Update represents a change in the asset’s history that links together two versions.

type Update implements Node {
    id: ID! @id(t: "update")
    change: String!
    submittedAt: TimestampUTC!
    submittedBy: Actor!
    result: UpdateResult!
}

schema

If the status is SUCCESS, versionBefore and versionAfter will link to the versions before the update was created and after it was applied respectively. If the calling actor does not have the VIEW_ASSET permission on either of these asset versions, the respective field will be null.

If the status is ERROR, error will provide a description of the error.

enum UpdateStatus {
    SUCCESS
    ERROR
    PENDING
}

domain-helpers

interface UpdateResult {
    status: UpdateStatus!
}

type UpdateSuccess implements UpdateResult {
    status: UpdateStatus!
    transactionHash: String!
    versionBefore: AssetVersion @f(name: "get_update_success_version_before", nullOnNotFound: true, transform: true)
    versionAfter: AssetVersion @f(name: "get_update_success_version_after", nullOnNotFound: true, transform: true)
}

type UpdatePending implements UpdateResult {
    status: UpdateStatus!
}

type UpdateError implements UpdateResult {
    status: UpdateStatus!
    error: String
}

domain-helpers

5.5 Files

Any actor can upload files to be used as evidence on asset claims.

issuer links to the actor who uploaded the file.

All files are stored in IPFS, and ipfsHash will return the IPFS multihash string of the file in the Geora private IPFS network (such as QmZJ1xT1T9KYkHhgRhbv8D7mYrbemaXwYUkg7CeHdrk1Ye).

To view the file, use the url file. This will generate a temporary link to the file in centralised storage. The link will only last for ten minutes.

type File implements Node {
    id: ID! @id(t: "file")
    issuer: Actor
    ipfsHash: IPFSHash
    url: URL

    actorRelationships(
      first: Int
      after: Cursor
    ): FileActorRelationshipConnection @c(name: "get_file_actor_relationships_connection", hasEdgeData: true)
}

type FileActorRelationshipConnection implements Connection {
    pageInfo: PageInfo!
    edges: [FileActorRelationshipEdge!]!
}

type FileActorRelationshipEdge {
    cursor: Cursor!
    node: Actor!
    permissions: [FilePermission!]!
}

schema

The actorRelationships connection lists actors with one or more permissions on the file. The available permissions are:

type FilePermission {
    permission: FilePermissionType
}
enum FilePermissionType {
    SET_PERMISSION
    VIEW_FILE
    USE_FILE
}

schema

scalar IPFSHash
scalar URL

domain-helpers

5.6 Actors

The Actor node provides the actor's name and auto-generated Ethereum address.

type Actor implements Node {
    id: ID! @id(t: "actor")
    name: String! @f(name: "get_actor_name")
    address: EthereumAddress @f(name: "get_actor_address")
    inventory: Float! @f(name: "get_actor_inventory")
    email: EmailInfo @f(name: "get_actor_email", nullOnNotFound: true)

    <<domain-actor-methods>>
}

schema

scalar EthereumAddress

domain-helpers

You can access all the assets that an actor has ownership of (i.e. ownershipPercentage > 0) with the assets connection.

assets(
    first: Int
    after: Cursor
): ActorAssetsConnection @c(name: "get_actor_assets_connection")

domain-actor-methods

type ActorAssetsConnection implements Connection {
    pageInfo: PageInfo!
    edges: [ActorAssetEdge!]!
}

type ActorAssetEdge implements Edge {
    cursor: Cursor!
    node: Asset!
}

domain-helpers

We can list all the claims of an actor using the claims field.

claims(
    first: Int
    after: Cursor
): ActorClaimConnection @c(name: "get_actor_claims_connection")

domain-actor-methods

Unlike Claims on assets, there is no evidence as yet on ActorClaims.

type ActorClaim {
    id: ID! @id(t: "claim")
    label: String!
    value: String!
}

schema

type ActorClaimConnection implements Connection {
    pageInfo: PageInfo!
    edges: [ActorClaimEdge!]!
}

type ActorClaimEdge implements Edge {
    cursor: Cursor!
    node: ActorClaim!
}

domain-helpers

We can get all the registries an actor belongs to, along with their current certification status.

registries(
  first: Int, after: Cursor
): ActorRegistryConnection @c(name: "get_actor_registries_connection", hasEdgeData: true) @requiresActor

domain-actor-methods

type ActorRegistryConnection implements Connection {
    pageInfo: PageInfo!
    edges: [ActorRegistryEdge!]!
}

type ActorRegistryEdge implements Edge {
    cursor: Cursor!
    status: CertificationStatus
    userType: RegistryUserType!
    node: Registry!
}

domain-helpers

The edge's status field will be null if the userType is ISSUER.

We can get the certification history of a user.

history(
  first: Int, after: Cursor
): CertificationHistoryConnection @c(name: "get_actor_history_connection") @requiresActor

domain-actor-methods

And their production details.

production(
  first: Int, after: Cursor
): ProductionConnection @c(name: "get_actor_production_connection") @requiresActor @beta

domain-actor-methods

The actor's status can be certified, uncertified, or revoked, and each case gives different data. If they're certified the issuer who granted it, when it was granted, and when it expires is given. If they're uncertified the time when the last certification expired is given, if they've never been certified this is null. If their certification was revoked then the timestamp and the issuer who revoked it is given.

union CertificationStatus =
  Certified | PreCertified | Uncertified | Revoked | Pending

type Certified {
  status: CertificationStatusType!
  certificate: File!
  user: Actor!
  issuer: Actor!
  granted: TimestampUTC!
  expires: TimestampUTC!
}

type PreCertified {
  status: CertificationStatusType!
  certificate: File!
  user: Actor!
  issuer: Actor!
  granted: TimestampUTC!
  expires: TimestampUTC!
}

type Uncertified {
  status: CertificationStatusType!
  user: Actor!
  expired: TimestampUTC
}

type Revoked {
  status: CertificationStatusType!
  user: Actor!
  issuer: Actor!
  timestamp: TimestampUTC!
}

type Pending {
  status: CertificationStatusType!
  user: Actor!
}


enum CertificationStatusType {
  CERTIFIED
  PRE_CERTIFIED
  UNCERTIFIED
  REVOKED
  PENDING
}

domain-helpers

Email contact information, and whether the address has been successfully verified.

scalar Email

type EmailInfo {
  email: Email!
  isVerified: Boolean!
}

domain-helpers

5.7 Registries

Registries have issuers and users, issuers manage the certification status of the users. The registry can also be queried for the overall production statistics.

type Registry {
  id: ID! @id(t: "registry")
  name: String!
  description: String!
  certificationName: String!
  theme: RegistryTheme! @f(name: "get_registry_theme") @requiresActor

  verification: VerificationResult!

  requiredClaims: [String!]! @f(name: "get_registry_required_claims")

  actors(
    first: Int, after: Cursor
  ): RegistryActorConnection
  @c(name: "get_registry_actors_connection",
     hasEdgeData: true,
     orderableOnEdge: [
       {field: "certificationStatus", orderAs: STRING},
       {field: "issued", orderAs: DATE},
       {field: "expiry", orderAs: DATE},
       {field: "daysUntilExpiry", orderAs: FLOAT}
     ],
     orderableOnNode: [{field: "name", orderAs: STRING}])
  @requiresActor

  production(
    first: Int, after: Cursor
  ): ProductionConnection @requiresActor
}

schema

There are three types of user in a registry. An issuer can issue certificates to users, and users have a certification status. If the current user is an admin user they will have ADMIN access at least to every registry if they don't already have one of the other two levels.

enum RegistryUserType {
  USER
  ISSUER
  ADMIN
}

domain-helpers

type RegistryActorConnection implements Connection {
  pageInfo: PageInfo!
  edges: [RegistryActorEdge!]!
}

type RegistryActorEdge implements Edge {
  cursor: Cursor!
  status: CertificationStatus
  userType: RegistryUserType!
  node: Actor!
}

domain-helpers

The edge's status field will be null if the userType is ISSUER.

The issuer can invite new users to a registry.

type Invitation {
  registry: ID! @id(t: "registry"),
  user: ID! @id(t: "actor")
}

schema

5.7.1 Registry Verification

When registries are initially created, they are not verified in the Geora system. Unverified registries have a limit to the number of users that can be created and participate in the registry. Once the registry is verified by Geora, this limit is removed.

interface VerificationResult {
    status: Boolean!
}

type Verified implements VerificationResult {
  status: Boolean!
  verifier: Actor!
  timestamp: TimestampUTC!
  comment: String!
}

type Unverified implements VerificationResult {
  status: Boolean!
}

domain-helpers

5.8 Asset Lenses

An asset lens is a view / set of permissions which can be applied to any number of assets. Each lens has a name and a description; the user can choose to show asset's class, value, and ownership information. Optionally, time bounds on asset events can be set, and a list of claims which will be shown through the lens.

union LensBound =
    TimestampBound | IndexBound

type TimestampBound {
  timestamp: TimestampUTC!
}

type IndexBound {
  index: Int!
}

type Lens {
  id: ID! @id(t: "lens")
  name: String!
  description: String!
  showClass: Boolean!
  showQuantity: Boolean!
  showOwnership: Boolean!
  earliest: LensBound
  latest: LensBound
  claimsWhitelist: [LensClaimApproval!]!
  isActivated: Boolean!
}

schema

The claimsWhitelist has some unique behaviour when creating or updating an lens.

When creating with lensCreate, not specifying the claimsWhitelist means that the lens will not show any claims on this asset or, in other words, the claimsWhitelist is empty.

When updating with lensUpdate, not specifying the claimsWhitelist will leave the whitelist untouched. This has 2 consequences:

input LensInput {
  name: String!
  description: String!
  showClass: Boolean!
  showQuantity: Boolean!
  showOwnership: Boolean!
  earliestTimestamp: TimestampUTC
  latestTimestamp: TimestampUTC
  earliestIndex: Int
  latestIndex: Int
  claimsWhitelist: [LensClaimApprovalInput!]
}

schema

When a lens has been created, it can be applied to one or more assets to create lens instances. When viewing these instances, the lens filters out the information visible on the asset. A lens can be deactivated and reactivated, and we keep track of various statistics around people using the lens. Finally, visitors can leave messages for the owner of the lens to read, and they can leave a return email address if they wish.

type LensInstance {
  id: ID! @id(t: "lens-instance")
  lens: Lens!
  visits: Int!
  lastVisitAt: TimestampUTC
  messages: [LensMessage!]!
  createdAt: TimestampUTC!

  <<domain-lens-methods>>
}

schema

latestVersion: AssetVersion
@f(name: "get_lens_instance_latest_asset_version", transform: true, nullOnNotFound: true) @beta

domain-lens-methods

versions(first: Int after: Cursor):
  VersionConnection!
  @c(name: "get_lens_instance_asset_versions_connection", transform: true) @beta

domain-lens-methods

The UpdatesConnection in LensInstances only contains successful updates.

updates(first: Int, after: Cursor):
  UpdatesConnection!
  @c(name: "get_lens_instance_asset_updates_connection", transform: true) @beta

domain-lens-methods

The decision of whether to show claim data is done in a "white-list" fashion, in that only the explicitly approved claims are revealed. If any claims are present that the lens owner failed to anticipate then they will not be visible. For each claim the lens owner can also say whether users should be able to view the evidence files attached to the claim.

type LensClaimApproval {
  label: String!
  showEvidence: Boolean!
}

input LensClaimApprovalInput {
  label: String!
  showEvidence: Boolean!
}

schema

type LensMessage {
  senderEmail: String
  message: String!
  timestamp: TimestampUTC!
}

schema

5.9 Asset Standards

An asset standard is used to build assets which are compatible across multiple users and their supply chains. It consists of a set of required and optional claims, transformations, and metadata that define the "shape" of an asset.

type AssetStandardClaim {
  label: String!
  required: Boolean!

  humanLabel: String
  humanDescription: String
  exampleValues: [String!]!
  defaultValue: String

  claimGroup: String
}

type AssetStandardStage {
  id: ID! @id(t: "assetStandardStage")
  name: String!
  claims: [AssetStandardClaim!]!
  representativeClaim: String
}

type AssetStandard {
  id: ID! @id(t: "assetStandard")
  name: String!
  description: String!

  createdBy: Actor!

  usageStatus: AssetStandardUsageStatus!

  stages: [AssetStandardStage!]!

  createdAt: TimestampUTC!
}

schema

The asset standard usage status enum is useful for filtering and sorting the most relevant standards for a user. IN_USE_BY_CALLER means that the calling actor is the owner of at least one asset that uses this asset standard. CREATED_BY_CALLER means that the calling actor is the creator of the standard. AVAILABLE is all other asset standards.

enum AssetStandardUsageStatus {
  IN_USE_BY_CALLER
  CREATED_BY_CALLER
  AVAILABLE
}

schema

The corresponding input types:

input AssetStandardClaimInput {
  label: String!
  required: Boolean!

  humanLabel: String
  humanDescription: String
  exampleValues: [String!]
  defaultValue: String

  claimGroup: String
}

input AssetStandardStageInput {
  name: String!
  claims: [AssetStandardClaimInput!]
  representativeClaim: String
}

input AssetStandardInput {
  name: String!
  description: String!
  stages: [AssetStandardStageInput!]
}

schema

6 Queries

GraphQL queries read data from the API.

Singular queries return an object by its ID. They work the same as the Node query but don’t require fragment matching on returned type. If the caller does not have permission to view the object, the query will return null and an error.

Plural queries return a Connection.

<<query-helpers>>

scalar TypedID

type Query {
    node(id: TypedID!): Node @f(name: "get_node") @requiresActor
    <<query-queries>>
}

schema

6.1 Asset

The asset query will return a single asset, if the caller has the VIEW_ASSET permission on the latest version of that asset.

The assets query will return all assets for which the caller has the VIEW_ASSET permission on the latest version.

asset(id: ID!): Asset @f(name: "get_asset") @requiresActor
assets(first: Int, after: Cursor): AssetsConnection!
  @c(name: "get_assets_connection")
  @requiresActor

query-queries

type AssetsConnection implements Connection {
    pageInfo: PageInfo!
    edges: [AssetEdge!]!
}

type AssetEdge implements Edge {
    cursor: Cursor!
    node: Asset!
}

query-helpers

6.2 Asset version

The assetVersion query will return a single asset version, if the caller has the VIEW_ASSET permission.

The assetVersions query will return all assets versions for which the caller has the VIEW_ASSET permission.

assetVersion(id: ID!): AssetVersion @f(name: "get_asset_version") @requiresActor
assetVersions(first: Int, after: Cursor, latestOnly: Boolean = false):
  AssetVersionsConnection!
  @c(name: "get_asset_versions_connection",
      hasEdgeData: true,
      orderableOnNode: [{field: "class", orderAs: STRING}],
      orderableOnEdge: [{field: "primaryValue", orderAs: FLOAT}])
  @requiresActor

query-queries

type AssetVersionsConnection implements Connection {
    pageInfo: PageInfo!
    edges: [AssetVersionEdge!]!
}

type AssetVersionEdge implements Edge {
    cursor: Cursor!
    node: AssetVersion!
}

query-helpers

6.3 Actor

The actor query will return a single actor.

The actors query will return all actors that were created by the calling user.

actor(id: ID!): Actor @f(name: "get_actor") @requiresActor
actors(first: Int, after: Cursor): ActorsConnection! @c(name: "get_actors_connection") @requiresActor

query-queries

type ActorsConnection implements Connection {
    pageInfo: PageInfo!
    edges: [ActorEdge!]!
}

type ActorEdge {
    cursor: Cursor!
    node: Actor!
}

query-helpers

6.4 Update

The update query will return a single update if the caller can view that update.

The updates query will return all updates that a caller can view.

A calling actor can view an update if either of the following conditions are met:

update(id: ID!): Update @f(name: "get_update") @requiresActor
updates(first: Int, after: Cursor): UpdatesConnection! @c(name: "get_updates_connection") @requiresActor

query-queries

type UpdatesConnection implements Connection {
    pageInfo: PageInfo!
    edges: [UpdateEdge!]!
}

type UpdateEdge implements Edge {
    cursor: Cursor!
    node: Update!
}

query-helpers

6.4.1 History

The history of the user's certification status is of interest, and can be fully queried. Any event such as granting, revoking, or extending certification is revealed here.

type CertificationHistory {
  event: CertificationHistoryEvent!
  registry: Registry!
  issuer: Actor!
  notes: String!
  timestamp: TimestampUTC!
}

schema

enum CertificationHistoryEvent {
  CERTIFICATION_GRANTED
  CERTIFICATION_EXPIRED
  CERTIFICATION_EXTENDED
  CERTIFICATION_REVOKED
}

domain-helpers

type CertificationHistoryConnection implements Connection {
  pageInfo: PageInfo!
  edges: [CertificationHistoryEdge!]!
}

type CertificationHistoryEdge implements Edge {
  cursor: Cursor!
  node: CertificationHistory!
}

domain-helpers

6.4.2 Production

The quantity in units and weight of TTO assets produced can be queried, aggregating over various time periods as given by Granularity, grouped by certification status.

type Production {
  dateFrom: Date!
  dateTo: Date!
  assetsCertified: Int!
  assetsUncertified: Int!
  assetsRevoked: Int!
  kgCertified: Float!
  kgUncertified: Float!
  kgRevoked: Float!
  kgSold: Float!
  kgPurchased: Float!
}

schema

enum Granularity {
  PER_DAY
  PER_WEEK_MONDAY
  PER_WEEK_SUNDAY
  PER_MONTH
  PER_QUARTER
}

domain-helpers

The where defaults are from the beginning of time up to the current time, and PER_DAY granularity.

type ProductionConnection implements Connection {
  pageInfo: PageInfo!
  edges: [ProductionEdge!]!
}

type ProductionEdge implements Edge {
  cursor: Cursor!
  node: Production!
}

domain-helpers

6.5 Notifications

Give system users access to notifications addressed to them. A message may not have a from field if it has come from an user unknown to the user. The metaData field is a JSONified string which can be an empty JSON object.

type Notification {
  id: ID! @id(t: "notification")
  from: Actor
  message: String!
  metaData: String!
  timestamp: TimestampUTC!
}

schema

notifications(
  first: Int, after: Cursor
): NotificationConnection!
     @c(name: "get_notifications_connection", ascending: true)
     @requiresActor

query-queries

type NotificationConnection implements Connection {
  pageInfo: PageInfo!
  edges: [NotificationEdge!]!
}

type NotificationEdge implements Edge {
  cursor: Cursor!
  node: Notification!
}

domain-helpers

We can set notifications as having been seen by the user so they won't keep seeing the same ones.

setNotificationsSeen(
  ids: [ID!]!
): SetNotificationsSeenResult!
     @f(name: "set_notifications_seen")
     @requiresActor

mutation-methods

type SetNotificationsSeenResult {
  updated: [ID!]!
}

domain-helpers

6.6 File

The file query will return a single file if the caller has the VIEW_FILE permission.

The files query will return all files for which the caller has the VIEW_FILE permission.

file(id: ID!): File @f(name: "get_file") @requiresActor
files(first: Int, after: Cursor): FilesConnection! @c(name: "get_files_connection") @requiresActor

query-queries

type FilesConnection implements Connection {
    pageInfo: PageInfo!
    edges: [FileEdge!]!
}

type FileEdge implements Edge {
    cursor: Cursor!
    node: File!
}

query-helpers

6.7 Registries

registry(id: ID!): Registry
  @f(name: "get_registry")
  @requiresActor

registries(
  first: Int, after: Cursor
): RegistryConnection!
  @c(name: "get_registries_connection",
     hasEdgeData: true,
     orderableOnEdge: [
       {field: "status", orderAs: STRING},
       {field: "expiry", orderAs: DATE}
     ],
     orderableOnNode: [{field: "name", orderAs: STRING}])
  @requiresActor

query-queries

type RegistryConnection implements Connection {
  pageInfo: PageInfo!
  edges: [RegistryEdge!]!
}

type RegistryEdge implements Edge {
  cursor: Cursor!
  status: CertificationStatus
  userType: RegistryUserType!
  node: Registry!
}

domain-helpers

The edge's status field will be null if the userType is ISSUER.

6.8 Lenses

lens(id: ID!): Lens!
  @f(name: "get_lens")
  @requiresActor @beta

lenses(
  first: Int, after: Cursor
): LensConnection!
  @c(name: "get_lenses_connection")
  @requiresActor @beta

lensInstance(id: ID!): LensInstance!
  @f(name: "get_lens_instance", transform: true) @beta

lensInstances(
  first: Int, after: Cursor
): LensInstanceConnection!
  @c(name: "get_lens_instances_connection")
  @requiresActor @beta

query-queries

type LensConnection implements Connection {
  pageInfo: PageInfo!
  edges: [LensEdge!]!
}

type LensEdge implements Edge {
  cursor: Cursor!
  node: Lens!
}

type LensInstanceConnection implements Connection {
    pageInfo: PageInfo!
    edges: [LensInstanceEdge!]!
  }

type LensInstanceEdge implements Edge {
    cursor: Cursor!
    node: LensInstance!
}

domain-helpers

6.9 Asset standards

The assetStandard query will return a single asset standard.

The assetStandards query will return all asset standards and their status. It can be sorted and filtered by status to return only those standards in use or created by the caller.

assetStandard(id: ID!): AssetStandard @f(name: "get_asset_standard") @requiresActor @alpha
assetStandards(first: Int, after: Cursor): AssetStandardConnection!
  @c(name: "get_asset_standards_connection", orderableOnNode: [{field: "usageStatus", orderAs: STRING}])
  @requiresActor
  @alpha

query-queries

type AssetStandardConnection implements Connection {
  pageInfo: PageInfo!
  edges: [AssetStandardEdge!]!
}

type AssetStandardEdge implements Edge {
  cursor: Cursor!
  node: AssetStandard!
}

domain-helpers

6.10 Filtering and ordering connections

Coming soon.

The Date type represents a date as an ISO-8601 string, like "2006-08-14T02:34:56-06:00".

scalar Date

schema

input WhereClauseString {
    _eq: String
    _neq: String
    _gt: String
    _gte: String
    _lt: String
    _lte: String
    _like: String
}

input WhereClauseID {
    _eq: ID
    _neq: ID
    _gt: ID
    _gte: ID
    _lt: ID
    _lte: ID
}

input WhereClauseInt {
    _eq: Int
    _neq: Int
    _gt: Int
    _gte: Int
    _lt: Int
    _lte: Int
}

input WhereClauseFloat {
    _eq: Float
    _neq: Float
    _gt: Float
    _gte: Float
    _lt: Float
    _lte: Float
}

input WhereClauseBoolean {
    _eq: Boolean
    _neq: Boolean
}

enum OrderBy {
    ASCENDING
    DESCENDING
}

input Orderable {
    field: String!
    orderAs: OrderAs!
}

enum OrderAs {
    STRING
    FLOAT
    DATE
}

schema

7 Mutations

Mutations make updates to the system state. All mutations are atomic.

<<mutation-helpers>>

type Mutation {
    <<mutation-methods>>
}

schema

7.1 Users

To reset the calling user's API key and return a new autogenerated key, use userResetKey.

userResetKey: String!

mutation-methods

7.2 Actors

You can create an actor by providing a name, and update them by changing their name. The actorCreate mutation is the only mutation or query that doesn't require a calling actor to be specified with the x-geora-actor header key.

actorCreate(input: ActorCreateInput!): Actor
actorUpdate(id: ID!, input: ActorUpdateInput!): Actor @requiresActor

mutation-methods

input ActorCreateInput {
  name: String!
  claims: [ActorClaimInput!]
  registries: [ID!]
  email: String
}

mutation-helpers

input ActorUpdateInput {
  name: String
  claims: [ActorClaimInput!]
}

mutation-helpers

Claims which are being provided to can have absent or null ids, which means a claim with the label given is expected to not yet exist on the actor. If such a claim does exist an error is thrown. If the id given does not match the current latest version id then an error is also thrown.

An input claim can also have a null value, in which case the claim will be deleted if the provided id is the latest id.

input ActorClaimInput {
    id: ID
    label: String!
    value: String
}

mutation-helpers

7.3 Assets

Assets are created and updated using the flexible AssetUpdateInput type.

assetCreate(input: AssetUpdateInput!): Update @f(name: "get_update", afterDefault: true) @requiresActor
assetUpdate(versionID: ID!, input: AssetUpdateInput!): Update @f(name: "get_update", afterDefault: true) @requiresActor

mutation-methods

input AssetUpdateInput {
    class: String
    quantity: QuantityInput

    claims: [ClaimInput!]

    actorRelationships: [ActorRelationshipInput!]
    assetVersionRelationships: [AssetVersionRelationshipInput!]

    assetStandard: ID
}

input ActorRelationshipInput {
    id: ID!
    permissions: [AssetVersionPermissionInput!]!
    isCustodian: Boolean!
    isManager: Boolean!
    ownershipPercentage: Float
    exactOwnershipPercentage : Decimal
}

input AssetVersionPermissionInput {
    permission: AssetVersionPermissionType!
}

input AssetVersionRelationshipInput {
    id: ID!
    relationship: AssetVersionRelationshipInputType!
}

mutation-helpers

Calling any mutation that creates a new asset version will return an Update, with an initial status of pending. When the update has been processed, its status will change to successful and the new version returned by the update query.

When creating an asset, any omitted field will receive the default values.

When updating the asset, any omitted field will be unchanged in the update. Updating assets using the assetUpdate mutation allows you to make multiple changes in the same update, but performs replacement on the provided collections. For example, providing an actorRelationships array in assetUpdate will replace all existing relationships. If this is not desired, use the more granular update functions below.

Any mutations to an asset are rejected if the asset status is BURNED.

QuantityInput is used to ensure that both quantity fields are set together or not at all.

input QuantityInput {
    primaryValue: Float!
    primaryUnit: String!
}

mutation-helpers

If an assetStandard is specified, but the asset does not conform to that standard, the update will be rejected.

7.4 Asset relationships

These functions provide finer-grained editing for relationships.

assetPut* will update any existing relationships from the input to match the input, and create any new relationships.

assetPutActorRelationships(
    versionID: ID!
    input: [ActorRelationshipInput!]!
): Update @f(name: "get_update", afterDefault: true) @requiresActor

assetPutAssetVersionRelationships(
    versionID: ID!
    input: [AssetVersionRelationshipInput!]!
): Update @f(name: "get_update", afterDefault: true) @requiresActor

mutation-methods

assetRemove* will remove all the specified relationships.

assetRemoveActorRelationships(
    versionID: ID!,
    actors: [ID!]!
): Update @f(name: "get_update", afterDefault: true) @requiresActor

assetRemoveAssetVersionRelationships(
    versionID: ID!
    assetVersions: [ID!]!
): Update @f(name: "get_update", afterDefault: true) @requiresActor

mutation-methods

Note that all of the above will create a new Update. To edit permissions without creating an update, use the *Permissions mutations.

assetGrantPermissions(
    versionID: ID!
    actorID: ID!
    permissions: [AssetVersionPermissionInput!]!
): Update @f(name: "get_update", afterDefault: true) @requiresActor

assetRevokePermissions(
    versionID: ID!
    actorID: ID!
    permissions: [AssetVersionPermissionInput!]!
): Update @f(name: "get_update", afterDefault: true) @requiresActor

mutation-methods

The assetAddToBulkCollection mutation supports bulk "in-turns" to a silo or other container. The mutation:

  1. Adds a new asset version relationship between the given asset version and the collection asset

If transfer is true, the mutation also:

  1. Increases the quantity of the collection by the quantity of the added asset
  2. Burns the given asset

If transfer is true, this mutation will fail if the asset's primary unit does not match that of the collection.

The assetRemoveFromBulkCollection mutation does the opposite, removing the relationship and decreasing the collection quantity.

assetAddToBulkCollection(
    versionID: ID!
    collectionVersionID: ID!,
    transfer: Boolean!
): Update @f(name: "get_update", afterDefault: true) @requiresActor @beta

assetRemoveFromBulkCollection(
    versionID: ID!
    collectionVersionID: ID!
): Update @f(name: "get_update", afterDefault: true) @requiresActor @beta

mutation-methods

The Update returned is for the collection asset.

7.5 Claims

assetPutClaims will update any existing claims to match the input, and create any new claims.

input ClaimInput {
    label: String!
    value: String!
    evidence: [ID!]
}

mutation-helpers

assetPutClaims(versionID: ID!, input: [ClaimInput!]!): Update @f(name: "get_update", afterDefault: true) @requiresActor

mutation-methods

7.6 Burning an asset

assetBurn will change the status of the asset to BURNED. This is a one-way operation.

assetBurn(versionID: ID!): Update @requiresActor

mutation-methods

7.7 Uploading a file

The fileCreate mutation will upload and create a new file.

fileCreate(file: Upload!): File @requiresActor @f(name: "get_file", afterDefault: true)

mutation-methods

Upload is a special type which receives a file from the request body’s multipart form data.

This means that the mutation must be called in a different way than the other mutations. Instead of taking a JSON query or mutation as the body, a form data object must be sent, containing three fields:

To make this more concrete, a valid fileCreate operation to upload blah.txt with curl would look like:

curl "http://localhost:3131/v2" \
    -F operations='{"query":"mutation($file: Upload!) {fileCreate(file:$file) {id}}","variables":{"file":null}}' \
    -F map='{ "0": ["variables.file"] }' \
    -F 0=@'blah.txt' \
    -H 'x-api-user: 50d5463a-f4f1-42e3-9ebb-c20171ea0896' \
    -H 'x-api-key: my-secret-key' \
    -H 'x-geora-actor: YWN0b3J8NTBkNTQ2M2EtZjRmMS00MmUzLTllYmItYzIwMTcxZWEwODk2Cg=='

The map field links the file sent in the form data with the mutation's file variable.

7.8 File relationships

These functions provide finer-grained editing for file relationships.

input FileActorRelationshipInput {
    id: ID!
    permissions: [FilePermissionInput!]
}

input FilePermissionInput {
    permission: FilePermissionType!
}

mutation-helpers

filePut* will update any existing relationships from the input to match the input, and create any new relationships.

filePutActorRelationships(
    fileID: ID!
    input: [FileActorRelationshipInput!]!
): File @f(name: "get_file", afterDefault: true) @requiresActor

mutation-methods

fileRemove* will remove all the specified relationships.

fileRemoveActorRelationships(
    fileID: ID!,
    actors: [ID!]!
): File @f(name: "get_file", afterDefault: true) @requiresActor

mutation-methods

7.9 Registries

A registry can have different behaviour for passing certificates along a chain of ownership.

enum CertificatesRequiredToContinue {
  NONE
  ONE
  ALL
}

domain-helpers

Anyone can create a new registry and become its first issuer.

input RegistryCreateInput {
    name: String!,
    description: String!,
    certificationName: String!,

    ipfsHashOfLogo: String,
    ipfsHashOfDocumentation: String,

    requiredClaims: [String!]!,
    userUniquenessClaim: String!,

    isPreCertifiable: Boolean!,
    certificatesRequiredForContinuation: CertificatesRequiredToContinue!
}

domain-helpers

ipfsHashOfLogo has been deprecated - see RegistryTheme API.

registryCreate(
    input: RegistryCreateInput!
): Registry @f(name: "get_registry", afterDefault: true) @requiresActor

mutation-methods

grantCertification(
  registry: ID!,
  user: ID!,
  begins: TimestampUTC!,
  expires: TimestampUTC!,
  fileID: ID!,
  notes: String!
): Actor @f(name: "get_actor", afterDefault: true) @requiresActor

extendCertification(
  registry: ID!,
  user: ID!,
  expires: TimestampUTC!,
  notes: String!
): Actor @f(name: "get_actor", afterDefault: true) @requiresActor

revokeCertification(
  registry: ID!,
  user: ID!,
  notes: String!
): Actor @f(name: "get_actor", afterDefault: true) @requiresActor

mutation-methods

input InvitationCreateInput {
  registry: ID!,
  role: RegistryUserType!,
  userID: ID,
  userEmail: String
}

domain-helpers

invitationCreate(
  input: InvitationCreateInput!
): InvitationCreateResult @requiresActor

mutation-methods

union InvitationCreateResult =
    InvitationCreateSuccess | InvitationCreateFailure

type InvitationCreateSuccess {
  successful: Boolean!
  invitation: Invitation!
}

type InvitationCreateFailure {
  successful: Boolean!
  reason: String!
}

domain-helpers

The recipient of an invitation can accept or reject it.

input InvitationAcceptInput {
  registry: ID!
}

input InvitationRejectInput {
  registry: ID!
}

domain-helpers

invitationAccept(
  input: InvitationAcceptInput!
): Invitation @requiresActor

invitationReject(
  input: InvitationRejectInput!
): Invitation @requiresActor

mutation-methods

If for some reason an asset fails automatic certification, you can edit and resubmit it.

assetSubmitForCertification(
    versionID: ID!
    registries: [ID!]!
): AssetVersion @f(name: "get_asset_version", afterDefault: true) @requiresActor

mutation-methods

7.10 Lenses

Lenses can be created and updated given a definition.

lensCreate(
  input: LensInput!
): Lens!
  @f(name: "get_lens", afterDefault: true)
  @requiresActor @beta

lensUpdate(
  lens: ID!
  input: LensInput!
): Lens!
  @f(name: "get_lens", afterDefault: true)
  @requiresActor @beta

mutation-methods

Given an existing lens, the lens can be "instantiated" to apply it to an asset very easily. The underlying lens is shared between all assets it is applied to so that an update to the lens affects all instances as well.

lensApply(
  lens: ID!
  asset: ID!
): LensInstance!
  @requiresActor @beta

mutation-methods

Lenses can be deactivated by the owner and later reactivated. When they are deactivated they no longer return the asset's history, but the person trying to use the lens instance will still have the chance to send a message to the lens' owner.

lensActivate(
  lens: ID!
): Lens!
  @f(name: "get_lens", afterDefault: true)
  @requiresActor @beta

lensDeactivate(
  lens: ID!
): Lens!
  @f(name: "get_lens", afterDefault: true)
  @requiresActor @beta

mutation-methods

Lenses which have been deactivated may also be permanently deleted by their owner.

lensDelete(
  lens: ID!
): Boolean!
  @requiresActor @beta

mutation-methods

Lens instances can be deleted at any time.

lensInstanceDelete(
  id: ID!
): Boolean!
  @requiresActor @beta

mutation-methods

7.11 Asset standards

Asset standards can be created and updated.

An asset standard can only be updated by its creator.

assetStandardCreate(
  input: AssetStandardInput!
): AssetStandard!
  @f(name: "get_asset_standard", afterDefault: true)
  @requiresActor
  @alpha

assetStandardUpdate(
  id: ID!
  input: AssetStandardInput!
): AssetStandard!
  @f(name: "get_asset_standard", afterDefault: true)
  @requiresActor
  @alpha

mutation-methods

Individual stages and claims on the asset standard can be updated independently.

assetStandardStageUpdate(
  id: ID!,
  stageName: String!,
  input: AssetStandardStageInput!
): AssetStandard!
  @requiresActor
  @alpha

assetStandardClaimUpdate(
  id: ID!,
  stage: ID!,
  input: AssetStandardClaimInput!
): AssetStandard!
  @requiresActor
  @alpha

mutation-methods

When using assetStandardClaimUpdate, if the claim input has the same label as a claim that exists on that stage, the claim will be edited in-place. Otherwise, a new claim will be added to the stage.

When updating claims in asset standards, the new claims must be compatible with assets created with a previous revision of the standard. This means that new claims must either be optional or have a default value. The human-readable label and description can be changed at any time.

7.12 Theming

Each registry has its own theme attached. There is a default theme provided by Geora which can be configured by a registry issuer.

A theme consists of a logo url and colour scheme.

The ColorHex scalar is a hexadecimal string representing a valid CSS color. For example #e35b2f or #f46f56. Note that the '#' should be included in the string.

type RegistryTheme {
    logoUrl: String!
    colorScheme: ColorScheme!
}

type ColorScheme {
    colorPrimary: ColorHex!
    colorPrimaryHighlight: ColorHex!
    colorSecondary: ColorHex!
    colorGray1: ColorHex!
    colorGray2: ColorHex!
    colorGray3: ColorHex!
    colorGray4: ColorHex!
    colorGray5: ColorHex!
    colorGray6: ColorHex!
    colorWhite: ColorHex!
    colorDanger: ColorHex!
    colorWarning: ColorHex!
    colorSuccess: ColorHex!
    colorDangerLight: ColorHex!
    colorWarningLight: ColorHex!
    colorSuccessLight: ColorHex!
}

scalar ColorHex

domain-helpers

The theme of a registry can be queried by any actor (issuer or user) in the registry.

The theme of a registry can be updated only by the issuer of a registry.

input ColorSchemeInput {
      colorPrimary: ColorHex
      colorPrimaryHighlight: ColorHex
      colorSecondary: ColorHex
      colorGray1: ColorHex
      colorGray2: ColorHex
      colorGray3: ColorHex
      colorGray4: ColorHex
      colorGray5: ColorHex
      colorGray6: ColorHex
      colorWhite: ColorHex
      colorDanger: ColorHex
      colorWarning: ColorHex
      colorSuccess: ColorHex
      colorDangerLight: ColorHex
      colorWarningLight: ColorHex
      colorSuccessLight: ColorHex
}

input SetRegistryThemeInput {
  registryId: ID!
  logoUrl: String
  colorScheme: ColorSchemeInput
}

mutation-helpers

Note that fields in the ColorSchemeInput are not compulsory. This is because each colour has a default value which will be applied if no value is given.

setRegistryTheme(
  input: SetRegistryThemeInput!
): RegistryTheme @f(name: "get_registry_theme_by_id", afterDefault: true) @requiresPayingUser(plan: STARTER) @requiresActor

mutation-methods

8 Errors

When a query or mutation fails, the returned JSON body will contain an errors field.

For example, the following JSON is a failed request to the assetUpdate mutation, where the calling actor does not exist.

{
  "errors": [
    {
      "message": "Could not find actor with ID YWN0b3J8NGI3YjFkMjAtZWM3Yi00YTI5LWI5OGEtZDJiMmRiNjhhMTM1",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "assetUpdate"
      ],
      "extensions": {
        "code": "FORBIDDEN",
      }
    }
  ],
  "data": {
    "assetUpdate": null
  }
}

For each error, there will be a message provided explaining the error, as well as a code to help categorise errors. Note that the expected return data is null when an error occurs.

9 Changelog

  • 3.0.10

    (2021-02-09)

    API

    • Asset lenses API

      Create permissioned views of assets to share with different user types.

    • Check for duplicate registry names when joining registries

      When a user joins a new registry, an error will be thrown if the registry has the same name as another registry they belong to.

    • Add ordering on GraphQL connections

      On certain API connections, an orderBy attribute is now available which allows sorting the connection based on fields in the data it contains.

    • Add totalPages field on GraphQL connections

      All API connections now return the total number of pages they contain when using pagination with first and limit.

    • Bugfix: filtering GraphQL connections

      The where field in connections allows filtering the items in the collection before returning from the API. In some connections, it was sorting numerical fields lexicographically, resulting in an incorrect sort order.

    Application

    • Custom branding and theming for users

      Users can use the theming API to brand the Geora user interface according to their brand guidelines.

    • Data tables with sorting and searching

      Parts of the user interface that consume connections from the API now support searching, filtering, and editable columns in enhanced data tables.

    • Responsive UI for mobile devices

      The Geora user interface now adapts to mobile device screen sizes.

    • Add attachments (images, files) to assets

      Users can attach files and images to their assets when creating or editing them.

    • Improved asset history view

      When a user inspects the history of an asset, they see an improved overview sidebar containing the current information stored on the asset, and a historical timeline containing all updates. The timeline embeds photos and documents that have been attached to the asset.

    • Asset history view filtered by lens

      A user can share a permissioned history of their asset through the lens API, and view the lens in the user interface.

    • Drag-and-drop certificate uploads

      Certificate (and other file) uploads now support dragging and dropping files into the user interface.

    • Open certificates and files in a new tab

      Certificates and files open in a new tab when clicked.

    • Show timezone of updates in asset history

      In the asset history timeline, the timezone of each update timestamp is displayed.

10 Definition Index

AACTIVE actor Actor ActorAssetEdge ActorAssetsConnection ActorClaim ActorClaimConnection ActorClaimEdge ActorClaimInput ActorCreateInput ActorEdge ActorRegistryConnection ActorRegistryEdge ActorRelationshipConnection ActorRelationshipEdge ActorRelationshipInput actors ActorsConnection ActorUpdateInput alpha ASCENDING asset Asset AssetCertificationEdge AssetEdge assets AssetsConnection AssetStandard assetStandard AssetStandardClaim AssetStandardClaimInput assetStandardClaimUpdate AssetStandardConnection assetStandardCreate AssetStandardEdge AssetStandardInput assetStandards AssetStandardStage AssetStandardStageInput assetStandardStageUpdate assetStandardUpdate AssetStandardUsageStatus AssetUpdateEdge AssetUpdateInput AssetUpdatesConnection assetVersion AssetVersion AssetVersionEdge AssetVersionPermission AssetVersionPermissionInput AssetVersionPermissionType AssetVersionRelationshipConnection AssetVersionRelationshipEdge AssetVersionRelationshipInput AssetVersionRelationshipInputType AssetVersionRelationshipType assetVersions AssetVersionsConnection AssetVersionStatus Attachment AVAILABLE

Bbeta BURNED BUSINESS

CCertificatesRequiredToContinue CERTIFICATION_EXPIRED CERTIFICATION_EXTENDED CERTIFICATION_GRANTED CERTIFICATION_REVOKED CertificationConnection CertificationHistory CertificationHistoryConnection CertificationHistoryEdge CertificationHistoryEvent CertificationStatus Certified CHANGE_CLAIMS CHANGE_CLASS CHANGE_COLLECTION CHANGE_CUSTODIAN CHANGE_IS_BURNT CHANGE_PARENTS CHILD_OF Claim ClaimConnection ClaimEdge ClaimInput COLLECTED_BY COLLECTS ColorHex ColorScheme ColorSchemeInput Connection CREATED_BY_CALLER Cursor

DDate DATE Decimal DESCENDING

EEdge Email EmailInfo ERROR EthereumAddress EvidenceConnection EvidenceEdge extendCertification

FFile file FileActorRelationshipConnection FileActorRelationshipEdge FileActorRelationshipInput FileEdge FilePermission FilePermissionInput FilePermissionType files FilesConnection FLOAT FREE FUTURE

GgrantCertification Granularity

IIN_USE_BY_CALLER IndexBound Invitation invitationAccept InvitationAcceptInput invitationCreate InvitationCreateFailure InvitationCreateInput InvitationCreateResult InvitationCreateSuccess invitationReject InvitationRejectInput IPFSHash

LLens lens lensActivate lensApply LensBound LensClaimApproval LensClaimApprovalInput LensConnection lensCreate lensDeactivate lensDelete LensEdge lenses LensInput lensInstance LensInstance LensInstanceConnection lensInstanceDelete LensInstanceEdge lensInstances lensUpdate

NNode node Notification NotificationConnection NotificationEdge notifications

OOrderable OrderAs OrderBy

PPageInfo PARENT_OF PAST PENDING Pending PER_DAY PER_MONTH PER_QUARTER PER_WEEK_MONDAY PER_WEEK_SUNDAY PreCertified PRO Production ProductionConnection ProductionEdge

QQuantity QuantityInput

Rregistries Registry registry RegistryActorConnection RegistryActorEdge RegistryConnection registryCreate RegistryCreateInput RegistryEdge RegistryTheme RegistryUserType requiresActor revokeCertification Revoked

SSET_PERMISSION SET_PERMISSION setNotificationsSeen SetNotificationsSeenResult setRegistryTheme SetRegistryThemeInput STARTER STRING SubscriptionPlan SUCCESS

TTimestampBound TimestampUTC TypedID

UUncertified Unverified Update update UpdateEdge UpdateError UpdatePending UpdateResult updates UpdatesConnection UpdateStatus UpdateSuccess UpdateTemporalLocation URL USE_AS_PARENT USE_FILE USE_IN_COLLECTION

VVerificationResult Verified VersionConnection VersionEdge versions VIEW_ASSET VIEW_FILE