Skip to content
synthreo.ai

API Code Examples - Synthreo Builder

Code examples for the Builder API - ready-to-use curl, JavaScript, and Python samples for executing agents, managing users, and handling async job responses.

This guide provides comprehensive code examples for integrating with the Synthreo Builder API across multiple programming languages. Each example demonstrates the complete workflow: authentication, job execution, and status monitoring.

Before running these examples, ensure you have:

  • A Synthreo API key - its key id and secret. See Authentication to create one.
  • A cognitive diagram ID for execution
  • Node IDs for training operations (if applicable)

Environment Variables:

Terminal window
SYNTHREO_API_KEY_ID=42
SYNTHREO_API_KEY_SECRET=sak_your_key_secret_here
SYNTHREO_DIAGRAM_ID=12345
SYNTHREO_TRAINING_NODE_ID=your-training-node-uuid
Terminal window
curl -X POST 'https://auth.synthreo.ai/connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=apikey-42' \
--data-urlencode 'client_secret=sak_your_key_secret_here' \
--data-urlencode 'target_app=builder'
Terminal window
curl -X POST 'https://builder-api.synthreo.ai/CognitiveDiagram/12345/Execute' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Origin: https://builder.synthreo.ai' \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
-d '{
"Action": "Execute",
"UserSays": "[{\"userSays\":\"Hello, how can you help me?\"}]"
}'
Terminal window
# -i so you can see the status: 202 means poll, 200 means this IS the result.
curl -i -X POST 'https://builder-api.synthreo.ai/CognitiveDiagram/12345/ExecuteAsJob' \
-H 'Content-Type: application/json' \
-H 'Accept: */*' \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
-d '{
"Action": "Execute",
"UserSays": "[{\"userSays\":\"start processing\"}]",
"RobotSays": "",
"CallerSource": 1
}'
# Only if the POST answered 202: poll the job id from its body (or its Location
# header). A 200 above already returned the result and there is nothing to poll.
curl -i -X GET 'https://builder-api.synthreo.ai/job/JOB_ID' \
-H 'Accept: */*' \
-H 'Authorization: Bearer YOUR_JWT_TOKEN'
Terminal window
curl -X PATCH 'https://builder-api.synthreo.ai/CognitiveDiagram/8139/TrainNode' \
-H 'Content-Type: application/json' \
-H 'Accept: */*' \
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
-d '{
"nodeId": "your-training-node-uuid",
"repositoryNodeId": 59,
"finishedFlag": false,
"logText": "Training started by API user"
}'
curl -X GET 'https://builder-api.synthreo.ai/CognitiveDiagram/8139' \
-H 'Accept: */*' \
-H 'Authorization: Bearer YOUR_JWT_TOKEN'
import requests
import json
import time
import os
from typing import Optional, Dict, Any
class SynthreoClient:
def __init__(self, key_id: str, key_secret: str, target_app: str = "builder"):
self.client_id = f"apikey-{key_id}"
self.key_secret = key_secret
self.target_app = target_app
self.token = None
# Access tokens live 15 minutes with no refresh token, so track expiry and
# re-exchange the API key before it lapses (jobs can poll for far longer).
self.token_expiry = 0.0
self.base_url = 'https://builder-api.synthreo.ai'
self.auth_url = 'https://auth.synthreo.ai/connect/token'
def authenticate(self) -> str:
"""Exchange the API key for a JWT access token (client_credentials grant)"""
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.key_secret,
"target_app": self.target_app,
}
try:
response = requests.post(self.auth_url, data=data)
response.raise_for_status()
body = response.json()
self.token = body['access_token']
# Renew a minute early to absorb clock skew and request latency.
self.token_expiry = time.time() + body.get('expires_in', 900) - 60
print('Authentication successful')
return self.token
except requests.exceptions.RequestException as e:
print(f'Authentication failed: {e}')
raise
def get_headers(self) -> Dict[str, str]:
"""Get headers with a valid authorization token, re-authenticating if expired."""
if not self.token or time.time() >= self.token_expiry:
self.authenticate()
return {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
'Accept': '*/*'
}
def execute_diagram(self, diagram_id: int, message: str,
conversation_id: Optional[str] = None) -> Dict[str, Any]:
"""Execute cognitive diagram synchronously"""
url = f'{self.base_url}/CognitiveDiagram/{diagram_id}/Execute'
user_says_data = {"userSays": message}
if conversation_id:
user_says_data["conversationId"] = conversation_id
# json.dumps, not an f-string: a message containing a quote, a backslash, or a
# newline has to be escaped or the API receives invalid JSON.
payload = {
"Action": "Execute",
"UserSays": json.dumps([user_says_data])
}
headers = self.get_headers()
headers.update({
'Accept': 'application/json',
'Origin': 'https://builder.synthreo.ai'
})
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f'Diagram execution failed: {e}')
raise
def execute_as_job(self, diagram_id: int, message: str = "start") -> Dict[str, Any]:
"""Start a job. Returns {'job_id', 'result'}: a quick diagram finishes inline
and comes back as 'result' with no job id to poll."""
url = f'{self.base_url}/CognitiveDiagram/{diagram_id}/ExecuteAsJob'
payload = {
"Action": "Execute",
"UserSays": json.dumps([{"userSays": message}]),
"RobotSays": "",
"CallerSource": 1
}
headers = self.get_headers()
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
# 202 carries a job to poll; 200 means it already finished and the body IS
# the result, with no job id. Handle both or quick jobs crash here.
if response.status_code == 202:
job = data.get('job') if isinstance(data, dict) else None
if not (job and job.get('id')):
raise RuntimeError(f'202 response carried no job id: {data}')
print(f'Job initiated with ID: {job["id"]}')
return {'job_id': job['id'], 'result': None}
print('Job finished immediately; no polling needed')
return {'job_id': None, 'result': data}
except requests.exceptions.RequestException as e:
print(f'Job initiation failed: {e}')
raise
def poll_job_status(self, job_id: str, interval: int = 30,
timeout: int = 3600) -> Dict[str, Any]:
"""Poll job status until completion"""
url = f'{self.base_url}/job/{job_id}'
start_time = time.time()
while True:
if time.time() - start_time > timeout:
raise TimeoutError(f'Job {job_id} timed out after {timeout} seconds')
try:
# Fetch headers each iteration so the 15-minute token is refreshed
# for polls that run longer than the token's lifetime.
# Bound the request too: without a timeout a stalled connection can
# hang past the caller's own deadline.
remaining = timeout - (time.time() - start_time)
response = requests.get(
url,
headers=self.get_headers(),
timeout=(10, min(30, max(1, remaining))),
)
if response.status_code == 202:
print('Job is still running...')
# Never sleep past the deadline.
time.sleep(min(interval, max(0, timeout - (time.time() - start_time))))
continue
elif response.status_code == 200:
# The body IS the result. An empty body means the id is unknown or
# was already read - not success.
if not response.content.strip():
raise RuntimeError(
f'Job {job_id} is unknown, already read, or expired'
)
print('Job completed!')
return response.json()
elif response.status_code == 400:
# The job ran and failed; the envelope carries the reason.
payload = response.json()
raise RuntimeError(payload.get('error') or 'Job failed')
else:
raise requests.exceptions.HTTPError(
f'Unexpected status code: {response.status_code}'
)
except requests.exceptions.RequestException as e:
print(f'Error polling job status: {e}')
raise
def trigger_training(self, diagram_id: int, node_id: str,
repository_node_id: int = 59) -> Dict[str, Any]:
"""Trigger agent training"""
url = f'{self.base_url}/CognitiveDiagram/{diagram_id}/TrainNode'
payload = {
"nodeId": node_id,
"repositoryNodeId": repository_node_id,
"finishedFlag": False,
"logText": "Training started by API user"
}
headers = self.get_headers()
try:
response = requests.patch(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
print(f'Training triggered for agent {diagram_id}')
return data
except requests.exceptions.RequestException as e:
print(f'Training trigger failed: {e}')
raise
def monitor_training(self, diagram_id: int, interval: int = 60,
timeout: int = 3600) -> Dict[str, Any]:
"""Monitor training progress"""
url = f'{self.base_url}/CognitiveDiagram/{diagram_id}'
start_time = time.time()
while True:
if time.time() - start_time > timeout:
raise TimeoutError(f'Training timed out after {timeout} seconds')
try:
# Refresh headers each iteration so a long-running monitor keeps a
# valid 15-minute token.
response = requests.get(url, headers=self.get_headers())
response.raise_for_status()
data = response.json()
state_id = data.get('stateId')
if state_id == 6:
print('Agent is training...')
time.sleep(interval)
elif state_id == 2:
print('Training completed! Agent is ready.')
return data
else:
print(f'Unexpected state ID: {state_id}')
return data
except requests.exceptions.RequestException as e:
print(f'Error monitoring training: {e}')
raise
def parse_response(api_response: Dict[str, Any]) -> str:
"""Parse API response to extract meaningful output"""
if api_response.get('outputData'):
try:
import json
output_data = json.loads(api_response['outputData'])
if isinstance(output_data, list) and output_data:
return str(output_data[0])
elif isinstance(output_data, dict):
# Try common response field names
for field in ['response', 'gpt_response', 'answer', 'result']:
if field in output_data:
return str(output_data[field])
return json.dumps(output_data, indent=2)
else:
return str(output_data)
except (json.JSONDecodeError, KeyError):
return api_response['outputData']
if api_response.get('errorData') and api_response['errorData'] != "[]":
import json
try:
errors = json.loads(api_response['errorData'])
return f"Error: {errors[0].get('message', 'Unknown error')}"
except json.JSONDecodeError:
return f"Error: {api_response['errorData']}"
return "No response generated"
if __name__ == '__main__':
# Initialize client
client = SynthreoClient(
key_id=os.getenv('SYNTHREO_API_KEY_ID'),
key_secret=os.getenv('SYNTHREO_API_KEY_SECRET')
)
diagram_id = int(os.getenv('SYNTHREO_DIAGRAM_ID'))
try:
# Example 1: Synchronous execution
response = client.execute_diagram(diagram_id, "Hello, how are you?")
ai_response = parse_response(response)
print(f"AI Response: {ai_response}")
# Example 2: Asynchronous job execution
started = client.execute_as_job(diagram_id, "start processing")
job_result = (
started['result']
if started['job_id'] is None
else client.poll_job_status(started['job_id'])
)
print(f"Job Result: {parse_response(job_result)}")
# Example 3: Training workflow (optional)
training_node_id = os.getenv('SYNTHREO_TRAINING_NODE_ID')
if training_node_id:
client.trigger_training(diagram_id, training_node_id)
client.monitor_training(diagram_id)
except Exception as e:
print(f"Error: {e}")
const axios = require('axios');
class SynthreoClient {
constructor(keyId, keySecret, targetApp = 'builder') {
this.clientId = `apikey-${keyId}`;
this.keySecret = keySecret;
this.targetApp = targetApp;
this.token = null;
// Access tokens live 15 minutes with no refresh token, so track expiry.
this.tokenExpiry = 0;
this.baseUrl = 'https://builder-api.synthreo.ai';
this.authUrl = 'https://auth.synthreo.ai/connect/token';
}
async authenticate() {
const body = new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.keySecret,
target_app: this.targetApp
});
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
try {
const response = await axios.post(this.authUrl, body, { headers });
this.token = response.data.access_token;
// Renew a minute early to absorb clock skew and request latency.
this.tokenExpiry = Date.now() + (response.data.expires_in * 1000) - 60000;
console.log('Authentication successful');
return this.token;
} catch (error) {
console.error('Authentication failed:', error.response?.data || error.message);
throw error;
}
}
// Re-exchange the API key when the cached token is missing or about to expire.
async ensureToken() {
if (!this.token || Date.now() >= this.tokenExpiry) {
await this.authenticate();
}
}
getHeaders() {
return {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
'Accept': '*/*'
};
}
async executeDiagram(diagramId, message, conversationId = null) {
const url = `${this.baseUrl}/CognitiveDiagram/${diagramId}/Execute`;
const userSaysData = { userSays: message };
if (conversationId) {
userSaysData.conversationId = conversationId;
}
const payload = {
Action: "Execute",
UserSays: JSON.stringify([userSaysData])
};
await this.ensureToken();
const headers = {
...this.getHeaders(),
'Accept': 'application/json',
'Origin': 'https://builder.synthreo.ai'
};
try {
const response = await axios.post(url, payload, { headers });
return response.data;
} catch (error) {
console.error('Diagram execution failed:', error.response?.data || error.message);
throw error;
}
}
async executeAsJob(diagramId, message = "start") {
const url = `${this.baseUrl}/CognitiveDiagram/${diagramId}/ExecuteAsJob`;
const payload = {
Action: "Execute",
UserSays: `[{"userSays":"${message}"}]`,
RobotSays: "",
CallerSource: 1
};
await this.ensureToken();
const headers = this.getHeaders();
try {
const response = await axios.post(url, payload, { headers });
// 202 carries a job to poll; 200 means it already finished and the body IS
// the result, with no job id. Handle both or quick jobs throw here.
if (response.status === 202 && response.data?.job?.id) {
const jobId = response.data.job.id;
console.log(`Job initiated with ID: ${jobId}`);
return { jobId, result: null };
}
console.log('Job finished immediately; no polling needed');
return { jobId: null, result: response.data };
} catch (error) {
console.error('Job initiation failed:', error.response?.data || error.message);
throw error;
}
}
async pollJobStatus(jobId, interval = 30000, timeout = 3600000) {
const url = `${this.baseUrl}/job/${jobId}`;
const startTime = Date.now();
return new Promise((resolve, reject) => {
const poll = async () => {
if (Date.now() - startTime > timeout) {
reject(new Error(`Job ${jobId} timed out after ${timeout/1000} seconds`));
return;
}
try {
// Refresh the token each poll so long jobs keep a valid 15-minute token.
await this.ensureToken();
// A failed job answers 400 with the reason in the body, so do not let
// axios turn it into a bare transport error.
const response = await axios.get(url, {
headers: this.getHeaders(),
validateStatus: (status) => [200, 202, 400].includes(status),
});
if (response.status === 202) {
console.log('Job is still running...');
setTimeout(poll, interval);
} else if (response.status === 400) {
reject(new Error(response.data?.error || 'Job failed'));
} else if (response.status === 200) {
// An empty body means the id is unknown or was already read.
if (response.data === '' || response.data === null) {
reject(new Error(`Job ${jobId} is unknown, already read, or expired`));
return;
}
console.log('Job completed!');
resolve(response.data);
} else {
reject(new Error(`Unexpected status code: ${response.status}`));
}
} catch (error) {
console.error('Error polling job status:', error.response?.data || error.message);
reject(error);
}
};
poll();
});
}
async triggerTraining(diagramId, nodeId, repositoryNodeId = 59) {
const url = `${this.baseUrl}/CognitiveDiagram/${diagramId}/TrainNode`;
const payload = {
nodeId: nodeId,
repositoryNodeId: repositoryNodeId,
finishedFlag: false,
logText: "Training started by API user"
};
await this.ensureToken();
const headers = this.getHeaders();
try {
const response = await axios.patch(url, payload, { headers });
console.log(`Training triggered for agent ${diagramId}`);
return response.data;
} catch (error) {
console.error('Training trigger failed:', error.response?.data || error.message);
throw error;
}
}
async monitorTraining(diagramId, interval = 60000, timeout = 3600000) {
const url = `${this.baseUrl}/CognitiveDiagram/${diagramId}`;
const startTime = Date.now();
return new Promise((resolve, reject) => {
const monitor = async () => {
if (Date.now() - startTime > timeout) {
reject(new Error(`Training timed out after ${timeout/1000} seconds`));
return;
}
try {
// Refresh the token each cycle so long monitors keep a valid token.
await this.ensureToken();
const response = await axios.get(url, { headers: this.getHeaders() });
const stateId = response.data.stateId;
if (stateId === 6) {
console.log('Agent is training...');
setTimeout(monitor, interval);
} else if (stateId === 2) {
console.log('Training completed! Agent is ready.');
resolve(response.data);
} else {
console.log(`Unexpected state ID: ${stateId}`);
resolve(response.data);
}
} catch (error) {
console.error('Error monitoring training:', error.response?.data || error.message);
reject(error);
}
};
monitor();
});
}
}
function parseResponse(apiResponse) {
if (apiResponse.outputData) {
try {
const outputData = JSON.parse(apiResponse.outputData);
if (Array.isArray(outputData) && outputData.length > 0) {
return String(outputData[0]);
} else if (typeof outputData === 'object' && outputData !== null) {
// Try common response field names
const commonFields = ['response', 'gpt_response', 'answer', 'result'];
for (const field of commonFields) {
if (outputData[field]) {
return String(outputData[field]);
}
}
return JSON.stringify(outputData, null, 2);
} else {
return String(outputData);
}
} catch (e) {
return apiResponse.outputData;
}
}
if (apiResponse.errorData && apiResponse.errorData !== "[]") {
try {
const errors = JSON.parse(apiResponse.errorData);
return `Error: ${errors[0]?.message || 'Unknown error'}`;
} catch (e) {
return `Error: ${apiResponse.errorData}`;
}
}
return "No response generated";
}
// Usage Example
async function main() {
const client = new SynthreoClient(
process.env.SYNTHREO_API_KEY_ID,
process.env.SYNTHREO_API_KEY_SECRET
);
const diagramId = parseInt(process.env.SYNTHREO_DIAGRAM_ID);
try {
// Authenticate
await client.authenticate();
// Example 1: Synchronous execution
const response = await client.executeDiagram(diagramId, "Hello, how are you?");
const aiResponse = parseResponse(response);
console.log(`AI Response: ${aiResponse}`);
// Example 2: Asynchronous job execution
const started = await client.executeAsJob(diagramId, "start processing");
const jobResult = started.jobId
? await client.pollJobStatus(started.jobId)
: started.result;
console.log(`Job Result: ${parseResponse(jobResult)}`);
// Example 3: Training workflow (optional)
const trainingNodeId = process.env.SYNTHREO_TRAINING_NODE_ID;
if (trainingNodeId) {
await client.triggerTraining(diagramId, trainingNodeId);
await client.monitorTraining(diagramId);
}
} catch (error) {
console.error('Error:', error.message);
}
}
// Run if this file is executed directly
if (require.main === module) {
main();
}
module.exports = { SynthreoClient, parseResponse };
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SynthreoClient {
private static final String BASE_URL = "https://builder-api.synthreo.ai";
private static final String AUTH_URL = "https://auth.synthreo.ai/connect/token";
private final String clientId;
private final String keySecret;
private final String targetApp;
private String token;
// Access tokens live 15 minutes with no refresh token; track expiry to re-exchange.
private long tokenExpiryMillis = 0;
private final ObjectMapper objectMapper;
public SynthreoClient(String keyId, String keySecret) {
this(keyId, keySecret, "builder");
}
public SynthreoClient(String keyId, String keySecret, String targetApp) {
this.clientId = "apikey-" + keyId;
this.keySecret = keySecret;
this.targetApp = targetApp;
this.objectMapper = new ObjectMapper();
}
public String authenticate() throws Exception {
String formBody = String.format(
"grant_type=client_credentials&client_id=%s&client_secret=%s&target_app=%s",
URLEncoder.encode(clientId, StandardCharsets.UTF_8),
URLEncoder.encode(keySecret, StandardCharsets.UTF_8),
URLEncoder.encode(targetApp, StandardCharsets.UTF_8)
);
HttpURLConnection connection = createConnection(AUTH_URL, "POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
try (OutputStream os = connection.getOutputStream()) {
os.write(formBody.getBytes(StandardCharsets.UTF_8));
}
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
String response = readResponse(connection);
JsonNode jsonResponse = objectMapper.readTree(response);
this.token = jsonResponse.get("access_token").asText();
long expiresIn = jsonResponse.has("expires_in") ? jsonResponse.get("expires_in").asLong() : 900;
// Renew a minute early to absorb clock skew and request latency.
this.tokenExpiryMillis = System.currentTimeMillis() + (expiresIn * 1000) - 60000;
System.out.println("Authentication successful");
return this.token;
} else {
throw new RuntimeException("Authentication failed with code: " + responseCode);
}
}
// Re-exchange the API key when the cached token is missing or about to expire.
private void ensureToken() throws Exception {
if (this.token == null || System.currentTimeMillis() >= this.tokenExpiryMillis) {
authenticate();
}
}
public String executeDiagram(int diagramId, String message) throws Exception {
return executeDiagram(diagramId, message, null);
}
public String executeDiagram(int diagramId, String message, String conversationId) throws Exception {
String url = BASE_URL + "/CognitiveDiagram/" + diagramId + "/Execute";
String userSaysData = conversationId != null
? String.format("{\"userSays\":\"%s\",\"conversationId\":\"%s\"}", message, conversationId)
: String.format("{\"userSays\":\"%s\"}", message);
String jsonPayload = String.format(
"{\"Action\":\"Execute\",\"UserSays\":\"[%s]\"}",
userSaysData.replace("\"", "\\\"")
);
HttpURLConnection connection = createConnection(url, "POST");
addAuthHeaders(connection);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("Origin", "https://builder.synthreo.ai");
try (OutputStream os = connection.getOutputStream()) {
os.write(jsonPayload.getBytes(StandardCharsets.UTF_8));
}
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
return readResponse(connection);
} else {
throw new RuntimeException("Diagram execution failed with code: " + responseCode);
}
}
/** Outcome of starting a job: either a pollable jobId, or an inline result. */
public record JobStart(String jobId, String result) {}
public JobStart executeAsJob(int diagramId, String message) throws Exception {
String url = BASE_URL + "/CognitiveDiagram/" + diagramId + "/ExecuteAsJob";
String jsonPayload = String.format(
"{\"Action\":\"Execute\",\"UserSays\":\"[{\\\"userSays\\\":\\\"%s\\\"}]\",\"RobotSays\":\"\",\"CallerSource\":1}",
message
);
HttpURLConnection connection = createConnection(url, "POST");
addAuthHeaders(connection);
try (OutputStream os = connection.getOutputStream()) {
os.write(jsonPayload.getBytes(StandardCharsets.UTF_8));
}
// 202 carries a job to poll; 200 means the diagram already finished and the
// body IS the result, with no job id. Both are returned to the caller so a
// quick job's result is never thrown away.
int responseCode = connection.getResponseCode();
String response = readResponse(connection);
if (responseCode == 202) {
JsonNode job = objectMapper.readTree(response).get("job");
if (job == null || !job.hasNonNull("id")) {
throw new RuntimeException("202 response carried no job id: " + response);
}
String jobId = job.get("id").asText();
System.out.println("Job initiated with ID: " + jobId);
return new JobStart(jobId, null);
} else if (responseCode == 200) {
System.out.println("Job finished immediately; no polling needed");
return new JobStart(null, response);
} else {
throw new RuntimeException("Job initiation failed with code: " + responseCode);
}
}
public String pollJobStatus(String jobId, int intervalSeconds, int timeoutSeconds) throws Exception {
String url = BASE_URL + "/job/" + jobId;
long startTime = System.currentTimeMillis();
long timeoutMs = timeoutSeconds * 1000L;
while (System.currentTimeMillis() - startTime < timeoutMs) {
long remainingMs = timeoutMs - (System.currentTimeMillis() - startTime);
HttpURLConnection connection = createConnection(url, "GET");
addAuthHeaders(connection);
// Bound the request so a stalled connection cannot outlive the deadline.
connection.setConnectTimeout((int) Math.min(10_000L, Math.max(1_000L, remainingMs)));
connection.setReadTimeout((int) Math.min(30_000L, Math.max(1_000L, remainingMs)));
int responseCode = connection.getResponseCode();
if (responseCode == 202) {
System.out.println("Job is still running...");
// Never sleep past the deadline.
long remainingSeconds = Math.max(0L,
(timeoutMs - (System.currentTimeMillis() - startTime)) / 1000L);
TimeUnit.SECONDS.sleep(Math.min((long) intervalSeconds, remainingSeconds));
continue;
} else if (responseCode == 200) {
String body = readResponse(connection);
// An empty body means the id is unknown or was already read, not success.
if (body == null || body.isBlank()) {
throw new RuntimeException("Job " + jobId + " is unknown, already read, or expired");
}
System.out.println("Job completed!");
return body;
} else if (responseCode == 400) {
// The job ran and failed; the error is in the body, which for an error
// status has to be read from the error stream.
String body = readErrorResponse(connection);
throw new RuntimeException("Job failed: " + body);
} else {
throw new RuntimeException("Unexpected response code: " + responseCode);
}
}
throw new RuntimeException("Job polling timed out after " + timeoutSeconds + " seconds");
}
public String triggerTraining(int diagramId, String nodeId, int repositoryNodeId) throws Exception {
String url = BASE_URL + "/CognitiveDiagram/" + diagramId + "/TrainNode";
String jsonPayload = String.format(
"{\"nodeId\":\"%s\",\"repositoryNodeId\":%d,\"finishedFlag\":false,\"logText\":\"Training started by API user\"}",
nodeId, repositoryNodeId
);
HttpURLConnection connection = createConnection(url, "PATCH");
addAuthHeaders(connection);
try (OutputStream os = connection.getOutputStream()) {
os.write(jsonPayload.getBytes(StandardCharsets.UTF_8));
}
int responseCode = connection.getResponseCode();
if (responseCode == 200 || responseCode == 204) {
System.out.println("Training triggered for agent " + diagramId);
return responseCode == 200 ? readResponse(connection) : "Training initiated";
} else {
throw new RuntimeException("Training trigger failed with code: " + responseCode);
}
}
public String monitorTraining(int diagramId, int intervalSeconds, int timeoutSeconds) throws Exception {
String url = BASE_URL + "/CognitiveDiagram/" + diagramId;
long startTime = System.currentTimeMillis();
long timeoutMs = timeoutSeconds * 1000L;
while (System.currentTimeMillis() - startTime < timeoutMs) {
HttpURLConnection connection = createConnection(url, "GET");
addAuthHeaders(connection);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
String response = readResponse(connection);
JsonNode jsonResponse = objectMapper.readTree(response);
int stateId = jsonResponse.get("stateId").asInt();
if (stateId == 6) {
System.out.println("Agent is training...");
TimeUnit.SECONDS.sleep(intervalSeconds);
} else if (stateId == 2) {
System.out.println("Training completed! Agent is ready.");
return response;
} else {
System.out.println("Unexpected state ID: " + stateId);
return response;
}
} else {
throw new RuntimeException("Training monitoring failed with code: " + responseCode);
}
}
throw new RuntimeException("Training monitoring timed out after " + timeoutSeconds + " seconds");
}
public static String parseResponse(String apiResponse) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonResponse = mapper.readTree(apiResponse);
JsonNode outputData = jsonResponse.get("outputData");
if (outputData != null && !outputData.isNull()) {
String outputDataStr = outputData.asText();
try {
JsonNode parsedOutput = mapper.readTree(outputDataStr);
if (parsedOutput.isArray() && parsedOutput.size() > 0) {
return parsedOutput.get(0).asText();
} else if (parsedOutput.isObject()) {
// Try common response field names
String[] fields = {"response", "gpt_response", "answer", "result"};
for (String field : fields) {
if (parsedOutput.has(field)) {
return parsedOutput.get(field).asText();
}
}
return parsedOutput.toString();
} else {
return parsedOutput.asText();
}
} catch (Exception e) {
return outputDataStr;
}
}
JsonNode errorData = jsonResponse.get("errorData");
if (errorData != null && !errorData.isNull() && !errorData.asText().equals("[]")) {
try {
JsonNode errors = mapper.readTree(errorData.asText());
if (errors.isArray() && errors.size() > 0) {
return "Error: " + errors.get(0).get("message").asText();
}
} catch (Exception e) {
return "Error: " + errorData.asText();
}
}
return "No response generated";
} catch (Exception e) {
return "Failed to parse response: " + e.getMessage();
}
}
private HttpURLConnection createConnection(String urlString, String method) throws Exception {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(method);
connection.setDoOutput(true);
return connection;
}
private void addAuthHeaders(HttpURLConnection connection) throws Exception {
ensureToken();
connection.setRequestProperty("Authorization", "Bearer " + token);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "*/*");
}
private String readResponse(HttpURLConnection connection) throws Exception {
return readStream(connection.getInputStream());
}
// A 4xx body arrives on the error stream, not the input stream - a failed job's
// reason is only readable through here.
private String readErrorResponse(HttpURLConnection connection) throws Exception {
java.io.InputStream stream = connection.getErrorStream();
return stream == null ? "" : readStream(stream);
}
private String readStream(java.io.InputStream stream) throws Exception {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(stream, StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
}
}
// Usage Example
public static void main(String[] args) {
try {
// Initialize client with environment variables
String keyId = System.getenv("SYNTHREO_API_KEY_ID");
String keySecret = System.getenv("SYNTHREO_API_KEY_SECRET");
int diagramId = Integer.parseInt(System.getenv("SYNTHREO_DIAGRAM_ID"));
SynthreoClient client = new SynthreoClient(keyId, keySecret);
// Authenticate
client.authenticate();
// Example 1: Synchronous execution
String response = client.executeDiagram(diagramId, "Hello, how are you?");
String aiResponse = parseResponse(response);
System.out.println("AI Response: " + aiResponse);
// Example 2: Asynchronous job execution
// A null job id means the diagram finished inline; use that result directly.
SynthreoClient.JobStart started = client.executeAsJob(diagramId, "start processing");
String jobResult = started.jobId() == null
? started.result()
: client.pollJobStatus(started.jobId(), 30, 3600);
System.out.println("Job Result: " + parseResponse(jobResult));
// Example 3: Training workflow (optional)
String trainingNodeId = System.getenv("SYNTHREO_TRAINING_NODE_ID");
if (trainingNodeId != null && !trainingNodeId.isEmpty()) {
client.triggerTraining(diagramId, trainingNodeId, 59);
client.monitorTraining(diagramId, 60, 3600);
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
}
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Threading;
public class SynthreoClient
{
private readonly string _clientId;
private readonly string _keySecret;
private readonly string _targetApp;
private string _token;
// Access tokens live 15 minutes with no refresh token; track expiry to re-exchange.
private DateTimeOffset _tokenExpiry = DateTimeOffset.MinValue;
private readonly HttpClient _httpClient;
private const string BaseUrl = "https://builder-api.synthreo.ai";
private const string AuthUrl = "https://auth.synthreo.ai/connect/token";
public SynthreoClient(string keyId, string keySecret, string targetApp = "builder")
{
_clientId = $"apikey-{keyId}";
_keySecret = keySecret;
_targetApp = targetApp;
_httpClient = new HttpClient();
}
public async Task<string> AuthenticateAsync()
{
using var request = new HttpRequestMessage(HttpMethod.Post, AuthUrl)
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = _clientId,
["client_secret"] = _keySecret,
["target_app"] = _targetApp
})
};
try
{
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseBody);
_token = doc.RootElement.GetProperty("access_token").GetString();
var expiresIn = doc.RootElement.TryGetProperty("expires_in", out var e) ? e.GetInt32() : 900;
// Renew a minute early to absorb clock skew and request latency.
_tokenExpiry = DateTimeOffset.UtcNow.AddSeconds(expiresIn - 60);
Console.WriteLine("Authentication successful");
return _token;
}
catch (HttpRequestException ex)
{
throw new Exception($"Authentication failed: {ex.Message}");
}
}
// Re-exchange the API key when the cached token is missing or about to expire, then set headers.
private async Task EnsureAuthHeadersAsync()
{
if (string.IsNullOrEmpty(_token) || DateTimeOffset.UtcNow >= _tokenExpiry)
{
await AuthenticateAsync();
}
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _token);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("*/*"));
}
public async Task<string> ExecuteDiagramAsync(int diagramId, string message,
string conversationId = null)
{
await EnsureAuthHeadersAsync();
var url = $"{BaseUrl}/CognitiveDiagram/{diagramId}/Execute";
var userSaysData = new { userSays = message };
var userSaysJson = conversationId != null
? JsonSerializer.Serialize(new { userSays = message, conversationId })
: JsonSerializer.Serialize(userSaysData);
var payload = new
{
Action = "Execute",
UserSays = $"[{userSaysJson}]"
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Add specific headers for diagram execution
content.Headers.Clear();
content.Headers.Add("Content-Type", "application/json");
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = content
};
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Add("Origin", "https://builder.synthreo.ai");
try
{
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
throw new Exception($"Diagram execution failed: {ex.Message}");
}
}
public async Task<(string JobId, string Result)> ExecuteAsJobAsync(int diagramId,
string message = "start")
{
await EnsureAuthHeadersAsync();
var url = $"{BaseUrl}/CognitiveDiagram/{diagramId}/ExecuteAsJob";
var payload = new
{
Action = "Execute",
UserSays = $"[{{\"userSays\":\"{message}\"}}]",
RobotSays = "",
CallerSource = 1
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
try
{
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseBody);
// 202 carries a job to poll; 200 means the diagram already finished and the
// body IS the result, with no job id. Both are returned so a quick job's
// result is never thrown away.
if (response.StatusCode == System.Net.HttpStatusCode.Accepted)
{
if (!doc.RootElement.TryGetProperty("job", out var job) ||
job.ValueKind != JsonValueKind.Object ||
!job.TryGetProperty("id", out var id))
{
throw new Exception($"202 response carried no job id: {responseBody}");
}
var jobId = id.GetString();
Console.WriteLine($"Job initiated with ID: {jobId}");
return (jobId, null);
}
Console.WriteLine("Job finished immediately; no polling needed");
return (null, responseBody);
}
catch (HttpRequestException ex)
{
throw new Exception($"Job initiation failed: {ex.Message}");
}
}
public async Task<string> PollJobStatusAsync(string jobId, int intervalSeconds = 30,
int timeoutSeconds = 3600)
{
var url = $"{BaseUrl}/job/{jobId}";
var startTime = DateTime.UtcNow;
var timeout = TimeSpan.FromSeconds(timeoutSeconds);
while (DateTime.UtcNow - startTime < timeout)
{
try
{
// Refresh the token each poll so long jobs keep a valid 15-minute token.
await EnsureAuthHeadersAsync();
var response = await _httpClient.GetAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.Accepted)
{
Console.WriteLine("Job is still running...");
await Task.Delay(TimeSpan.FromSeconds(intervalSeconds));
continue;
}
else if (response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
// An empty body means the id is unknown or was already read - the job
// is gone, which is not the same as success.
if (string.IsNullOrWhiteSpace(body))
{
throw new Exception($"Job {jobId} is unknown, already read, or expired");
}
Console.WriteLine("Job completed!");
return body;
}
else if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
// The job ran and failed. Read the reason instead of calling
// EnsureSuccessStatusCode, which would discard it.
var body = await response.Content.ReadAsStringAsync();
throw new Exception($"Job failed: {body}");
}
else
{
throw new Exception($"Unexpected status code: {response.StatusCode}");
}
}
catch (HttpRequestException ex)
{
throw new Exception($"Error polling job status: {ex.Message}");
}
}
throw new TimeoutException($"Job {jobId} timed out after {timeoutSeconds} seconds");
}
public async Task<string> TriggerTrainingAsync(int diagramId, string nodeId,
int repositoryNodeId = 59)
{
await EnsureAuthHeadersAsync();
var url = $"{BaseUrl}/CognitiveDiagram/{diagramId}/TrainNode";
var payload = new
{
nodeId = nodeId,
repositoryNodeId = repositoryNodeId,
finishedFlag = false,
logText = "Training started by API user"
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
try
{
var response = await _httpClient.PatchAsync(url, content);
response.EnsureSuccessStatusCode();
Console.WriteLine($"Training triggered for agent {diagramId}");
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
throw new Exception($"Training trigger failed: {ex.Message}");
}
}
public async Task<string> MonitorTrainingAsync(int diagramId, int intervalSeconds = 60,
int timeoutSeconds = 3600)
{
var url = $"{BaseUrl}/CognitiveDiagram/{diagramId}";
var startTime = DateTime.UtcNow;
var timeout = TimeSpan.FromSeconds(timeoutSeconds);
while (DateTime.UtcNow - startTime < timeout)
{
try
{
// Refresh the token each cycle so long monitors keep a valid token.
await EnsureAuthHeadersAsync();
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseBody);
var stateId = doc.RootElement.GetProperty("stateId").GetInt32();
if (stateId == 6)
{
Console.WriteLine("Agent is training...");
await Task.Delay(TimeSpan.FromSeconds(intervalSeconds));
}
else if (stateId == 2)
{
Console.WriteLine("Training completed! Agent is ready.");
return responseBody;
}
else
{
Console.WriteLine($"Unexpected state ID: {stateId}");
return responseBody;
}
}
catch (HttpRequestException ex)
{
throw new Exception($"Error monitoring training: {ex.Message}");
}
}
throw new TimeoutException($"Training monitoring timed out after {timeoutSeconds} seconds");
}
public static string ParseResponse(string apiResponse)
{
try
{
using var doc = JsonDocument.Parse(apiResponse);
var root = doc.RootElement;
if (root.TryGetProperty("outputData", out var outputDataElement) &&
!outputDataElement.ValueEquals(""))
{
var outputDataStr = outputDataElement.GetString();
try
{
using var outputDoc = JsonDocument.Parse(outputDataStr);
var outputRoot = outputDoc.RootElement;
if (outputRoot.ValueKind == JsonValueKind.Array && outputRoot.GetArrayLength() > 0)
{
return outputRoot[0].ToString();
}
else if (outputRoot.ValueKind == JsonValueKind.Object)
{
// Try common response field names
var fields = new[] { "response", "gpt_response", "answer", "result" };
foreach (var field in fields)
{
if (outputRoot.TryGetProperty(field, out var fieldElement))
{
return fieldElement.GetString();
}
}
return outputRoot.ToString();
}
else
{
return outputRoot.ToString();
}
}
catch
{
return outputDataStr;
}
}
if (root.TryGetProperty("errorData", out var errorDataElement) &&
!errorDataElement.ValueEquals("[]"))
{
var errorDataStr = errorDataElement.GetString();
try
{
using var errorDoc = JsonDocument.Parse(errorDataStr);
var errorRoot = errorDoc.RootElement;
if (errorRoot.ValueKind == JsonValueKind.Array && errorRoot.GetArrayLength() > 0)
{
var firstError = errorRoot[0];
if (firstError.TryGetProperty("message", out var messageElement))
{
return $"Error: {messageElement.GetString()}";
}
}
}
catch
{
return $"Error: {errorDataStr}";
}
}
return "No response generated";
}
catch (Exception ex)
{
return $"Failed to parse response: {ex.Message}";
}
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
// Usage Example
public class Program
{
public static async Task Main(string[] args)
{
try
{
// Initialize client with environment variables
var keyId = Environment.GetEnvironmentVariable("SYNTHREO_API_KEY_ID");
var keySecret = Environment.GetEnvironmentVariable("SYNTHREO_API_KEY_SECRET");
var diagramId = int.Parse(Environment.GetEnvironmentVariable("SYNTHREO_DIAGRAM_ID"));
using var client = new SynthreoClient(keyId, keySecret);
// Authenticate
await client.AuthenticateAsync();
// Example 1: Synchronous execution
var response = await client.ExecuteDiagramAsync(diagramId, "Hello, how are you?");
var aiResponse = SynthreoClient.ParseResponse(response);
Console.WriteLine($"AI Response: {aiResponse}");
// Example 2: Asynchronous job execution
// A null job id means the diagram finished inline; use that result directly.
var started = await client.ExecuteAsJobAsync(diagramId, "start processing");
var jobResult = started.JobId == null
? started.Result
: await client.PollJobStatusAsync(started.JobId);
Console.WriteLine($"Job Result: {SynthreoClient.ParseResponse(jobResult)}");
// Example 3: Training workflow (optional)
var trainingNodeId = Environment.GetEnvironmentVariable("SYNTHREO_TRAINING_NODE_ID");
if (!string.IsNullOrEmpty(trainingNodeId))
{
await client.TriggerTrainingAsync(diagramId, trainingNodeId);
await client.MonitorTrainingAsync(diagramId);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
from synthreo_client import SynthreoClient
import os
client = SynthreoClient(
os.getenv('SYNTHREO_API_KEY_ID'),
os.getenv('SYNTHREO_API_KEY_SECRET')
)
response = client.execute_diagram(12345, "Hello AI!")
print(parse_response(response))
const client = new SynthreoClient(
process.env.SYNTHREO_API_KEY_ID,
process.env.SYNTHREO_API_KEY_SECRET
);
async function quickStart() {
await client.authenticate();
const response = await client.executeDiagram(12345, "Hello AI!");
console.log(parseResponse(response));
}
quickStart();
started = client.execute_as_job(12345, "process data")
# A quick diagram finishes inline and has no job id to poll.
result = (
started['result']
if started['job_id'] is None
else client.poll_job_status(started['job_id'], interval=30, timeout=3600)
)
print(f"Final result: {parse_response(result)}")
// Start job and wait for completion
const started = await client.executeAsJob(12345, "process data");
// A quick diagram finishes inline and has no job id to poll.
const result = started.jobId
? await client.pollJobStatus(started.jobId, 30000, 3600000)
: started.result;
console.log(`Final result: ${parseResponse(result)}`);
try:
response = client.execute_diagram(diagram_id, message)
result = parse_response(response)
if result.startswith("Error:"):
print(f"AI Agent Error: {result}")
else:
print(f"Success: {result}")
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e}")
except requests.exceptions.RequestException as e:
print(f"Request Error: {e}")
except Exception as e:
print(f"Unexpected Error: {e}")
try {
const response = await client.executeDiagram(diagramId, message);
const result = parseResponse(response);
if (result.startsWith("Error:")) {
console.error(`AI Agent Error: ${result}`);
} else {
console.log(`Success: ${result}`);
}
} catch (error) {
if (error.response) {
console.error(`HTTP Error ${error.response.status}: ${error.response.data}`);
} else if (error.request) {
console.error('Network Error: No response received');
} else {
console.error(`Error: ${error.message}`);
}
}
requests>=2.25.0
python-dotenv>=0.19.0
{
"dependencies": {
"axios": "^1.6.0",
"dotenv": "^16.3.0"
}
}
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
<PackageReference Include="System.Text.Json" Version="7.0.0" />
  1. Always authenticate before making API calls
  2. Use environment variables for sensitive data
  3. Implement proper error handling and timeouts
  4. Choose appropriate polling intervals based on expected job duration
  5. Parse responses carefully and handle different output formats
  6. Log important events for debugging and monitoring
  7. Implement retry logic for transient failures