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 Type | Protocol | Use Case |
|---|---|---|
| GraphQL | HTTPS | Primary data access, queries, mutations |
| REST | HTTPS | Administrative operations, file uploads |
| AMQP | RabbitMQ | Real-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.
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
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "octo_api"
}
Client-credentials tokens have no user identity (sub claim) and bypass tenant authorization checks.
API Scopes
| Scope | Description |
|---|---|
octo_api | Full read/write access to all OctoMesh APIs |
octo_api.read_only | Read-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:
| Operator | Description |
|---|---|
eq | Equals |
neq | Not equals |
gt / gte | Greater than / Greater than or equal |
lt / lte | Less than / Less than or equal |
contains | String contains |
startsWith | String starts with |
endsWith | String ends with |
in | Value in list |
AND / OR | Logical 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
| Code | Description |
|---|---|
UNAUTHORIZED | Missing or invalid token |
FORBIDDEN | Insufficient permissions |
ENTITY_NOT_FOUND | Requested entity does not exist |
VALIDATION_ERROR | Input validation failed |
CK_TYPE_NOT_FOUND | Construction Kit type not found |
ASSOCIATION_ERROR | Invalid 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 Type | Limit |
|---|---|
| GraphQL queries | 100 req/min per client |
| Mutations | 50 req/min per client |
| File uploads | 10 req/min per client |
When rate limited, you'll receive:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Best Practices
Query Optimization
- Request only needed fields: Reduce response size
- Use pagination: Always set
firstparameter - Filter server-side: Don't fetch and filter client-side
- Batch related queries: Combine in single request
Connection Management
- Reuse HTTP clients: Use connection pooling
- Handle token refresh: Implement automatic refresh
- Implement retry logic: Handle transient failures
- Use timeouts: Set appropriate request timeouts
Security
- Secure credentials: Use secrets management
- Minimal scopes: Request only needed permissions
- Validate responses: Don't trust external data
- Log securely: Don't log tokens or sensitive data
Next Steps
- SDK Overview: Explore available SDK libraries
- Adapter Development: Build custom integrations
- API Reference: Complete API documentation