Introduction

CerQlar provides a REST API to interact with the platform. Our APIs have predictable resource-oriented URLs, accept JSON-encoded request bodies, return JSON-encoded responses, and use standard HTTP response codes, authentication, and verbs.

Authentication

Every request to the CerQlar REST API needs to be authenticated with an Authorization header.

The header should take the form Authorization: Bearer {bearer_token} where {bearer_token} is replaced with a valid token. To request a new token, make a call to the /authentication/oauth/token endpoint. You can find more details and try it out on the Administration API page.

Your request would look like:

POST https://{environment}/administration/v1/authentication/oauth/token
Content-Type: application/json

{
  "client_id": "{client_id}",
  "client_secret":"{client_secret}",
  "audience": "cerqlar-backend",
  "grant_type": "client_credentials"
}

Where:

  • {environment} will be either

    • api.demo.cerqlar.com for the demo environment or

    • api.cerqlar.com for production

  • {client_id} and {client_secret} are your API key credentials. New API keys can be created here for demo or here for production. If you see an error message when accessing this page, please contact support. Make sure to always keep client_secret secure. If you think it may have been compromised, delete your API key and create a new one.

After executing the request, you should receive a response back which looks something like this:

{
  "access_token": "{bearer_token}",
  "scope": ["trade:capture", "trade:list", ...],
  "expires_in": 86461,
  "token_type": "Bearer"
}

The value {bearer_token} can now be used in the Authorization header. This token can be used for multiple requests, but will expire after some number of seconds, indicated by the expires_in field. After this, a new token will need to be requested.

Rate limiting

CerQlar rate limits the REST API based on the client_id you provided when calling the /authentication/oauth/token endpoint. The current rate limits are:

  • 3 requests per hour for the /authentication/oauth/token endpoint which issues access tokens. Docs for this endpoint can be found on the Administration API page

  • 10 requests per second for all other endpoints combined

Up-to-date information about rate limits can also be found in the response headers:

  • x-ratelimit-limit - indicates the request-quota associated to the client in the current time-window followed by the description of the quota policy. E.g. 3, 3;w=60 - 3 requests in your current quota, and then the description of the quota policy - 3 requests per 60 seconds

  • x-ratelimit-remaining - indicates the remaining requests in the current time-window

  • x-ratelimit-reset - indicates the number of seconds until reset of the current time-window

If you have exceeded your quota, the response status will be 429 Too Many Requests.

APIs

Administration API

API for user and counterparty management.

Trading API

API for trade lifecycle management, including payments and deliveries.

Inventory API

API for retrieving transactions and managing transaction attributions

Datatypes

Dates and times

Dates and times always use ISO 8601 with a UTC timezone (where applicable).

Examples:

  • 2023-01-01 is 1st January 2023

  • 2023-01-01T12:00:00.000Z is midday UTC on 1st January 2023

  • 2023-01 is January 2023

Enumerations

In the CerQlar API, there are two types of enumerations; standard and extensible.

Standard enumerations

A standard enumeration is a closed set of values. Changes to these enumerations are considered a breaking change. Should we need to make a change to a standard enumeration, we will bump the API version.

Extensible enumerations

Values in an extensible enumeration are open-ended. We may add new values without notice and this is not considered a breaking change. In the API docs and our OpenAPI schemas, extensible enums appear as x-extensible-enum. This is a non-standard extension and will be ignored by most code generation tools.

Important

You must ensure that any client code can handle new values which are returned from the CerQlar API, i.e. it must implement a fallback for any values which were unknown at the time of writing the client.

Searching

There are several search endpoints in the CerQlar API, all of which follow the same pattern. A search request is made by sending a POST request to a search endpoint with a body containing the search criteria. For example, the following request will search for buy trades where the deal date is greater than 2023-01-01 and sort the results by when the trade was last modified. The first 100 results will be returned:

  • Trading v1

  • Trading v2

POST /trading/v1/trades/search
Content-Type: application/json
Authorization: Bearer {bearer_token}

{
    "query": {
        "filter": {
            "and": [
                {
                    "field": "side",
                    "operator": "EQUALS",
                    "value": "BUY",
                    "_type": "string"
                },
                {
                    "field": "dealDate",
                    "operator": "GREATER_THAN",
                    "value": "2023-01-01",
                    "_type": "date"
                }
            ]
        },
        "sort": [
            {
                "field": "lastChangedAt",
                "direction": "DESC"
            }
        ],
        "page": {
            "pageNumber": 0,
            "pageSize": 100
        }
    }
}
POST /trading/v2/trades/search
Content-Type: application/json
Authorization: Bearer {bearer_token}

{
    "query": {
        "filter": {
            "and": [
                {
                    "field": "side",
                    "operator": "EQUALS",
                    "value": "BUY",
                    "_type": "string"
                },
                {
                    "field": "dealDate",
                    "operator": "GREATER_THAN",
                    "value": "2023-01-01T12:00:00Z",
                    "_type": "datetime"
                }
            ]
        },
        "sort": [
            {
                "field": "lastChangedAt",
                "direction": "DESC"
            }
        ],
        "page": {
            "pageNumber": 0,
            "pageSize": 100
        }
    }
}

Response 200 OK:

{
    "items": [
        {
            ...
        },
        ...
    ],
    "page": {
        "pageSize": 100,
        "pageNumber": 0,
        "totalItems": 7,
        "totalPages": 1
    }
}

All the fields within the query object of the request are optional. In this case no filtering or sorting are applied and the zeroth page containing the first 10 elements will be returned:

{
    "query": {
    }
}

More examples are provided here.

Note

Not all fields can be used in filter and sort clauses. The list of fields which search endpoints support is available in the api docs.

Filtering

Field matching

Results can be filtered based on field values. For example:

{
  "field": "side",
  "operator": "EQUALS",
  "value": "BUY",
  "_type": "string"
}

Where:

  • field is the name of the field to filter on. Where the field is in a nested object, field names can be joined into a path using . to separate fields, e.g. tradeDetails.technologies.

  • operator values include EQUALS,NOT_EQUALS,GREATER_THAN,GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL and CONTAINS. CONTAINS is only valid for string type fields.

  • value is the value to filter against.

  • _type is the type of the value passed, one of string, long, date or datetime.

It’s also possible to check if a field contains one of a set of values:

{
  "field": "clientUserId",
  "operator": "IN",
  "values": ["alice", "bob"],
  "_type": "string"
}

The operator must be either IN or NOT_IN.

Logical operators

Filters can also be combined using and or or operators, for example:

{
  "and": [
    {
      "field": "",
      ...
    },
    {
      "field": "",
      ...
    }
  ]
}

Sorting

Results can be sorted on zero or more fields. When no sorting criteria are provided, the results are unsorted.

Paging

Results are always paged. Page numbers are zero based. When the page field is not provided, the default value is used which is to return the zeroth page with a page size of 10. In the response, the total number of items matching the query and the number of pages available are returned.

Warning

If the contents of the database are modified while results are being paged through, the values of totalItems and totalPages may change. Results could also be returned more than once or not at all.

Examples

Search for counterparties with a legalName equal to Acme, returning the first 10 results and without any sorting:

POST /administration/v1/counterparties/search
Content-Type: application/json
Authorization: Bearer {bearer_token}

{
    "query": {
        "filter": {
            "field": "legalName",
            "operator": "EQUALS",
            "value": "Acme",
            "_type": "string"
        }
    }
}

Search for trades where the issuing countries include Belgium and the Netherlands and sort the results by when the trade was last updated, newest first:

  • Trading v1

  • Trading v2

POST /trading/v1/trades/search
Content-Type: application/json
Authorization: Bearer {bearer_token}

{
    "query": {
        "filter": {
            "field": "tradeDetails.issuingCountrySpec.countries",
            "operator": "IN",
            "values": ["NL","BE"],
            "_type": "string"
        },
        "sort": [
            {
                "field": "lastChangedAt",
                "direction": "DESC"
            }
        ]
    }
}
POST /trading/v2/trades/search
Content-Type: application/json
Authorization: Bearer {bearer_token}

{
    "query": {
        "filter": {
            "field": "productSpecification.GO.issuingCountry.countries.include.values",
            "operator": "IN",
            "values": ["NL","BE"],
            "_type": "string"
        },
        "sort": [
            {
                "field": "lastChangedAt",
                "direction": "DESC"
            }
        ]
    }
}

Get the 50 most recently updated trades:

  • Trading v1

  • Trading v2

POST /trading/v1/trades/search
Content-Type: application/json
Authorization: Bearer {bearer_token}

{
    "query": {
        "sort": [
            {
                "field": "lastChangedAt",
                "direction": "DESC"
            }
        ],
        "page": {
            "pageSize": 50,
            "pageNumber": 0
        }
    }
}
POST /trading/v2/trades/search
Content-Type: application/json
Authorization: Bearer {bearer_token}

{
    "query": {
        "sort": [
            {
                "field": "lastChangedAt",
                "direction": "DESC"
            }
        ],
        "page": {
            "pageSize": 50,
            "pageNumber": 0
        }
    }
}

Patching

The patch endpoints can be used to modify an entity. These are RFC-6902 compliant JSON patch endpoints. A description of how JSON patch works is here.

Note that not all fields can be patched. The list of fields which can be patched is available in the api docs.

Important

It’s important to understand that patches are always applied to the entity as it appears in the GET endpoint for that entity.

For example, when patching a trade, the path you provide in patches must correspond to locations in the trade as it appears when you GET that trade.

Examples

Updating fields on a trade

The following call will update trade ABC123 such that the clientTradeId becomes trade-123 and the code T0201 is added to the existing list of technologies in trade details.

  • Trading v1

  • Trading v2

PATCH /trading/v1/trades/ABC123
Content-Type: application/json
Authorization: Bearer {bearer_token}

[
    {
        "op":"replace",
        "path":"/clientTradeId",
        "value": "trade-123"
    },
    {
        "op": "add",
        "path": "/tradeDetails/technologies/-",
        "value": "T0201"
    }
]
PATCH /trading/v2/trades/ABC123
Content-Type: application/json
Authorization: Bearer {bearer_token}

[
    {
        "op":"replace",
        "path":"/clientTradeId",
        "value": "trade-123"
    },
    {
        "op": "add",
        "path": "/productSpecification/GO/technology/values/-",
        "value": "T0101"
    }
]

Replacing the product specification on a trade

It is possible to replace a whole object at once rather than individual fields. For example, the whole product specification can be replaced like this:

  • Trading v1

  • Trading v2

PATCH /trading/v1/trades/ABC123
Content-Type: application/json
Authorization: Bearer {bearer_token}

[
    {
        "op":"replace",
        "path": "/tradeDetails",
        "value": {
            "technologies": ["T01"],
            "plantCommissioningDate": "2002-01-01"
        }
    }
]
PATCH /trading/v2/trades/ABC123
Content-Type: application/json
Authorization: Bearer {bearer_token}

[
    {
        "op": "replace",
        "path": "/productSpecification/GO",
        "value": {
            "technology": {
                "values": ["T01"]
            },
            "plantCommissioningDate": {
                "start": {
                    "value": "2002-01-01"
                }
            }
        }
    }
]

Update volume on a production period

To update the trade volume for a production period, the trade should be patched like this:

  • Trading v2

Note that in the below request, the volume needs to be in watt-hours, not MWh. Also {n} and {m} in the path should be replaced by integers which corresponds to the position the delivery and product period you want to update appear in when you GET the trade. These positions are zero indexed, so the first delivery would be 0, the second 1, etc.

PATCH /trading/v2/trades/ABC123
Content-Type: application/json
Authorization: Bearer {bearer_token}

[
    {
        "op": "replace",
        "path": "/schedule/{n}/productionPeriods/{m}/volume/value",
        "value": 200000000
    }
]

Change delivery method

When changing between cancellation and transfer delivery methods, it is recommended to replace the whole deliveryDestination object as there are some fields which exists on both cancellation and transfer details. Since the two delivery methods have different validation rules, patching individual fields within the deliveryDestination object can lead to confusion.

  • Trading v2

PATCH /trading/v2/trades/ABC123
Content-Type: application/json
Authorization: Bearer {bearer_token}

[
    {
        "op": "replace",
        "path": "/deliveryDestination",
        "value": {
            "deliveryMethod": "CANCEL",
            "registryId": "ATOS",
            "usageCategory":"DISCLOSURE"
        }
    }
]

Changelog

Trading v2

The trades v2 API is available under the path /trading/v2.The most significant change is the addition of multi-product support (i.e. products other than GOs), however other changed have also been made.

Multi-product support

In the trade response, the tradeDetails field has been replaced by productSpecification. Inside the product specification is another object where the key is the product type (GO, IREC, etc) and the value is an object. Which fields can appear within this object depend on the product type. All fields are optional.

In the case of GOs, the same set of fields are available as in tradeDetails, however some changes have been made to the field names and the structure of the values. See the API docs for more details.

Example GO trade body
{
    // ...
    "productSpecification": {
        "GO": {                   // Product type of this trade
            "typesOfSupport": {   // The fields which can appear here depend on the product type.
                "values": [
                    "INVESTMENT"
                ]
            },
            "technology": {
                "values": ["T01"]
            }
        }
    },
    // ...
}

Sometimes a field with the same name can appear in more than one product type. For example, both GOs and I-RECs have a technology field, however these are different fields with a different set of acceptable values.

Searching

When searching for trades, it is now necessary to list the product types you wish to retrieve using the productType query parameter. This is required so that as more product types are added to the API in the future, existing requests won’t automatically receive those new trade types in the response. The productType query param can accept multiple values separated by commas.

POST /trading/v2/trades/search?productType=GO,IREC

In the body of the request, the filtering and sorting paths must reflect the new structure of trades. For example in v1 the path to the technology field was tradeDetails.technologies but in v2, it is now productSpecification.GO.technology.values. If you want to filter on I-REC technologies instead of GO ones, then the path would become productSpecification.IREC.technology.values.

Volumes and units

The volume field on the production period has changed from a simple number with an implicit unit of watt-hours to an object.

{
    "volume": {
        "value": 1000000,
        "unit": "WATT_HOUR"
    }
}

When filtering on this field, use a numeric value in watt-hours as in API v1.

Date only fields

dealDate , paymentDue and deliveryDue fields have all been converted from date only fields to datetime fields. As with all the other datetime fields in the API, the timezone is always UTC. The values should be given in ISO format, for example 2024-07-05T11:34:00Z.

Other changes

  • The PUT /trading/v1/production-periods/{id}/volume endpoint has been removed in v2. The same can be achieved using PATCH /trading/v2/trades/{tradeId}

  • The deliveryMethod field on trades, which was already deprecated in v1, has been removed completely and replaced by deliveryDestination

  • The delivery field on trade schedules has been renamed to deliveryObligation

  • The deliveredAt and deliveryConfirmedAt fields on deliveries have been renamed to sellerSettledAt and buyerSettledAt respectively

  • The expectedAmount field on payments has been removed.

  • The deliveryId field on payments has been removed. The field schedule.{n}.delivery.id can be used instead

  • Only the IDs of GO certifications are supported in v2, display names are no longer allowed. For example in v1 both TÜV NORD CERT and TUV_NORD_CERT were allowed, but in v2 only TUV_NORD_CERT is