> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Java SDK: Orbit quickstart for Java

> Official Orbit Java SDK quickstart — messaging, voice, contacts, campaigns, and OTP verify with typed methods.

# Java SDK

The Orbit Java SDK wraps the platform's core API resources — messaging (SMS, WhatsApp, email), voice, contacts, campaigns, verify (OTP), and webhook signature verification — with typed methods. It requires JDK 11+ and has zero external runtime dependencies.

<Note>
  **Pre-publish — source-only.** This SDK is not yet on Maven Central — the
  dependencies below describe the future registry shape. Until first
  publish, vendor the source from the monorepo (`packages/sdk-java/`) or
  call the [REST API](/api-reference) directly. See
  [Java: core-scope, not full parity](/sdks#java-core-scope-not-full-parity)
  on the SDK index for exactly what is and isn't wrapped, and the low-level
  `client.request(method, path, ...)` escape hatch for uncovered routes
  (worked example [below](#covered-route-missing-use-the-escape-hatch)).
</Note>

## Installation

No registry dependency block is live yet — nothing here resolves today.
Vendor the SDK source from the monorepo (`packages/sdk-java/`) and build it into
your project, or call the REST API directly with any HTTP client until the first
Maven Central release ships (coordinates change).

## Client initialization

```java theme={null}
import io.devotel.orbit.OrbitClient;

OrbitClient client = OrbitClient.fromApiKey("dv_live_sk_...");
```

Or read the key from an environment variable (`ORBIT_API_KEY`):

```java theme={null}
OrbitClient client = OrbitClient.fromEnv();
```

Tune timeouts and retries with the builder:

```java theme={null}
OrbitClient client = OrbitClient.builder()
        .apiKey("dv_live_sk_...")
        .baseUrl("https://api.orbit.devotel.io/api/v1")
        .timeout(Duration.ofSeconds(30))
        .maxRetries(3)
        .initialBackoff(Duration.ofSeconds(1))
        .build();
```

`OrbitClient` is safe for concurrent use across threads — share a single instance per JVM.

## Quickstart: send your first SMS

A runnable end-to-end — the key comes from `ORBIT_API_KEY`, never from
source. Copy it into `Main.java` and run it:

```java theme={null}
import io.devotel.orbit.OrbitClient;

import java.util.Map;

OrbitClient client = OrbitClient.fromEnv();  // reads ORBIT_API_KEY

Map<String, Object> result = client.messages.sendSms("+14155552671", "Hello from Orbit!");
Map<?, ?> data = (Map<?, ?>) result.get("data");

System.out.println("message id: " + data.get("id"));        // msg_abc123
System.out.println("status: " + data.get("status"));        // "queued" — watch it reach "delivered"
System.out.println("status URL: /api/v1/messages/" + data.get("id"));
```

Point `ORBIT_API_KEY` at a sandbox key prefixed `dv_test_sk_` first —
sandbox sends are simulated, free, and never reach a carrier. Swap in your
live key (`dv_live_sk_...`) when you're ready to send for real; the code
does not change.

The response shape above comes from the
[Messages API reference](/api-reference/endpoints/messaging) — its language
tabs include this exact call.

## Messaging

```java theme={null}
Map<String, Object> result = client.messages.sendSms("+14155552671", "Hello!");
System.out.println(result.get("data"));
```

Sending is only the first half — fetch a message by id to read its delivery
state (`queued` → `sent` → `delivered`), which is what a status poller or a
support lookup does:

```java theme={null}
Map<String, Object> message = client.messages.get("msg_abc123");
Map<?, ?> messageData = (Map<?, ?>) message.get("data");
System.out.println("status: " + messageData.get("status"));   // "delivered"
```

`client.messages` also exposes `sendWhatsApp` and `sendEmail`; `get` (above) fetches a message by id.

## Voice

A call's full lifecycle fits in one flow: place it, poll its status, then hang
up (or hand it off with a blind transfer). `getCall` returns the same call
record `createCall` did — read `status` off it as it moves (`queued` →
`ringing` → `in-progress` → `completed`):

```java theme={null}
// 1. Place an outbound call — routed through Orbit's own softswitch.
Map<String, Object> call = client.voice.createCall("+14155552671", "+14155550100");
String callId = (String) ((Map<?, ?>) call.get("data")).get("id");
System.out.println("call id: " + callId);       // call_abc123

// 2. Poll its status until it answers.
Map<String, Object> detail = client.voice.getCall(callId);
Map<?, ?> detailData = (Map<?, ?>) detail.get("data");
System.out.println("status: " + detailData.get("status"));   // "in-progress"

// 3a. Hang up.
client.voice.hangup(callId);

// 3b. …or transfer the live call to another destination (blind transfer).
client.voice.transfer(callId, "+14155552500");
```

List past calls with direction/status filters and cursor pagination — pass the
filter map straight through (`listCalls(null)` for no filters):

```java theme={null}
Map<String, Object> filter = new LinkedHashMap<>();
filter.put("direction", "outbound");
filter.put("status", "completed");
filter.put("limit", 50);

Map<String, Object> page = client.voice.listCalls(filter);
java.util.List<?> calls = (java.util.List<?>) page.get("data");
System.out.println("calls on page: " + calls.size());
```

Every outbound call is dispatched by the Orbit API itself — this SDK never
talks to a telephony carrier.

## Verify (OTP)

The send-and-check round trip is the most common first integration on the
platform — here as a complete flow. Send the code, keep the returned
verification id, and check the code the user typed in against it:

```java theme={null}
// 1. Send an OTP (channel: sms | whatsapp | email).
Map<String, Object> sent = client.verify.send("+14155552671", "sms");
String verificationId = (String) ((Map<?, ?>) sent.get("data")).get("id");

// 2. Check the code the user typed in, against the id send() returned.
Map<String, Object> result = client.verify.check(verificationId, "482901");
Map<?, ?> data = (Map<?, ?>) result.get("data");

System.out.println("valid: " + data.get("valid"));      // true
System.out.println("status: " + data.get("status"));    // "approved"
```

A wrongly-typed or expired code reports `valid: false` — it never throws
for a bad guess (only for transport/auth failures), so branch on the flag.
The [Verify API reference](/api-reference/endpoints/verify) covers the full
contract, and [Starter examples](/guides/starter-examples) ships a complete
OTP sign-in starter repo (`orbit-otp-nextjs`).

Resend the code for a still-pending verification — the same verification id
is reused, so no new state is created:

```java theme={null}
Map<String, Object> resent = client.verify.resend(verificationId);
Map<?, ?> resentData = (Map<?, ?>) resent.get("data");
System.out.println("status: " + resentData.get("status"));   // "pending" — new code on its way
```

For anything beyond send/check/resend — e.g. a verification detail view — use
the escape hatch (`client.request`) shown [below](#covered-route-missing-use-the-escape-hatch);
the verification detail endpoint is `GET /api/v1/verify/{id}/detail`.

## Contacts

The full contact lifecycle reads off the id `create` returned — fetch it,
patch it, tag it, page the directory, and delete it:

```java theme={null}
// Create, then keep the id for everything below.
Map<String, Object> contact = client.contacts.create("+14155552671");
String contactId = (String) ((Map<?, ?>) contact.get("data")).get("id");
System.out.println("contact id: " + contactId);      // ctn_abc123

// Fetch one contact.
Map<String, Object> detail = client.contacts.get(contactId);

// Partial update — the patch map carries only the fields you change.
Map<String, Object> patch = new LinkedHashMap<>();
patch.put("first_name", "Grace");
client.contacts.update(contactId, patch);

// Tag the contact.
client.contacts.addTags(contactId, java.util.List.of("vip"));

// Page through the directory with optional search (pass null for no filters).
Map<String, Object> query = new LinkedHashMap<>();
query.put("limit", 50);
query.put("search", "grace");
Map<String, Object> page = client.contacts.list(query);
java.util.List<?> contacts = (java.util.List<?>) page.get("data");
System.out.println("contacts on page: " + contacts.size());

// Permanently delete.
client.contacts.delete(contactId);
```

## Campaigns

Create a campaign, trigger its send, then re-fetch it to watch the status
progress (`draft` → `sending` → `completed`):

```java theme={null}
// Create and trigger a send.
Map<String, Object> campaign = client.campaigns.create("Welcome drip", "sms");
String campaignId = (String) ((Map<?, ?>) campaign.get("data")).get("id");
client.campaigns.send(campaignId);

Map<String, Object> state = client.campaigns.get(campaignId);
Map<?, ?> stateData = (Map<?, ?>) state.get("data");
System.out.println("status: " + stateData.get("status"));   // "sending"
```

Rename before the send fires, list every campaign in the organization, or
delete one outright — the update map carries only the fields you change:

```java theme={null}
Map<String, Object> patch = new LinkedHashMap<>();
patch.put("name", "Welcome drip — Q4");
client.campaigns.update(campaignId, patch);

Map<String, Object> all = client.campaigns.list();
java.util.List<?> campaigns = (java.util.List<?>) all.get("data");
System.out.println("campaigns: " + campaigns.size());

client.campaigns.delete(campaignId);
```

## Paginate a list

List endpoints are cursor-paginated — read `meta.pagination.cursor` and
`meta.pagination.has_more` off each response and pass the cursor back as a
query param until `has_more` is false. Page through SMS messages with the
escape hatch:

```java theme={null}
Map<String, Object> query = new LinkedHashMap<>();
query.put("channel", "sms");
query.put("limit", 100);
int total = 0;

while (true) {
    Map<String, Object> page = client.request("GET", "/messages", query, null, null);

    java.util.List<?> data = (java.util.List<?>) page.get("data");
    total += data.size();

    Map<?, ?> pagination = (Map<?, ?>) ((Map<?, ?>) page.get("meta")).get("pagination");
    if (!Boolean.TRUE.equals(pagination.get("has_more"))) {
        break;
    }
    query.put("cursor", pagination.get("cursor"));
}

System.out.println("Fetched " + total + " messages");
```

The full pagination model (cursor vs. offset endpoints, page-size caps, and
why cursors are not bookmarkable) is in the
[Pagination guide](/guides/pagination).

## Error handling

All Orbit-originated errors inherit from `OrbitError`:

| Exception                  | Raised when                                                |
| -------------------------- | ---------------------------------------------------------- |
| `OrbitAuthenticationError` | 401/403 — bad or missing-scope API key                     |
| `OrbitRateLimitError`      | 429 after retries exhausted; sleep `e.getRetryAfter()`     |
| `OrbitClientError`         | other 4xx (invalid request)                                |
| `OrbitServerError`         | 5xx after retries exhausted, or persistent network failure |
| `OrbitError`               | base class — catch to handle any Orbit-originated error    |

```java theme={null}
try {
    client.messages.sendSms("+14155552671", "...");
} catch (OrbitRateLimitError e) {
    Thread.sleep(e.getRetryAfter().toMillis());
} catch (OrbitAuthenticationError e) {
    // rotate API key
} catch (OrbitError e) {
    log.error("orbit: {} ({}) — {}", e.getCode(), e.getStatusCode(), e.getMessage());
}
```

Every non-GET request automatically carries an `Idempotency-Key` header (UUIDv4); pass your own stable key as the fourth argument to `sendSms` when retrying from your own queue (`client.messages.sendSms("+14155552671", "...", null, "job-7a3b9d-attempt-1")`).

## Covered route missing? Use the escape hatch

The typed clients wrap 8 core resources; the rest of the API — contact segments, event sinks, frequency caps, and everything else listed as out of scope on the [SDK index](/sdks#java-core-scope-not-full-parity) — is reachable through `client.request(method, path, ...)`. It returns the raw JSON body as a `Map<String, Object>`. Fetch a segment by id:

```java theme={null}
Map<String, Object> segment = client.request("GET", "/contacts/segments/seg_01hxyz", null, null, null);
System.out.println(((Map<?, ?>) segment.get("data")).get("name"));
```

The escape hatch carries the same auth, retry, and error model as the typed clients — treat it as a first-class client, not a fallback `HttpClient`.

## Webhook signature verification

```java theme={null}
import io.devotel.orbit.Webhooks;
import io.devotel.orbit.errors.OrbitWebhookSignatureError;

try {
    Map<String, Object> event = Webhooks.verify(
            requestBody,
            request.getHeader("X-Orbit-Signature"),
            System.getenv("ORBIT_WEBHOOK_SECRET"));
    // event is a Map<String, Object>
} catch (OrbitWebhookSignatureError e) {
    response.setStatus(400);
    return;
}
```
