Skip to main content

API Integration

This document covers how to integrate with OctoMesh APIs, including authentication, GraphQL operations, and the service client SDK.

API Overview​

OctoMesh provides multiple API access methods:

API TypeProtocolUse Case
GraphQLHTTPSPrimary data access, queries, mutations
RESTHTTPSAdministrative operations, file uploads
AMQPRabbitMQReal-time messaging, adapter communication

Authentication​

OAuth 2.0 / OpenID Connect​

OctoMesh uses OAuth 2.0 with OpenID Connect for authentication.

Obtaining Access Tokens​

OctoMesh supports two primary authentication methods depending on whether a user context is needed.

Authorization Code with PKCE (User-Based)​

Use this flow when your application acts on behalf of a user. The user authenticates interactively, and the issued token contains their identity, roles, and tenant context.

Step 1 — Redirect the user to the authorize endpoint:

GET /connect/authorize?
response_type=code
&client_id=my-application
&redirect_uri=https://my-app.example.com/callback
&scope=openid profile email role octo_api
&acr_values=tenant:{tenantId}
&code_challenge={challenge}
&code_challenge_method=S256
&state={state}

The acr_values=tenant:{tenantId} parameter directs the Identity Service to the correct tenant's login page. The user authenticates with the credentials and identity providers configured for that tenant.

Step 2 — Exchange the authorization code for tokens:

POST /connect/token HTTP/1.1
Host: identity.octomesh.local
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=my-application
&code={authorization_code}
&redirect_uri=https://my-app.example.com/callback
&code_verifier={verifier}

Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email role octo_api"
}

The issued access token contains the user's sub, tenant_id, role, and allowed_tenants claims scoped to the selected tenant.

Tenant-specific client registration

The OAuth client must be registered in each tenant where users need to authenticate. See Clients and API Scopes for details on client management.

Client Credentials (Service-to-Service)​

Use this flow for background services and automated processes where no user is involved. The client authenticates with its own credentials.

POST /connect/token HTTP/1.1
Host: identity.octomesh.local
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=my-service
&client_secret=my-secret
&scope=octo_api
&acr_values=tenant:my-tenant
acr_values is required

Clients are registered per tenant, and /connect/token has no tenant path segment. The acr_values=tenant:{tenantId} form parameter tells the Identity Service which tenant's client store to search — without it the lookup falls back to the system tenant and the request fails with invalid_client for any tenant-registered client.

Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "octo_api"
}

Client-credentials tokens have no user identity (sub claim) and no tenant_id / allowed_tenants claims; downstream services therefore skip the route-tenant authorization check for them. The tenant binding is established solely at token time, by which tenant's client store matched via acr_values.

API Scopes​

ScopeDescription
octo_apiFull read/write access to all OctoMesh APIs
octo_api.read_onlyRead-only access to all OctoMesh APIs

Using the Token​

Include the token in the Authorization header:

GET /tenants/my-tenant/graphql HTTP/1.1
Host: asset-repo.octomesh.local
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

GraphQL API​

Endpoint​

https://{asset-repo-host}/tenants/{tenantId}/graphql

GraphQL Playground (interactive explorer):

https://{asset-repo-host}/tenants/{tenantId}/graphql/playground

Query Structure​

The GraphQL schema is organized into two main query roots:

type Query {
runtime: RuntimeQuery # Master data (MongoDB)
streamData: StreamQuery # Time-series data (CrateDB)
}

type Mutation {
runtime: RuntimeMutation
}

Runtime Queries​

Query all entities of a type:

query GetEnergyMeters {
runtime {
rtIndustryEnergyEnergyMeter(
first: 50
after: "cursor"
filter: {
status: { eq: "Active" }
}
orderBy: { name: ASC }
) {
items {
rtId
ckTypeId
wellKnownName
name
serialNumber
status
location {
items {
rtId
name
address
}
}
}
pageInfo {
hasNextPage
endCursor
totalCount
}
}
}
}

Query by ID:

query GetMeterById($rtId: OctoObjectId!) {
runtime {
rtIndustryEnergyEnergyMeterById(rtId: $rtId) {
rtId
name
serialNumber
readings(first: 10) {
items {
timestamp
value
}
}
}
}
}

Query by wellKnownName:

query GetMeterByName {
runtime {
rtIndustryEnergyEnergyMeterByWellKnownName(
wellKnownName: "main-building-meter"
) {
rtId
name
}
}
}

Stream Data Queries​

Query time-series data:

query GetMeterReadings($rtId: OctoObjectId!) {
streamData {
tsIndustryEnergyMeterReading(
first: 1000
filter: {
rtId: { eq: $rtId }
timestamp: {
gte: "2024-01-01T00:00:00Z"
lte: "2024-01-31T23:59:59Z"
}
}
orderBy: { timestamp: DESC }
) {
items {
rtId
timestamp
voltage
current
power
energy
}
pageInfo {
hasNextPage
endCursor
}
}
}
}

Mutations​

Create entity:

mutation CreateEnergyMeter {
runtime {
createRtIndustryEnergyEnergyMeter(
entity: {
wellKnownName: "new-meter-001"
attributes: {
name: "New Energy Meter"
serialNumber: "EM-2024-001"
manufacturer: "Siemens"
status: Active
}
}
) {
rtId
wellKnownName
}
}
}

Update entity:

mutation UpdateEnergyMeter($rtId: OctoObjectId!) {
runtime {
updateRtIndustryEnergyEnergyMeter(
rtId: $rtId
entity: {
attributes: {
status: Maintenance
}
}
) {
rtId
status
}
}
}

Delete entity:

mutation DeleteEnergyMeter($rtId: OctoObjectId!) {
runtime {
deleteRtIndustryEnergyEnergyMeter(rtId: $rtId)
}
}

Manage associations:

mutation AddMeterToLocation($meterId: OctoObjectId!, $locationId: OctoObjectId!) {
runtime {
addAssociationRtIndustryEnergyEnergyMeter(
rtId: $meterId
association: "location"
targetRtIds: [$locationId]
) {
rtId
location {
items {
rtId
}
}
}
}
}

Filtering​

OctoMesh GraphQL supports rich filtering:

query FilteredQuery {
runtime {
rtIndustryEnergyEnergyMeter(
filter: {
AND: [
{ status: { eq: "Active" } }
{
OR: [
{ manufacturer: { contains: "Siemens" } }
{ manufacturer: { contains: "ABB" } }
]
}
{ maxCapacity: { gte: 100 } }
]
}
) {
items {
rtId
name
}
}
}
}

Available filter operators:

OperatorDescription
eqEquals
neqNot equals
gt / gteGreater than / Greater than or equal
lt / lteLess than / Less than or equal
containsString contains
startsWithString starts with
endsWithString ends with
inValue in list
AND / ORLogical operators

Service Client SDK​

The .NET SDK provides a typed client for OctoMesh APIs.

Installation​

dotnet add package Meshmakers.Octo.Sdk.ServiceClient

Configuration​

using Meshmakers.Octo.Sdk.ServiceClient;

var services = new ServiceCollection();

services.AddOctoMeshServiceClient(options =>
{
options.IdentityServiceUrl = "https://identity.octomesh.local";
options.AssetRepositoryUrl = "https://asset-repo.octomesh.local";
options.ClientId = "my-application";
options.ClientSecret = "my-secret";
options.TenantId = "my-tenant";
});

Using the Service Client​

public class MyService
{
private readonly IAssetRepositoryClient _client;

public MyService(IAssetRepositoryClient client)
{
_client = client;
}

public async Task<IEnumerable<EnergyMeter>> GetActiveMetersAsync(
CancellationToken cancellationToken)
{
var query = new GraphQLRequest
{
Query = @"
query {
runtime {
rtIndustryEnergyEnergyMeter(
filter: { status: { eq: ""Active"" } }
) {
items {
rtId
name
serialNumber
}
}
}
}"
};

var response = await _client.ExecuteGraphQLAsync<EnergyMeterResponse>(
query, cancellationToken);

return response.Runtime.RtIndustryEnergyEnergyMeter.Items;
}

public async Task<string> CreateMeterAsync(
CreateMeterRequest request,
CancellationToken cancellationToken)
{
var mutation = new GraphQLRequest
{
Query = @"
mutation CreateMeter($input: CreateEnergyMeterInput!) {
runtime {
createRtIndustryEnergyEnergyMeter(entity: $input) {
rtId
}
}
}",
Variables = new { input = request }
};

var response = await _client.ExecuteGraphQLAsync<CreateMeterResponse>(
mutation, cancellationToken);

return response.Runtime.CreateRtIndustryEnergyEnergyMeter.RtId;
}
}

Source Generation​

Use source generation to create typed DTOs:

dotnet add package Meshmakers.Octo.Sdk.SourceGeneration
// Define your models with source generation attributes
[GenerateGraphQLTypes("Industry.Energy/EnergyMeter")]
public partial class EnergyMeterDto
{
}

// Generated code provides:
// - Strongly typed properties matching CK attributes
// - Serialization support
// - GraphQL query/mutation helpers

REST API​

Administrative Endpoints​

Get tenant information:

GET /api/tenants/{tenantId}
Authorization: Bearer {token}

Upload file:

POST /api/tenants/{tenantId}/files
Authorization: Bearer {token}
Content-Type: multipart/form-data

file=@document.pdf

Get system health:

GET /health

Construction Kit Management​

List CK libraries:

GET /api/tenants/{tenantId}/construction-kits
Authorization: Bearer {token}

Upload CK library:

POST /api/tenants/{tenantId}/construction-kits
Authorization: Bearer {token}
Content-Type: application/json

{
"libraryId": "MyCompany.Domain",
"version": "1.0.0",
"types": [...],
"enums": [...]
}

Error Handling​

GraphQL Errors​

GraphQL errors are returned in the errors array:

{
"data": null,
"errors": [
{
"message": "Entity not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["runtime", "rtIndustryEnergyEnergyMeterById"],
"extensions": {
"code": "ENTITY_NOT_FOUND",
"details": {
"rtId": "invalid-id"
}
}
}
]
}

Common Error Codes​

CodeDescription
UNAUTHORIZEDMissing or invalid token
FORBIDDENInsufficient permissions
ENTITY_NOT_FOUNDRequested entity does not exist
VALIDATION_ERRORInput validation failed
CK_TYPE_NOT_FOUNDConstruction Kit type not found
ASSOCIATION_ERRORInvalid association operation

Client-Side Error Handling​

try
{
var result = await _client.ExecuteGraphQLAsync<Response>(query, ct);

if (result.Errors?.Any() == true)
{
foreach (var error in result.Errors)
{
_logger.LogError("GraphQL error: {Message} (Code: {Code})",
error.Message,
error.Extensions?["code"]);
}
throw new OctoMeshException("GraphQL operation failed", result.Errors);
}

return result.Data;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP request failed");
throw;
}
catch (AuthenticationException ex)
{
_logger.LogError(ex, "Authentication failed");
// Refresh token and retry
throw;
}

Rate Limiting​

OctoMesh implements rate limiting to protect the platform:

Endpoint TypeLimit
GraphQL queries100 req/min per client
Mutations50 req/min per client
File uploads10 req/min per client

When rate limited, you'll receive:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

Best Practices​

Query Optimization​

  1. Request only needed fields: Reduce response size
  2. Use pagination: Always set first parameter
  3. Filter server-side: Don't fetch and filter client-side
  4. Batch related queries: Combine in single request

Connection Management​

  1. Reuse HTTP clients: Use connection pooling
  2. Handle token refresh: Implement automatic refresh
  3. Implement retry logic: Handle transient failures
  4. Use timeouts: Set appropriate request timeouts

Security​

  1. Secure credentials: Use secrets management
  2. Minimal scopes: Request only needed permissions
  3. Validate responses: Don't trust external data
  4. Log securely: Don't log tokens or sensitive data

Next Steps​