> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.amniscient.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.amniscient.com/_mcp/server.

# Amniscient Detection SDK for Java

#### [Java SDK](https://central.sonatype.com/artifact/com.amniscient/amniscient-detection/overview)

Maven Repository

Java client for the Amniscient Inference API, over HTTPS or NATS (WebSocket). It gives you low-latency object detection behind one call, `client.detect(image, modelId)`.

Requires Java 8 or later, and is tested on 8, 11, 17 and 21. It has two dependencies, both Java 8 compatible: Jackson (JSON) and `jnats` (the NATS transport). HTTP is the JDK's built-in `HttpURLConnection`, and compression and annotation use the JDK's ImageIO and 2D graphics.

This SDK follows the same contract as the Python and TypeScript SDKs and passes the same conformance suite ([`sdk/contract`](../contract/README.md)). The calls, errors, retries and result shape are identical; only the language differs.

## Install

Maven:

```xml
<dependency>
    <groupId>com.amniscient</groupId>
    <artifactId>amniscient-detection</artifactId>
    <version>0.1.0</version>
</dependency>
```

Gradle:

```groovy
implementation "com.amniscient:amniscient-detection:0.1.0"
```

## Quick start

```java
import com.amniscient.detection.DetectionClient;
import com.amniscient.detection.model.Detection;
import com.amniscient.detection.model.DetectionResult;
import java.nio.file.Paths;

try (DetectionClient client = DetectionClient.builder()
        .endpoint("https://api.amniscient.com/inference")
        .apiKey("YOUR_KEY")
        .organizationId("YOUR_ORG")
        .build()) {

    DetectionResult result = client.detect(Paths.get("photo.jpg"), "MODEL_ID");

    System.out.println(result.detectionFound());
    for (Detection d : result.detections()) {
        // bbox is corner form, normalized 0-1: x1, y1, x2, y2
        System.out.printf("%s %.2f %s%n", d.classId(), d.confidence(), d.bbox());
    }
}
```

One client is thread-safe; share it across your application and close it on shutdown.

## How it works

1. **Auth is validated first:** a missing API key throws `AuthenticationException` before any image work or network call.
2. **The image is prepared:** checked (JPEG, PNG or WebP), then resized to fit 3840 px on the long side and JPEG-encoded at quality 70, unless you turn compression off.
3. **The request is sent** over HTTPS or NATS. A transient failure (network error, timeout, 5xx) is retried with exponential backoff.
4. **The reply becomes a `DetectionResult`,** or a typed exception is thrown (see [Errors](#errors)).

## Over NATS (WebSocket)

For lower latency and a persistent connection, use the NATS transport. The same API key works. The organization is resolved from the key when you don't pass it, and the server confines the connection to that organization.

```java
try (DetectionClient client = DetectionClient.builder()
        .endpoint("wss://nats-server.amniscient.com")
        .apiKey("YOUR_KEY")
        .transport(DetectionClient.TransportKind.NATS)
        .build()) {
    DetectionResult result = client.detect(Paths.get("photo.jpg"), "MODEL_ID");
}
```

* **One tenant per client.** NATS binds the key to the connection, so per-call `apiKey` / `organizationId` in `DetectOptions` throw `IllegalArgumentException`. Use one client per tenant, or HTTP for a multi-tenant proxy.
* **Same errors and retries as HTTP.** A rejected key is an `AuthenticationException`; timeouts and 5xx replies are retried.
* **`close()`** drains the connection. Use try-with-resources or close the client on shutdown.

## Usage

### Options per call

```java
import com.amniscient.detection.DetectOptions;

DetectionResult r = client.detect(Paths.get("shelf.jpg"), "MODEL_ID", DetectOptions.builder()
        .confidenceThreshold(0.5)   // default 0.25
        .iouThreshold(0.5)          // default 0.45
        .version(3)                 // pin a model version; default: the active one
        .detectBarcodes(true)       // read barcodes and QR codes too
        .requestId("order-123")     // correlation id; default: a new UUID
        .build());
```

To resolve the model from an inference point instead of a model id, set `.inferencePointId("…")` and pass `null` for the model id.

### Barcodes and QR codes

With `detectBarcodes(true)`, `result.barcodes()` lists the decoded codes. Object detection is unchanged.

```java
for (Barcode code : r.barcodes()) {
    System.out.println(code.type() + " " + code.value());   // e.g. upc-a 012345678905
}
```

* **Types:** `qr`, `upc-a`, `upc-e`, `code-39`.
* **When it's empty:** the list is empty when the option is off, no codes are found, or decoding fails. Decoding never fails the call.

### Filtering results

```java
DetectionResult confident = result.filterByConfidence(0.8);
DetectionResult colas = result.filterByClassId("cola", "diet-cola");
DetectionResult tagged = result.filterByTag("tag", "cola");
```

### Raw responses

`detectRaw(bytes, modelId, options)` returns the server's JSON as a `Map<String, Object>`, the `xywh` centre-box shape. Use it when you need fields the model doesn't cover. `result.raw()` gives you the same map.

### Multi-tenant / per-request credentials (HTTP)

Build one shared client with no stored credentials and pass them per call. They ride the request and nothing is kept:

```java
DetectionClient shared = DetectionClient.builder().endpoint("https://api.amniscient.com/inference").build();
DetectionResult r = shared.detect(image, "MODEL_ID",
        DetectOptions.builder().apiKey(tenantKey).organizationId(tenantOrg).build());
```

### Login gate, model picker, default model

```java
boolean ok = client.validate();               // true = key accepted; false = rejected
List<ModelInfo> models = client.listModels(); // id(), name(), status()
Map<String, Object> def = client.getDefaultModel(); // null when the org has none
```

`validate()` returns `false` **only** for a rejected key. A route that isn't deployed (`EndpointNotAvailableException`), a server error or a connection problem is thrown, never reported as `false`.

### Annotating detections

```java
Map<String, Object> payload = client.detectRaw(bytes, "MODEL_ID", DetectOptions.defaults());
AnnotationResult a = client.annotate(bytes, payload, AnnotateOptions.builder().label("id").build());
a.getBoxes();          // pixel-space, labelled boxes: draw them in any toolkit
byte[] png = a.toPng(); // or a ready-made annotated PNG
```

## Configuration

| Builder method    | Default                        | Description                                                                                                                                      |
| ----------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `endpoint`        | required                       | `https://api.amniscient.com/inference` (HTTP) or `wss://nats-server.amniscient.com` (NATS)                                                       |
| `apiKey`          | —                              | Your API key; over HTTP it can be given per call instead                                                                                         |
| `organizationId`  | —                              | Optional: the gateway sets it from the key over HTTP, and it's resolved from the key over NATS                                                   |
| `transport`       | `HTTP`                         | `TransportKind.HTTP` or `TransportKind.NATS`                                                                                                     |
| `timeout`         | 30 s                           | Per-request timeout                                                                                                                              |
| `maxRetries`      | 3                              | Retries after the first attempt for transient failures (network, timeout, 5xx). `0` means one attempt. Waits 0.25 s, 0.5 s, 1 s, … capped at 4 s |
| `compression`     | `true`                         | Resize and JPEG-encode before sending                                                                                                            |
| `highCompression` | `false`                        | Fit within 800x600 at a lower quality                                                                                                            |
| `jpegQuality`     | 70                             | JPEG quality, 1-100                                                                                                                              |
| `maxDimension`    | 3840                           | Longest-side cap in pixels                                                                                                                       |
| `detectPath`      | `/detect`                      | HTTP detect route; a `{model_id}` placeholder is substituted                                                                                     |
| `whoamiUrl`       | known for Amniscient endpoints | Gateway `/whoami`, used to resolve the org over NATS when `organizationId` isn't set                                                             |

Other environments: `https://api.stage.amniscient.com/uat/inference` + `wss://nats-server.uat.internal.amniscient.com`, `…/dev/…` + `wss://nats-server.dev.internal.amniscient.com`, `https://api.sandbox.amniscient.com/sandbox/inference` + `wss://nats-server.sandbox.internal.amniscient.com`. All of them are listed in `Contract.HTTP_ENDPOINTS` and `Contract.NATS_ENDPOINTS`.

## Errors

Every failure is an unchecked exception under `DetectionException`. Each carries `getStatusCode()` and `getResponseBody()` when a server answered.

| Exception                       | Meaning                                                                                               |
| ------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `AuthenticationException`       | Missing or rejected API key (401, or a 403 that isn't a missing route)                                |
| `EndpointNotAvailableException` | The route isn't deployed on this gateway/stage (403 `MISSING_AUTHENTICATION_TOKEN`); the key is valid |
| `ModelNotFoundException`        | The model doesn't exist (404)                                                                         |
| `ImageTooLargeException`        | The image is over the server's limit (413)                                                            |
| `RateLimitException`            | Too many requests (429)                                                                               |
| `ServerException`               | Server error (5xx), after retries for `detect`                                                        |
| `ConnectionException`           | Network failure or timeout, after retries for `detect`                                                |
| `InvalidImageException`         | The image is empty or not a JPEG, PNG or WebP                                                         |
| `NoDetectionsException`         | `annotate` was given a payload with no detections                                                     |
| `IllegalArgumentException`      | A bad argument, e.g. neither a model id nor an inference point                                        |

```java
try {
    client.detect(Paths.get("photo.jpg"), "MODEL_ID");
} catch (AuthenticationException e) {
    // fix the key
} catch (RateLimitException e) {
    // back off and retry later
} catch (DetectionException e) {
    // anything else; e.getStatusCode() / e.getResponseBody() for details
}
```

## Live harness

`HarnessRunner` drives the SDK end to end against a live endpoint. It runs `validate()`, `listModels()`, then `detect()` over one image or a folder, and prints a summary. Config comes from the environment, so no secrets go on the command line:

```bash
export AMNI_ENDPOINT="https://api.amniscient.com/inference"   # or wss://nats-server.amniscient.com
export AMNI_TRANSPORT="http"                                  # or nats
export AMNI_API_KEY="…"
export AMNI_ORG="…"
export AMNI_MODEL="…"
export AMNI_IMAGE="/path/to/image.jpg"    # a file, or a directory to batch
export AMNI_BARCODES="true"               # optional
export AMNI_ANNOTATE_OUT="annotated.png"  # optional: write an annotated PNG (single image)

mvn -q compile exec:java
```

## Build & test

Requires JDK 8+ and Maven.

```bash
mvn test                 # unit tests (canned local HTTP server, no network)
./conformance.sh         # the shared cross-SDK suite over HTTP and NATS (needs nats-server + python nats-py)
mvn package              # build the jar
```

`Contract.java` is generated from [`sdk/contract/contract.json`](../contract/contract.json); don't edit it by hand. Run `python3 sdk/contract/generate.py` instead.