Skip to main content
SDK

Java SDK

Install and use the Qoder Cloud Agents Java SDK, including builders, synchronous and asynchronous calls, streaming responses, resource management, retries, and timeouts.

The Qoder Cloud Agents Java SDK provides typed synchronous and asynchronous clients, uses builders for configuration and parameters, and consistently handles authentication, pagination, retries, timeouts, and SSE streaming responses.

Installation

Requires Java 8 or later.

Maven

Add the following to pom.xml:
<dependency>
  <groupId>com.qoder.sdk</groupId>
  <artifactId>qoder-cloud-agents-sdk</artifactId>
  <version>0.2.0</version>
</dependency>

Gradle

Use Maven Central and add a dependency with a pinned version:
repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.qoder.sdk:qoder-cloud-agents-sdk:0.2.0'
}

Basic usage

Save your personal access token (PAT) in an environment variable:
export QODER_PAT="replace-with-your-PAT"
Use the Forward client to create a template and identity, start a session, and send a message:
import com.qoder.cloudagents.ForwardClient;
import com.qoder.cloudagents.forward.*;
import java.util.Collections;

public class QuickStart {
    public static void main(String[] args) {
        try (ForwardClient client = ForwardClient.builder().build()) {
            Environment env = client.environments().create(
                    EnvironmentCreateParams.builder()
                            .name("demo")
                            .config(EnvironmentConfig.builder()
                                    .type("cloud")
                                    .build())
                            .build());

            Identity identity = client.identities().create(
                    IdentityCreateParams.builder()
                            .externalId("demo-user")
                            .name("Demo User")
                            .build());

            Template template = client.templates().create(
                    TemplateCreateParams.builder()
                            .name("assistant")
                            .modelId("auto")
                            .environmentId(env.id())
                            .build());

            Session session = client.sessions().create(
                    SessionCreateParams.builder()
                            .identityId(identity.id())
                            .templateId(template.id())
                            .build());

            SessionEventParam message = SessionEventParam.builder()
                    .type("user.message")
                    .content(EventContent.text("Hello"))
                    .build();

            client.sessions().events().send(
                    session.id(),
                    SessionEventSendParams.builder()
                            .events(Collections.singletonList(message))
                            .build());

            System.out.println(session.id());
        }
    }
}
Use the Managed client to create an Agent and environment, start a session, and send a message. Save the following code as ManagedQuickStart.java and run it in a project with the SDK dependency added:
import com.qoder.cloudagents.ManagedClient;
import com.qoder.cloudagents.managed.*;
import java.util.Collections;

public class ManagedQuickStart {
    public static void main(String[] args) {
        try (ManagedClient client = ManagedClient.builder().build()) {
            Agent agent = client.agents().create(
                    AgentCreateParams.builder()
                            .name("assistant")
                            .model("auto")
                            .build());

            Environment env = client.environments().create(
                    EnvironmentCreateParams.builder()
                            .name("demo")
                            .config(EnvironmentConfig.builder()
                                    .type("cloud")
                                    .build())
                            .build());

            Session session = client.sessions().create(
                    SessionCreateParams.builder()
                            .agent(agent.id())
                            .environmentId(env.id())
                            .build());

            SessionEventParam message = SessionEventParam.builder()
                    .type("user.message")
                    .content(EventContent.text("Hello"))
                    .build();

            client.sessions().events().send(
                    session.id(),
                    SessionEventSendParams.builder()
                            .events(Collections.singletonList(message))
                            .build());

            System.out.println(session.id());
        }
    }
}
Clients can be reused across threads. Close the resource-owning client when the application has finished using it; short programs can use try-with-resources for automatic closure. See the Managed quickstart and Forward API reference for more integration instructions.

Client configuration

ConfigurationForwardManaged
Synchronous clientForwardClientManagedClient
Asynchronous clientAsyncForwardClientAsyncManagedClient
Base URL environment variableQODER_FORWARD_BASE_URLQODER_BASE_URL
Global default URLhttps://api.qoder.com/api/v1/forwardhttps://api.qoder.com/api/v1/cloud
CN URLhttps://api.qoder.com.cn/api/v1/forwardhttps://api.qoder.com.cn/api/v1/cloud
Explicit builder configuration takes precedence over environment variables. Default PAT mode reads QODER_ACCESS_TOKEN first, then QODER_PAT. Clients do not automatically load .env files. Choose a full Base URL matching the region of the account, credentials, and resources.
import com.qoder.cloudagents.ForwardClient;
import java.time.Duration;

try (ForwardClient client = ForwardClient.builder()
        .baseUrl("https://api.qoder.com.cn/api/v1/forward")
        .timeout(Duration.ofSeconds(30))
        .maxRetries(2)
        .defaultHeader("X-Application", "my-app")
        .build()) {
    System.out.println(client.models().list().data().size());
}
Use withOptions(...) to create a client copy with different settings. The original client keeps its configuration:
import com.qoder.cloudagents.ForwardClient;
import java.time.Duration;

try (ForwardClient slowClient = client.withOptions(options ->
        options.timeout(Duration.ofSeconds(60)).maxRetries(1))) {
    System.out.println(slowClient.models().list().data().size());
}
Close the copy independently. Explicitly injected Transport and Credential objects remain owned by the caller; release these shared resources after all users have finished.

Authentication

PAT

Pass a personal access token explicitly with .pat(token), or configure it through environment variables. Store credentials in the application server environment.

Service Account

After setting QODER_SA_KEY, explicitly create a credential component. It exchanges and caches SATs (service account tokens), refreshing them when subsequent requests require it:
import com.qoder.cloudagents.ForwardClient;
import com.qoder.cloudagents.auth.Region;
import com.qoder.cloudagents.auth.ServiceAccountCredential;

try (ServiceAccountCredential credential = ServiceAccountCredential.builder()
        .saKey(System.getenv("QODER_SA_KEY"))
        .region(Region.GLOBAL)
        .scopes("qca.access", "forward.access")
        .build();
     ForwardClient client = ForwardClient.builder()
        .credential(credential)
        .baseUrl("https://api.qoder.com/api/v1/forward")
        .build()) {
    System.out.println(client.models().list().data().size());
}
ManagedClient accepts the same credential component. Multiple clients can share a credential instance; the example closes the client before closing the credential. Closing a client does not close externally supplied credentials for the application. For the China site, use both Region.CN and a CN Base URL. For Managed-only access, you can request the qca.access scope; permissions and workspace bindings must be configured on the server. Ordinary clients do not automatically enable token exchange merely because QODER_SA_KEY exists in the environment. An SA Key cannot serve directly as a business API access token. SA authentication failures do not fall back to PAT. If the application manages SATs itself, use .authMode(AuthMode.SA).accessToken(token), where AuthMode is in com.qoder.cloudagents.auth. The application must refresh static SATs. Configure only one explicit credential source per client. See the authentication documentation.

Synchronous and asynchronous calls

Synchronous methods return results directly. client.async() provides an asynchronous view sharing authentication and transport resources; ordinary asynchronous requests return CompletableFuture<T>:
import com.qoder.cloudagents.forward.Session;
import java.util.concurrent.CompletableFuture;

CompletableFuture<Session> pending = client.async().sessions().retrieve("sess_example");
pending.thenAccept(result -> System.out.println(result.id())).join();
You can also create an independent asynchronous client:
import com.qoder.cloudagents.AsyncForwardClient;
import com.qoder.cloudagents.ForwardClient;

try (AsyncForwardClient asyncClient = ForwardClient.builder()
        .pat(System.getenv("QODER_PAT"))
        .buildAsync()) {
    asyncClient.models().list()
        .thenAccept(result -> System.out.println(result.data().size()))
        .join();
}
The example uses join() to make a short program wait for request completion. Server-side asynchronous pipelines can compose Futures; call join() or get() only on threads that need to wait for results. Asynchronous failures are usually wrapped in CompletionException or ExecutionException; inspect the underlying SDK exception with getCause().
ObjectClose and cancellation behavior
View returned by client.async()Shares resources with the parent client; closing the view does not close the parent
Client returned by buildAsync()Owns resources independently; close it after use
Future returned by the SDKcancel(true) cancels the corresponding HTTP call and retry waits
Derived Future created by the application with thenApply or similar methodsFollows Java Future behavior; cancel the original SDK Future to stop the underlying request
Keep the resource-owning client open until requests complete. Interrupting a synchronous call cancels the request, preserves the thread interrupt flag, and throws ApiUserAbortException.

Request and response types

Request parameters are constructed through builders and become immutable afterward. Unset fields and fields explicitly set to null are encoded differently:
import com.qoder.cloudagents.forward.SessionUpdateParams;

SessionUpdateParams omitted = SessionUpdateParams.builder().build();
SessionUpdateParams explicitNull = SessionUpdateParams.builder().title(null).build();
SessionUpdateParams emptyString = SessionUpdateParams.builder().title("").build();
SessionUpdateParams omittedAgain = SessionUpdateParams.builder()
    .title("example")
    .omit("title")
    .build();
Builder behaviorJSON behavior
Do not call the field setterOmit the field
Set a field to nullSend null
Set an empty string or collectionSend "", [], or {}
omit("field JSON name")Remove a previously set field
Whether a field accepts null and how that value is handled are defined by the corresponding API. For parameters accepting multiple JSON representations, use public overloads or wrapper types rather than relying on internal serialization. Responses use immutable models with methods for reading fields. field(name) and rawJson() preserve access to the original JSON, and unknown fields are retained. Generic JSON values use com.qoder.cloudagents.core.JsonValue; applications do not need to depend on internal Jackson types.

Streaming responses

Consume a single SSE connection through EventStream<T> and release it with try-with-resources:
import com.qoder.cloudagents.forward.SessionEvent;
import com.qoder.cloudagents.forward.SessionEventStreamParams;
import com.qoder.cloudagents.streaming.EventStream;

try (EventStream<SessionEvent> stream = client.sessions().events().streamEvents(
        "sess_example", SessionEventStreamParams.builder().build())) {
    for (SessionEvent event : stream) {
        System.out.println(event.type());
    }
}
streamEvents ends at EOF and reports read or protocol errors to the caller. To keep subscribing after a recoverable disconnection, explicitly use resumableStream:
import com.qoder.cloudagents.core.RequestOptions;
import com.qoder.cloudagents.forward.SessionEvent;
import com.qoder.cloudagents.forward.SessionEventStreamParams;
import com.qoder.cloudagents.streaming.EventStream;
import java.time.Duration;

try (EventStream<SessionEvent> stream = client.sessions().events().resumableStream(
        "sess_example",
        SessionEventStreamParams.builder().lastEventId("evt_checkpoint").build(),
        RequestOptions.builder().totalTimeout(Duration.ofMinutes(5)).build())) {
    for (SessionEvent event : stream) {
        System.out.println(event.type());
        // Persist this resume position after the application successfully processes the event.
        System.out.println("cursor: " + stream.lastEventId());
    }
}
Replace evt_checkpoint with a valid resume position; omit lastEventId for an initial subscription. A resumable stream connects when consumption begins, then uses the SSE cursor of the most recently delivered event. Calling only the iterator hasNext() does not advance the delivery cursor. Without a cursor, the stream starts at the current event stream tail; reading history requires a valid historical resume position. Resumption does not resend business requests or guarantee exactly-once processing; multiple deltas for one event may share an ID. maxRetries controls retries for each HTTP connection attempt; totalTimeout controls the entire subscription, including reconnection waits and reads. Evaluate stream termination and business task completion separately; see the API definitions for event meanings.

Asynchronous subscriptions

Asynchronous streamEvents(...) returns a Future for establishing the connection; the resulting EventStream still uses blocking reads. To read in the background and process callbacks sequentially, use subscribe:
import com.qoder.cloudagents.core.RequestOptions;
import com.qoder.cloudagents.forward.SessionEvent;
import com.qoder.cloudagents.forward.SessionEventStreamParams;
import com.qoder.cloudagents.streaming.EventStream;
import com.qoder.cloudagents.streaming.EventSubscription;
import java.time.Duration;

try (EventStream<SessionEvent> stream = client.async().sessions().events().resumableStream(
        "sess_example", SessionEventStreamParams.builder().build(),
        RequestOptions.builder().totalTimeout(Duration.ofMinutes(5)).build());
     EventSubscription subscription = stream.subscribe(event ->
        System.out.println(event.type()))) {
    subscription.completion().join();
}
By default, subscribe uses its own reader thread, and it also accepts an application-provided Executor. Reads and callbacks execute in order; callbacks should return promptly. Do not schedule blocking reads on an application event-loop thread. Closing the stream, closing the subscription, or cancelling its completion() ends reading and reconnection. These actions stop only the local subscription; they do not automatically stop execution already started in the cloud.

Error handling and Request ID

Ordinary request failures inherit from ApiException. HTTP errors provide status codes and request identifiers through ApiStatusException:
import com.qoder.cloudagents.errors.ApiException;
import com.qoder.cloudagents.errors.ApiStatusException;
import com.qoder.cloudagents.errors.ApiTimeoutException;
import com.qoder.cloudagents.errors.ApiUserAbortException;

try {
    System.out.println(client.sessions().retrieve("sess_example").requestId());
} catch (ApiUserAbortException error) {
    System.err.println("Request canceled");
} catch (ApiTimeoutException error) {
    System.err.println("Request timed out");
} catch (ApiStatusException error) {
    System.err.println("HTTP " + error.statusCode() + ", request ID: " + error.requestId());
} catch (ApiException error) {
    System.err.println("Request failed: " + error.getClass().getSimpleName());
}
Connection failures use ApiConnectionException, with ApiTimeoutException as a subclass. SSE protocol or error frames may use StreamException; SA credential exchange failures use ServiceAccountExchangeException. Do not assume every failure has a business API HTTP status code. Normal response models provide requestId(). Streams expose the current connection status code, response headers, and Request ID through response(). For a lazy resumable stream that has not connected yet, response() may be null.

Retries and idempotency

By default, requests are retried up to 2 times, for a maximum of 3 attempts. The SDK checks the request method, idempotency key, and body replayability before deciding whether to retry:
RequestRetried by default for
GET, HEAD, or requests with an idempotency keyConnection errors, timeouts, 408, 429, 5xx
Other requests without an idempotency key429
409 responseNo automatic retries
Non-replayable request bodyNo automatic retries
The SDK handles server retry hints and delays within these rules; some APIs impose stricter limits. Change the count through the client builder or per-call RequestOptions.builder().maxRetries(...).build(). Set it to 0 to disable retries. For write requests, configure the idempotency key with RequestOptions.builder().idempotencyKey(key). Reuse the same key for all retries of one logical operation; use a new key for a new operation.

Timeouts and cancellation

The default connection timeout is 5 seconds, and response header wait and read/write idle timeouts are 10 minutes. Use RequestOptions to distinguish per-attempt limits from a total deadline:
import com.qoder.cloudagents.core.RequestOptions;
import java.time.Duration;

client.sessions().retrieve("sess_example", RequestOptions.builder()
    .timeout(Duration.ofSeconds(15))
    .totalTimeout(Duration.ofSeconds(60))
    .maxRetries(1)
    .build());
timeout adjusts the response header wait and read/write idle limits for each attempt. totalTimeout covers credential acquisition, all attempts, retry waits, and response body reads. By default, there is no total deadline for the entire call. After SSE response headers are accepted, the ordinary read-idle timer is not enabled. Set totalTimeout to limit subscription duration. Reaching a deadline or cancelling a local request does not mean that execution already started in the cloud is cancelled.

Pagination

List responses support current-page access and on-demand iteration across pages:
import com.qoder.cloudagents.forward.Session;
import com.qoder.cloudagents.forward.SessionListParams;
import com.qoder.cloudagents.forward.SessionListResponse;

SessionListResponse page = client.sessions().list(
    SessionListParams.builder().limit(20).build());
System.out.println(page.data().size());

for (Session item : page) {
    System.out.println(item.id());
}
Read page.data() to process only the current page. For manual pagination, check hasNextPage() before calling getNextPage(); use getNextPageAsync() for asynchronous pagination. Pagination preserves query conditions and request configuration, so you do not need to construct cursor parameters manually.

Advanced configuration

Raw response information

Use a resource withResponse() to obtain the parsed result, HTTP status, and response headers together:
import com.qoder.cloudagents.core.ApiResponse;
import com.qoder.cloudagents.forward.Session;

ApiResponse<Session> response = client.sessions().withResponse().retrieve("sess_example");
System.out.println(response.statusCode());
System.out.println(response.requestId());
System.out.println(response.data().id());
JSON data in ApiResponse is already parsed. Do not treat it as an unconsumed raw HTTP response body.

HTTP interceptors and Transport

Interceptors can observe each HTTP attempt, including retries and SSE reconnections:
import com.qoder.cloudagents.ForwardClient;

try (ForwardClient observed = client.withOptions(options ->
        options.interceptor((request, chain) -> {
            long started = System.nanoTime();
            return chain.proceed(request).whenComplete((response, error) -> {
                long elapsedMs = (System.nanoTime() - started) / 1_000_000;
                System.out.println("http_elapsed_ms=" + elapsedMs);
            });
        }))) {
    observed.models().list();
}
Interceptors should return promptly and call chain.proceed(...) at most once per invocation. Completion indicates response header receipt, not completion of SSE or response body consumption. Do not read streaming bodies early just to log them. To configure your own HTTP library, proxy, TLS, or connection pool, implement the public HttpTransport interface and inject it through .transport(...). Transport handles a single transmission and cancellation; do not enable its own automatic retries on top of SDK retries. The application owns explicitly injected Transport instances and manages their lifecycle. For individual calls, set additional request parameters with header(...) and query(...) on RequestOptions.builder(), then call build(). JSON fields already supported by the server but not yet modeled by the SDK can be supplied through public JSON model interfaces. Prefer typed methods for ordinary calls.