Skip to main content

12. C# and Java REST Integration

When To Use This Integration Path

Use this path when your application stack is primarily:

  • C#
  • Java
  • Spring Boot
  • ASP.NET
  • a custom orchestration layer that does not use the official JS, Python, or Vercel SDK wrappers

You do not need to wait for a native SDK to integrate AgentID. The public Data Plane API is enough to enforce guardrails and persist runtime telemetry.

What You Get Without An SDK

Direct REST integration still gives you the core backend contract:

  • /api/v1/guard remains the enforcement authority
  • prompts can still be blocked before provider billing
  • strict PII leakage checks still run on the backend
  • transformed_input can still be returned for prompt rewriting
  • /api/v1/ingest still records telemetry, cost, and business metrics
  • /api/v1/ingest/finalize still closes the lifecycle row

What You Must Implement Yourself

Without an official SDK, you must own the orchestration that the wrappers normally provide:

  • generate and persist a stable client_event_id
  • call /api/v1/guard before the provider call
  • stop execution when allowed=false
  • apply transformed_input before calling the upstream LLM
  • call /api/v1/ingest after the model returns
  • optionally call /api/v1/ingest/finalize
  • extract text yourself for multimodal requests
  • implement any local client-side masking or fallback logic you want

Practical rule:

prompt_for_provider = guardResponse.transformed_input ?? original_input

If you skip this step, backend rewrite/masking decisions will not reach the actual provider call.

Do not spread AgentID calls across controllers or frontend code.

Put the integration in the backend layer that already owns model execution, for example:

  • AiGateway
  • ModelExecutionService
  • LlmOrchestrator
  • a shared Spring Service
  • a shared ASP.NET application service

That gives you one place to enforce:

  • guard-before-execute
  • request correlation
  • ingest telemetry
  • retry behavior

Runtime Flow

The recommended direct integration lifecycle is:

1. Generate client_event_id
2. Optional: GET /api/v1/agent/config
3. POST /api/v1/guard
4. If denied: stop
5. If allowed: use transformed_input when present
6. Call the LLM provider
7. POST /api/v1/ingest
8. Optional: POST /api/v1/ingest/finalize

Authentication

Use one of these headers on every request:

x-agentid-api-key: sk_live_...
Content-Type: application/json

or:

Authorization: Bearer sk_live_...
Content-Type: application/json

x-agentid-api-key is the preferred server-side header.

Correlation And Idempotence

Use these identifiers consistently:

  • client_event_id: one stable UUID for the full request lifecycle
  • guard_event_id: returned by /api/v1/guard, pass it into ingest metadata
  • event_id: idempotency key for /api/v1/ingest

Recommended implementation:

  • set event_id == client_event_id
  • reuse the same event_id on safe network retries to /api/v1/ingest

Guard Request Example

{
"system_id": "cf84f936-95bc-45a4-bf98-9a1a16bf29c9",
"input": "Summarize this claims review for the customer.",
"model": "gpt-4o-mini",
"user_id": "user-123",
"client_event_id": "0467f78f-5821-4827-a09b-d59a9b08866c",
"request_identity": {
"user_agent": "Continero-App/1.0",
"tenant_id": "acme-prod"
},
"client_capabilities": {
"framework": "custom-java"
}
}

Guard Response Fields To Honor

At minimum, your code should read:

  • allowed
  • reason
  • client_event_id
  • guard_event_id
  • transformed_input
  • shadow_mode
  • simulated_decision

Behavior:

  • if allowed=false, do not call the provider
  • if allowed=true and transformed_input exists, call the provider with transformed_input

Ingest Request Example

{
"event_id": "0467f78f-5821-4827-a09b-d59a9b08866c",
"system_id": "cf84f936-95bc-45a4-bf98-9a1a16bf29c9",
"input": "Summarize this claims review for the customer.",
"output": "Here is a concise summary of the claims review.",
"model": "gpt-4o-mini",
"usage": {
"prompt_tokens": 33,
"completion_tokens": 19,
"total_tokens": 52
},
"tokens": {
"input": 33,
"output": 19,
"total": 52
},
"latency": 1450,
"event_type": "complete",
"severity": "info",
"timestamp": "2026-03-24T11:30:00Z",
"user_id": "user-123",
"metadata": {
"client_event_id": "0467f78f-5821-4827-a09b-d59a9b08866c",
"guard_event_id": "7a7394af-77a9-4d32-a714-ad7af7ab86f3"
}
}

Finalize Request Example

{
"client_event_id": "0467f78f-5821-4827-a09b-d59a9b08866c",
"system_id": "cf84f936-95bc-45a4-bf98-9a1a16bf29c9",
"sdk_ingest_ms": 83
}

PII, Masking, And Rewrite Semantics

There are two different layers to understand:

Backend Guard Enforcement

This works without an SDK:

  • strict PII leakage detection
  • hard blocks from /api/v1/guard
  • backend prompt rewrite via transformed_input

SDK-Local Client Behavior

This is not automatic without an SDK:

  • local client-side masking before remote guard
  • local fail-close fallback logic when backend guard is unreachable
  • automatic prompt rewriting inside provider wrappers

Direct REST customers therefore still get backend protection, but they must implement the client-side wrapper behavior themselves if they want near-SDK parity.

Multimodal Requests

Direct REST integrations should follow the same contract as the official SDKs:

  • send text-bearing content to /api/v1/guard
  • pass binary attachments through to the provider unchanged
  • do not expect the guard hot path to OCR or vision-scan files

If your application accepts image or file attachments, extract the user text for /guard and preserve the original attachment payload for the upstream provider call.

Streaming Guidance

For streaming providers:

  1. call /api/v1/guard before starting the stream
  2. start the provider stream only if allowed
  3. collect final usage and latency after stream completion
  4. call /api/v1/ingest
  5. optionally call /api/v1/ingest/finalize

The direct REST path works for streaming, but you must own the end-of-stream telemetry orchestration yourself.

C# Pseudo-Code

public async Task<string> GenerateWithAgentIdAsync(
string systemId,
string userPrompt,
string model,
string userId)
{
var clientEventId = Guid.NewGuid().ToString();
var eventId = clientEventId;

var guard = await AgentIdGuardAsync(new
{
system_id = systemId,
input = userPrompt,
model = model,
user_id = userId,
client_event_id = clientEventId,
client_capabilities = new
{
framework = "csharp-custom"
}
});

if (!guard.allowed)
{
throw new SecurityException($"AgentID blocked request: {guard.reason}");
}

var promptForProvider = !string.IsNullOrWhiteSpace(guard.transformed_input)
? guard.transformed_input
: userPrompt;

var llmResult = await CallProviderAsync(promptForProvider, model);

await AgentIdIngestAsync(new
{
event_id = eventId,
system_id = systemId,
input = promptForProvider,
output = llmResult.Text,
model = model,
usage = new
{
prompt_tokens = llmResult.PromptTokens,
completion_tokens = llmResult.CompletionTokens,
total_tokens = llmResult.TotalTokens
},
tokens = new
{
input = llmResult.PromptTokens,
output = llmResult.CompletionTokens,
total = llmResult.TotalTokens
},
latency = llmResult.LatencyMs,
event_type = "complete",
severity = "info",
timestamp = DateTime.UtcNow.ToString("O"),
user_id = userId,
metadata = new
{
client_event_id = clientEventId,
guard_event_id = guard.guard_event_id
}
});

await AgentIdFinalizeAsync(new
{
client_event_id = clientEventId,
system_id = systemId,
sdk_ingest_ms = 50
});

return llmResult.Text;
}

Java Pseudo-Code

public String generateWithAgentId(
String systemId,
String userPrompt,
String model,
String userId) throws Exception {

String clientEventId = UUID.randomUUID().toString();
String eventId = clientEventId;

Map<String, Object> guardRequest = new HashMap<>();
guardRequest.put("system_id", systemId);
guardRequest.put("input", userPrompt);
guardRequest.put("model", model);
guardRequest.put("user_id", userId);
guardRequest.put("client_event_id", clientEventId);

Map<String, Object> clientCapabilities = new HashMap<>();
clientCapabilities.put("framework", "java-custom");
guardRequest.put("client_capabilities", clientCapabilities);

GuardResponse guard = agentIdGuard(guardRequest);

if (!guard.allowed()) {
throw new RuntimeException("AgentID blocked request: " + guard.reason());
}

String promptForProvider = (guard.transformedInput() != null && !guard.transformedInput().isBlank())
? guard.transformedInput()
: userPrompt;

ProviderResult llm = callProvider(promptForProvider, model);

Map<String, Object> ingestRequest = new HashMap<>();
ingestRequest.put("event_id", eventId);
ingestRequest.put("system_id", systemId);
ingestRequest.put("input", promptForProvider);
ingestRequest.put("output", llm.text());
ingestRequest.put("model", model);
ingestRequest.put("latency", llm.latencyMs());
ingestRequest.put("event_type", "complete");
ingestRequest.put("severity", "info");
ingestRequest.put("timestamp", Instant.now().toString());
ingestRequest.put("user_id", userId);

Map<String, Object> usage = new HashMap<>();
usage.put("prompt_tokens", llm.promptTokens());
usage.put("completion_tokens", llm.completionTokens());
usage.put("total_tokens", llm.totalTokens());
ingestRequest.put("usage", usage);

Map<String, Object> tokens = new HashMap<>();
tokens.put("input", llm.promptTokens());
tokens.put("output", llm.completionTokens());
tokens.put("total", llm.totalTokens());
ingestRequest.put("tokens", tokens);

Map<String, Object> metadata = new HashMap<>();
metadata.put("client_event_id", clientEventId);
metadata.put("guard_event_id", guard.guardEventId());
ingestRequest.put("metadata", metadata);

agentIdIngest(ingestRequest);

Map<String, Object> finalizeRequest = new HashMap<>();
finalizeRequest.put("client_event_id", clientEventId);
finalizeRequest.put("system_id", systemId);
finalizeRequest.put("sdk_ingest_ms", 50);

agentIdFinalize(finalizeRequest);

return llm.text();
}