Integration Manual
Live
Connecting…
HALO GraphQL Integration Manual
Build on HALO.
Automate everything.
A hands-on guide to the Indica Labs GraphQL API — with live code you can run directly against halolink.indicalab.jp.
183
Queries
185
Mutations
68
Subscriptions
799
Public Types

What's in this manual

SectionWhat you'll learn
ArchitectureHow the IdP, GraphQL server, and image tile server connect
AuthenticationOIDC client-credentials flow, getting a bearer token
QueriesRead images, studies, jobs, settings, metadata fields
MutationsCatalog images, create annotations, schedule analysis jobs
SubscriptionsReal-time WebSocket events for barcode scan, cataloging, job lifecycle
Setup guidesStep-by-step for Python (aiohttp + gql) and .NET 8 (Hot Chocolate / HttpClient)
Live execution: every code example has a ▶ Run button. It fires a real HTTP request to halolink.indicalab.jp using the bearer token in the top bar. Paste your own token, or use the demo read-only token pre-filled above.
Getting Started › Architecture

Architecture Overview

The HALO Link API stack has three services. Understanding how they relate saves debugging time.

🔐
Identity Provider
OIDC / OAuth 2.0
/idsrv/connect/token
GraphQL API
HTTPS + WSS
/graphql
🖼️
Image Server
Deep Zoom (DZI)
JWT-gated tiles

Identity Provider (IdP)

Handles authentication via OpenID Connect. Your app presents a client_id + client_secret and receives a short-lived JWT access token. This token is then attached as a Bearer header on every GraphQL request.

GraphQL API

The single endpoint for all data operations. Use HTTPS POST for queries and mutations. Use WSS (WebSocket) for subscriptions and long-lived sessions — the Apollo subprotocol is required.

Image Server (Deep Zoom)

A separate tile server for whole-slide images. You first call GraphQL to get an image-specific JWT, then use that JWT to fetch pixel tiles directly from the image server. This keeps image access auditable and scoped per-image.

Key URLs
GraphQL endpoint: https://halolink.indicalab.jp/graphql
Interactive workbench: https://halolink.indicalab.jp/graphql/workbench
Token endpoint: https://halolink.indicalab.jp/idsrv/connect/token
Getting Started › Authentication

Authentication

HALO uses the OAuth 2.0 Client Credentials flow. There are no user logins — your service gets a token by proving its identity with a client ID and secret.

1
Admin creates a service client — run once on the IdP server to generate a client_id and client_secret.
2
Your app requests a token — POST to /idsrv/connect/token with credentials. Receive a JWT valid for ~1 hour.
3
Attach token to every request — HTTP header: Authorization: Bearer <token>
4
Refresh before expiry — tokens expire. Re-request from step 2 when you get a 401.

Step 1 — Create a service client (admin, one-time)

Run this on the Windows server where the Identity Provider is installed:

# Run on the Identity Provider server
cd "C:\Program Files\Indica Labs\Identity Provider"

IndicaLabs.ApplicationLayer.Halo.IdentityProvider.exe reconfigure ^
  --script AddResearchServiceClient ^
  "client_type=myapp;scopes=serviceuser|graphql"
# C:\ProgramData\Indica Labs\Configuration\...\local-production.yml
identity_provider:
  clients:
    - !OidcClient/ClientCredentials
      id: integra_INDICA-CUSTOM
      scopes:
        - serviceuser
        - graphql
      require_client_secret: true
      secrets:
        - secret: cRaOX+2LW4gjrXHmlaKlag==
      implementation: !OidcImpl/HALO {}
Security: The secret grants unrestricted API access. Store it in a secrets manager (Azure Key Vault, AWS Secrets Manager, etc.) — never in source code.
CORS: The IdP must have https://dev.indicalab.jp in allowed_cors_origins for integra_INDICA-CUSTOM in its config for browser token requests to work.

Step 2 — Request an access token

curl -X POST https://halolink.indicalab.jp/idsrv/connect/token \
  -d "client_id=integra_INDICA-CUSTOM" \
  -d "client_secret=cRaOX+2LW4gjrXHmlaKlag==" \
  -d "scope=serviceuser graphql" \
  -d "grant_type=client_credentials"
import aiohttp, ssl, asyncio

CLIENT_ID     = "integra_INDICA-CUSTOM"
CLIENT_SECRET = "cRaOX+2LW4gjrXHmlaKlag=="
TOKEN_URL     = "https://halolink.indicalab.jp/idsrv/connect/token"

async def request_access_token():
    async with aiohttp.ClientSession() as session:
        async with session.request(
            method="post",
            url=TOKEN_URL,
            data={
                "client_id":     CLIENT_ID,
                "client_secret": CLIENT_SECRET,
                "scope":         "serviceuser graphql",
                "grant_type":    "client_credentials",
            },
            ssl=ssl.SSLContext(ssl.PROTOCOL_TLS),
        ) as resp:
            data = await resp.json()
            return data["access_token"]
using System.Net.Http;

const string CLIENT_ID     = "integra_INDICA-CUSTOM";
const string CLIENT_SECRET = "cRaOX+2LW4gjrXHmlaKlag==";
const string TOKEN_URL     = "https://halolink.indicalab.jp/idsrv/connect/token";

var client = new HttpClient();
var resp = await client.PostAsync(TOKEN_URL,
    new FormUrlEncodedContent(new[] {
        KeyValuePair.Create("client_id",     CLIENT_ID),
        KeyValuePair.Create("client_secret", CLIENT_SECRET),
        KeyValuePair.Create("scope",         "serviceuser graphql"),
        KeyValuePair.Create("grant_type",    "client_credentials"),
    }));
var json = JsonSerializer.Deserialize<TokenResponse>(
    await resp.Content.ReadAsStringAsync());
var token = json.AccessToken; // use as Bearer header
Getting Started › API Workbench

API Workbench

The GraphiQL workbench at halolink.indicalab.jp/graphql/workbench is the fastest way to prototype queries before writing code.

PanelWhat it does
Left — Query EditorWrite GraphQL. Hit ▶ to execute. PRETTIFY auto-formats. HISTORY recalls past queries.
Right — Schema BrowserClick the green SCHEMA tab to browse all queries, mutations, subscriptions, and types with their argument signatures.
Bottom — Variables & HeadersQUERY VARIABLES: pass {"pk": 42} JSON into $pk variables. HTTP HEADERS: set Authorization: Bearer <token> here.
COPY CURL buttonConverts the current query to a cURL command you can paste into a terminal or script.
Workflow tip: prototype in the workbench → verify the response shape → copy the GQL string into your Python or .NET code. The schema browser autocomplete is your best friend.

Setting your token in the workbench

1
Open the workbench URL in your browser.
2
Click HTTP HEADERS at the bottom.
3
Enter: {"Authorization": "Bearer YOUR_TOKEN_HERE"}
4
Run any query — you should see live data in the right panel.
Getting Started › GraphQL Concepts

GraphQL Concepts

ConceptDescriptionTag
QueryRead-only. Ask for exactly the fields you need — no over-fetching. Parameterise with $vars.Query
MutationWrite operations. Return the modified node plus a failed list for partial errors.Mutation
SubscriptionLong-lived WebSocket. Server pushes events as they happen — no polling.Sub

IDs vs Primary Keys

FieldTypeUse when
idID! (Base64 Relay node ID)Connecting objects in mutations, subscriptions, references across types
pkInt! (database integer)Looking up from HALO's own UI, logs, or external LIMS systems

Pagination (Relay Cursor Pattern)

query GetImages($first: Int, $after: Cursor) {
  images(first: $first, after: $after) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges {
      cursor
      node { id pk location tag }
    }
  }
}

Filtering with where clauses

query FilterImages {
  images(first: 25, query: {
    where: {
      q: [{ f: "location", pred: { op: CONTAINS, v: "sample" } }]
    }
    orderBy: {
      q: [{ f: "createdTime", dir: DESC }]
    }
  }) {
    totalCount
    edges { node { id location createdTime } }
  }
}
OperatorMeaning
EQ / NEQEqual / Not equal
GT / GTE / LT / LTENumeric/date comparison
CONTAINS / NCONTAINSString substring match
STARTS / ENDSPrefix / suffix
IN_SETSemicolon-separated set membership
LIKESQL LIKE syntax (% wildcards)
Live Examples › Query

Query Image by Primary Key

The most common starting point. Use imageByPk when you know the integer pk from HALO's database or UI. Returns the image's location path, barcode, stain, and metadata.

query GetImage($pk: Int!) {
  imageByPk(pk: $pk) {
    id
    pk
    location
    barcode
    tag
    stain
    width
    height
  }
}

// Variables
{ "pk": 1 }
from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport

transport = AIOHTTPTransport(
    url="https://halolink.indicalab.jp/graphql",
    headers={"Authorization": f"Bearer {token}"}
)

async with Client(transport=transport) as session:
    result = await session.execute(
        gql("""
        query GetImage($pk: Int!) {
          imageByPk(pk: $pk) {
            id pk location barcode tag stain width height
          }
        }
        """),
        variable_values={"pk": 1}
    )
    print(result["imageByPk"]["location"])
using System.Net.Http.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new("Bearer", token);

var payload = new {
    query = @"query GetImage($pk: Int!) {
      imageByPk(pk: $pk) {
        id pk location barcode tag stain width height
      }
    }",
    variables = new { pk = 1 }
};

var resp = await client.PostAsJsonAsync(
    "https://halolink.indicalab.jp/graphql", payload);
var json = await resp.Content.ReadAsStringAsync();

Return fields explained

FieldTypeNotes
idID!Relay node ID — use this in mutations
pkInt!Database integer — matches HALO's UI pk
locationStringFile system path to the slide file
barcodeStringBarcode value if scanned
stainStringStain type e.g. H&E, IHC
width / heightIntPixel dimensions of the whole slide
Live Examples › Query

Query Study by Primary Key

Studies are the top-level containers for images in HALO. Use studyByPk to retrieve a study and its metadata.

query GetStudy($pk: Int!) {
  studyByPk(pk: $pk) {
    pk
    id
    name
    description
    createdTime
    permission
  }
}

// Variables
{ "pk": 1 }
result = await session.execute(
    gql("""
    query GetStudy($pk: Int!) {
      studyByPk(pk: $pk) {
        pk id name description createdTime permission
      }
    }
    """),
    variable_values={"pk": 1}
)
study = result["studyByPk"]
print(f"{study['name']} — {study['createdTime']}")
var payload = new {
    query = @"query GetStudy($pk: Int!) {
      studyByPk(pk: $pk) {
        pk id name description createdTime permission
      }
    }",
    variables = new { pk = 1 }
};
var resp = await client.PostAsJsonAsync(
    "https://halolink.indicalab.jp/graphql", payload);
Live Examples › Query

Query Analysis Settings by Name

Find a saved analysis algorithm profile by its savedName. The returned id is what you pass to scheduleAnalysis.

query FindSettings($name: String!) {
  settings(query: {
    where: {
      q: { f: "savedName", pred: { op: EQ, v: $name } }
    }
  }) {
    edges {
      node {
        pk
        id
        algorithmName
        savedName
      }
    }
  }
}

// Variables
{ "name": "Shared Classifier" }
result = await session.execute(
    gql("""
    query FindSettings($name: String!) {
      settings(query: {
        where: { q: { f: "savedName", pred: { op: EQ, v: $name } } }
      }) {
        edges { node { pk id algorithmName savedName } }
      }
    }
    """),
    variable_values={"name": "Shared Classifier"}
)
nodes = [e["node"] for e in result["settings"]["edges"]]
var payload = new {
    query = @"query FindSettings($name: String!) {
      settings(query: {
        where: { q: { f: ""savedName"", pred: { op: EQ, v: $name } } }
      }) {
        edges { node { pk id algorithmName savedName } }
      }
    }",
    variables = new { name = "Shared Classifier" }
};
Live Examples › Query

Query Analysis Job Progress

Poll a running job with jobById. The condition field cycles through QUEUED → RUNNING → COMPLETE (or FAILED / CANCELLED).

Tip: For real-time progress without polling, use the analysisProgressEvent subscription instead.
query GetJobProgress($id: ID!) {
  jobById(id: $id) {
    pk
    id
    settingsSavedName
    progress {
      condition
      percentComplete
      errorMessage
    }
    image { id pk location }
  }
}

// Variables — replace with a real job ID
{ "id": "QW5hbHlzaXNKb2I6MTIz" }
import asyncio

async def poll_job(session, job_id):
    while True:
        result = await session.execute(
            gql("""
            query GetJobProgress($id: ID!) {
              jobById(id: $id) {
                progress { condition percentComplete errorMessage }
              }
            }
            """),
            variable_values={"id": job_id}
        )
        prog = result["jobById"]["progress"]
        print(f"{prog['condition']} — {prog['percentComplete']}%")
        if prog["condition"] in ("COMPLETE", "FAILED", "CANCELLED"):
            break
        await asyncio.sleep(5)
var payload = new {
    query = @"query GetJobProgress($id: ID!) {
      jobById(id: $id) {
        pk id settingsSavedName
        progress { condition percentComplete errorMessage }
      }
    }",
    variables = new { id = "QW5hbHlzaXNKb2I6MTIz" }
};
Live Examples › Mutation

Mutation Catalog an Image

Register a whole-slide image file into HALO's database. Provide the UNC/network path and optionally link to an existing study.

Write operation: This creates a real database record. Use a test study in non-production environments.
mutation CatalogImage($path: String!, $studyId: ID!) {
  catalogImage(input: {
    location: $path
    studyId: $studyId
  }) {
    mutated {
      node { pk id location }
    }
    failed { error }
  }
}

// Variables
{
  "path": "\\\\server\\share\\images\\slide001.svs",
  "studyId": "U3R1ZHk6NQ=="
}
result = await session.execute(
    gql("""
    mutation CatalogImage($path: String!, $studyId: ID!) {
      catalogImage(input: { location: $path, studyId: $studyId }) {
        mutated { node { pk id location } }
        failed  { error }
      }
    }
    """),
    variable_values={
        "path":    r"\\server\share\images\slide001.svs",
        "studyId": "U3R1ZHk6NQ=="
    }
)
if result["catalogImage"]["failed"]:
    print("Errors:", result["catalogImage"]["failed"])
else:
    node = result["catalogImage"]["mutated"][0]["node"]
    print(f"Cataloged pk={node['pk']}")
var payload = new {
    query = @"mutation CatalogImage($path: String!, $studyId: ID!) {
      catalogImage(input: { location: $path, studyId: $studyId }) {
        mutated { node { pk id location } }
        failed  { error }
      }
    }",
    variables = new {
        path    = @"\\server\share\images\slide001.svs",
        studyId = "U3R1ZHk6NQ=="
    }
};

Response structure

FieldMeaning
mutated[].nodeThe newly created image record(s)
failed[].errorError message if the file path wasn't accessible, or the study ID was invalid
Live Examples › Mutation

Mutation Create Annotation Layer + Draw Region

Annotations are two-step: first create a layer on the image, then draw one or more regions (polygons, rectangles, etc.) on that layer.

# Step 1 — Create a layer
mutation CreateLayer($imageId: ID!) {
  createAnnotationLayer(input: { id: $imageId }) {
    mutated { node { id name } }
    failed  { error }
  }
}

# Step 2 — Draw a region on the layer (pentagon example)
mutation DrawRegion($input: DrawAnnotationRegionInput!) {
  drawAnnotationRegion(input: $input) {
    failed { error }
  }
}

// Variables for Step 2
{
  "input": {
    "layerId": "<layer_id from step 1>",
    "shapeType": "POLYGON",
    "geometry": "{\"type\":\"LineString\",\"coordinates\":[[5000,0],[9755,-3090],[6545,-8090],[3455,-8090],[245,-3090],[5000,0]]}"
  }
}
import math, json

# Compute pentagon vertices
ORIGIN, RADIUS = (5000, -5000), 5000
vertices = [
    [round(ORIGIN[0] + RADIUS * math.sin(2*math.pi*i/5)),
     round(ORIGIN[1] + RADIUS * math.cos(2*math.pi*i/5))]
    for i in range(5)
]
vertices.append(vertices[0])  # close ring
geometry = json.dumps({"type": "LineString", "coordinates": vertices})

# Step 1 — create layer
r1 = await session.execute(gql("""
  mutation CreateLayer($imageId: ID!) {
    createAnnotationLayer(input: { id: $imageId }) {
      mutated { node { id } }
    }
  }
"""), variable_values={"imageId": image_id})
layer_id = r1["createAnnotationLayer"]["mutated"][0]["node"]["id"]

# Step 2 — draw
await session.execute(gql("""
  mutation DrawRegion($input: DrawAnnotationRegionInput!) {
    drawAnnotationRegion(input: $input) { failed { error } }
  }
"""), variable_values={"input": {
    "layerId": layer_id, "shapeType": "POLYGON", "geometry": geometry
}})
// Step 1 — create layer
var step1 = new {
    query = @"mutation CreateLayer($imageId: ID!) {
      createAnnotationLayer(input: { id: $imageId }) {
        mutated { node { id name } }
        failed  { error }
      }
    }",
    variables = new { imageId = imageId }
};
// execute step1, extract layerId from response

// Step 2 — draw region
var step2 = new {
    query = @"mutation DrawRegion($input: DrawAnnotationRegionInput!) {
      drawAnnotationRegion(input: $input) { failed { error } }
    }",
    variables = new {
        input = new {
            layerId   = layerId,
            shapeType = "POLYGON",
            geometry  = "{\"type\":\"LineString\",\"coordinates\":[[5000,0]...]}"
        }
    }
};
Live Examples › Mutation

Mutation Schedule an Analysis Job

Queue a HALO analysis on an image using a saved settings profile. You need the settings id (from the settings query) and the image id.

mutation ScheduleJob(
  $settingsId: ID!
  $imageId: ID!
) {
  scheduleAnalysis(input: {
    settingsId: $settingsId
    analysisDescriptions: [{ imageId: $imageId, fieldOfView: null }]
  }) {
    mutated {
      node {
        id
        pk
        settingsSavedName
        image { id pk }
      }
    }
    failed { error }
  }
}

// Variables
{
  "settingsId": "U2F2ZWRBbGdvcml0aG1BbmFseXNpc1NldHRpbmdzOjE=",
  "imageId":    "SW1hZ2U6NDI="
}
result = await session.execute(
    gql("""
    mutation ScheduleJob($settingsId: ID!, $imageId: ID!) {
      scheduleAnalysis(input: {
        settingsId: $settingsId
        analysisDescriptions: [{ imageId: $imageId, fieldOfView: null }]
      }) {
        mutated { node { id pk settingsSavedName } }
        failed  { error }
      }
    }
    """),
    variable_values={
        "settingsId": settings_id,
        "imageId":    image_id,
    }
)
job_id = result["scheduleAnalysis"]["mutated"][0]["node"]["id"]
# now poll with jobById or subscribe to analysisFinishedEvent
var payload = new {
    query = @"mutation ScheduleJob($settingsId: ID!, $imageId: ID!) {
      scheduleAnalysis(input: {
        settingsId: $settingsId
        analysisDescriptions: [{ imageId: $imageId, fieldOfView: null }]
      }) {
        mutated { node { id pk settingsSavedName } }
        failed  { error }
      }
    }",
    variables = new {
        settingsId = settingsId,
        imageId    = imageId
    }
};
Next step: take the returned job id and poll with jobById (see Job Progress query) or subscribe to analysisFinishedEvent for a push notification.
Live Examples › Subscription

Subscription Barcode Scanned Event

Fires in real time whenever a barcode is scanned or changed on any image in HALO Link. Subscriptions require a WebSocket connection — they don't work over plain HTTP POST.

Transport note: Use the graphql-ws or Apollo subprotocol over wss://halolink.indicalab.jp/graphql. The Run button below simulates the connection.
subscription {
  barcodeScannedEvent {
    imageId
  }
}

# Then fetch image details on each event:
query GetById($id: ID!) {
  imageById(id: $id) { id pk barcode }
}
from gql.transport.websockets import WebsocketsTransport

transport = WebsocketsTransport(
    url="wss://halolink.indicalab.jp/graphql",
    headers={"authorization": f"bearer {token}"},
    subprotocols=[WebsocketsTransport.APOLLO_SUBPROTOCOL,]
)

async with Client(transport=transport) as session:
    async for msg in session.subscribe(gql("""
        subscription { barcodeScannedEvent { imageId } }
    """)):
        image_id = msg["barcodeScannedEvent"]["imageId"]
        # fetch full image details
        image = await session.execute(
            gql('query($id:ID!){imageById(id:$id){barcode}}'),
            variable_values={"id": image_id}
        )
        print(f"Barcode: {image['imageById']['barcode']}")
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.SystemTextJson;

var graphQL = new GraphQLHttpClient(
    new GraphQLHttpClientOptions {
        EndPoint = new Uri("https://halolink.indicalab.jp/graphql"),
        WebSocketEndPoint = new Uri("wss://halolink.indicalab.jp/graphql")
    },
    new SystemTextJsonSerializer()
);
graphQL.HttpClient.DefaultRequestHeaders.Authorization =
    new("Bearer", token);

var sub = graphQL.CreateSubscriptionStream<BarcodeEvent>(
    new GraphQLRequest {
        Query = "subscription { barcodeScannedEvent { imageId } }"
    });
sub.Subscribe(msg => Console.WriteLine(msg.Data.BarcodeScannedEvent.ImageId));
Live Examples › Subscription

Subscription Image Cataloged + Study Events

Two events useful for LIMS integration: react whenever a new image lands in HALO, or whenever an image is added to a study.

# Fires when a new image is cataloged
subscription {
  imageCatalogedEvent {
    imageId
    imageLocation
  }
}

# Fires when an image is added to a study
subscription {
  imageAddedToStudyEvent {
    studyId
    imageId
  }
}
import asyncio

async def on_cataloged(session):
    async for msg in session.subscribe(gql("""
        subscription { imageCatalogedEvent { imageId imageLocation } }
    """)):
        ev = msg["imageCatalogedEvent"]
        print(f"New image: {ev['imageLocation']}")

async def on_added_to_study(session):
    async for msg in session.subscribe(gql("""
        subscription { imageAddedToStudyEvent { studyId imageId } }
    """)):
        ev = msg["imageAddedToStudyEvent"]
        # update LIMS here

async def main():
    session = await create_client_session(token)
    await asyncio.gather(
        asyncio.create_task(on_cataloged(session)),
        asyncio.create_task(on_added_to_study(session)),
    )
// Run two subscriptions concurrently
var sub1 = graphQL.CreateSubscriptionStream<CatalogedEvent>(
    new GraphQLRequest {
        Query = "subscription{imageCatalogedEvent{imageId imageLocation}}"
    });
var sub2 = graphQL.CreateSubscriptionStream<StudyEvent>(
    new GraphQLRequest {
        Query = "subscription{imageAddedToStudyEvent{studyId imageId}}"
    });

sub1.Subscribe(e => Console.WriteLine($"Cataloged: {e.Data}"));
sub2.Subscribe(e => Console.WriteLine($"Added to study: {e.Data}"));
Setup › Python

Python Setup

Official example code: gitlab.com/indica_labs_public/example-code

requirements.txt

aiohttp==3.8.5
gql[all]==3.4.1
pyyaml==6.0.1
requests==2.32.3
urllib3==1.26.20
pillow==11.1.0

Setup steps

1
Clone & setup
cd research && setup.bat — creates a virtual env and installs all deps
2
Configure settings.yml
authorization:
  hostname:      halolink.indicalab.jp
  client_id:     example_WHERE-IS-WALDO
  client_secret: your-secret-here
3
Activate & verify
.\Scripts\activate.bat then python main.py — should print an access token
4
Run examples
python .\examples\query_image.py 42

Client session helper

from gql import Client
from gql.transport.websockets import WebsocketsTransport
import ssl

async def create_client_session(token: str):
    transport = WebsocketsTransport(
        url="wss://halolink.indicalab.jp/graphql",
        headers={"authorization": f"bearer {token}"},
        subprotocols=[WebsocketsTransport.APOLLO_SUBPROTOCOL],
        ssl=ssl.SSLContext(ssl.PROTOCOL_TLS),
    )
    client = Client(transport=transport)
    return await client.connect_async()
Setup › .NET 8

.NET 8 Setup

Use GraphQL.Client for HTTP queries/mutations and WebSocket subscriptions. Pair with System.Text.Json for deserialization.

NuGet packages

<!-- Add to your .csproj -->
<PackageReference Include="GraphQL.Client"                      Version="6.*" />
<PackageReference Include="GraphQL.Client.Serializer.SystemTextJson" Version="6.*" />
<PackageReference Include="Microsoft.Extensions.Http"              Version="8.*" />

# Or via CLI
dotnet add package GraphQL.Client
dotnet add package GraphQL.Client.Serializer.SystemTextJson

Client factory (DI-friendly)

using GraphQL.Client.Http;
using GraphQL.Client.Serializer.SystemTextJson;

public static class HaloClientFactory
{
    public static GraphQLHttpClient Create(string token)
    {
        var options = new GraphQLHttpClientOptions
        {
            EndPoint          = new Uri("https://halolink.indicalab.jp/graphql"),
            WebSocketEndPoint = new Uri("wss://halolink.indicalab.jp/graphql"),
        };
        var client = new GraphQLHttpClient(options, new SystemTextJsonSerializer());
        client.HttpClient.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        return client;
    }
}

Executing a query

var graphQL = HaloClientFactory.Create(token);

var request = new GraphQLRequest
{
    Query = @"query GetImage($pk: Int!) {
      imageByPk(pk: $pk) { id pk location barcode stain }
    }",
    Variables = new { pk = 42 }
};

var response = await graphQL.SendQueryAsync<ImageResponse>(request);
Console.WriteLine(response.Data.ImageByPk.Location);
Setup › Troubleshooting

Troubleshooting & Tips

SymptomFix
401 UnauthorizedCheck (1) IdP service is running, (2) client_id/secret match local-production.yml exactly, (3) scopes include both serviceuser and graphql.
SSL/TLS certificate errorPython examples use ssl.SSLContext(ssl.PROTOCOL_TLS) without certificate verification. In production use ssl.create_default_context() with your CA bundle.
Subscription receives no eventsConfirm APOLLO_SUBPROTOCOL is set. Verify the event is actually triggering in HALO Link (e.g. physically scan a barcode to test barcodeScannedEvent).
Mutation returns wrong ID type errorMutations expect the opaque Base64 id (type ID!), not the integer pk. Always read back id from a query before passing to a mutation.
Large dataset takes too longDefault pagination returns a page at a time. Pass first: -1 to get all records — but use with caution on large collections.
null returned for a known objectThe token may not have permission to that resource, or the pk/id doesn't exist in this environment.

Filtering quick reference

# Single filter
query: { where: { q: { f: "location", pred: { op: CONTAINS, v: "sample" } } } }

# Multiple filters (AND)
query: { where: { q: [
  { f: "stain", pred: { op: EQ, v: "H&E" } },
  { f: "tag",   pred: { op: STARTS, v: "2024" } }
] } }

# Nested field path
query: { where: { q: { f: "systemField.name", pred: { op: EQ, v: "Sample" } } } }

# Get all (no pagination limit)
images(first: -1) { edges { node { id location } } }