# Java Social Media API SDK

The official Java SDK. Java 11+, built on `java.net.http.HttpClient`, single
dependency (Jackson), Jackson `JsonNode` responses with a fluent params builder.

## Install

Maven:

```xml
<dependency>
  <groupId>com.omnisocials</groupId>
  <artifactId>omnisocials-java</artifactId>
  <version>0.1.0</version>
</dependency>
```

Gradle:

```groovy
implementation "com.omnisocials:omnisocials-java:0.1.0"
```

## Authentication

```java
OmniSocials client = OmniSocials.builder()
    .apiKey("omsk_live_...")
    .build();
```

Or `OmniSocials.fromEnv()` (reads `OMNISOCIALS_API_KEY`). A missing key throws
`AuthenticationException`.

## Create a post

```java
JsonNode res = client.posts().create(Params.builder()
    .put("content", "New drop this Friday")
    .put("channels", List.of("instagram", "facebook", "linkedin"))
    .put("scheduled_at", "2026-08-01T09:00:00Z")
    .put("media_urls", List.of("https://example.com/teaser.jpg"))
    .build());
JsonNode post = res.get("data");
System.out.println(post.get("id").asText() + " " + post.get("status").asText());
```

Any `media_urls` (or `media_ids`) entry can also be a map carrying per-media
alt text (max 1500 chars), delivered to Mastodon, Bluesky, X (photos/GIFs), and
Pinterest:

```java
.put("media_urls", List.of(Params.of(
    "url", "https://example.com/teaser.jpg",
    "alt", "Red sneaker on a white background")))
```

Publish immediately:

```java
client.posts().createAndPublish(Params.builder()
    .put("content", "Going live right now")
    .put("channels", List.of("x", "bluesky"))
    .build());
```

## Upload media

```java
JsonNode upload = client.media().uploadFromUrl(Params.builder()
    .put("url", "https://example.com/launch-video.mp4")
    .put("name", "launch-video-v2")
    .put("folder", "Campaigns")
    .build());
System.out.println(upload.get("data").get("id").asText());
System.out.println(upload.get("compatibility"));
```

Local files via multipart:

```java
import java.nio.file.Path;

// From a path (filename and content type detected from the file)
JsonNode res = client.media().upload(Path.of("photos/product.jpg"), Params.of("name", "product-hero"));

// Or from bytes + filename
byte[] bytes = java.nio.file.Files.readAllBytes(Path.of("photos/product.jpg"));
client.media().upload(bytes, "product.jpg");
```

## Analytics

```java
// One post's latest per-platform metrics
JsonNode stats = client.analytics().post("post_id");
System.out.println(stats.get("data").get("platforms").path("instagram").path("metrics"));

// Batch: up to 100 posts in one call
JsonNode batch = client.analytics().posts(List.of("id1", "id2", "id3"));
// or: client.analytics().posts("id1", "id2", "id3");

// Workspace-wide overview
JsonNode overview = client.analytics().overview(Params.of("period", "30d"));
System.out.println(overview.get("data").get("total_impressions").asLong());

// Account-level stats (followers etc)
JsonNode accountStats = client.analytics().accounts(Params.of("platform", "instagram"));
```

## Verify webhooks

Spring Boot:

```java
import com.fasterxml.jackson.databind.JsonNode;
import com.omnisocials.Webhooks;
import com.omnisocials.errors.WebhookVerificationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OmniSocialsWebhookController {

  private final String secret = System.getenv("OMNISOCIALS_WEBHOOK_SECRET");

  @PostMapping("/omnisocials/webhook")
  public ResponseEntity<Void> handle(
      @RequestBody String rawBody, // Spring gives you the raw body as a String
      @RequestHeader("X-OmniSocials-Signature") String signature) {
    JsonNode event;
    try {
      event = Webhooks.verifySignature(rawBody, signature, secret, 300);
    } catch (WebhookVerificationException e) {
      return ResponseEntity.badRequest().build();
    }

    switch (event.get("type").asText()) {
      case "post.published":
        System.out.println("Published: " + event.get("data").get("post_id").asText());
        break;
      case "post.failed":
        System.err.println("Failed: " + event.get("data").get("post_id").asText());
        break;
      default:
        break;
    }
    return ResponseEntity.ok().build();
  }
}
```

## Errors

All exceptions extend `OmniSocialsException` (unchecked) and live in
`com.omnisocials.errors`. `ApiException` exposes `getStatus()`, `getCode()`,
`getMessage()`, and `getBody()`. Types: `ValidationException` (400/422),
`AuthenticationException` (401), `PermissionDeniedException` (403),
`NotFoundException` (404), `RateLimitException` (429, `getRetryAfter()`),
`ServerException` (5xx), `ApiConnectionException`, `WebhookVerificationException`.

```java
import com.omnisocials.errors.*;

try {
  client.posts().create(Params.of("content", "Hi", "channels", List.of("instagram")));
} catch (RateLimitException e) {
  System.out.println("Rate limited, retry in " + e.getRetryAfter() + "s");
} catch (ValidationException e) {
  System.err.println("Bad request (" + e.getCode() + "): " + e.getMessage());
  System.err.println(e.getBody());
} catch (ApiConnectionException e) {
  System.err.println("Network problem: " + e.getMessage());
} catch (ApiException e) {
  System.err.println("API error " + e.getStatus() + " (" + e.getCode() + "): " + e.getMessage());
}
```

## Configuration

```java
import java.time.Duration;

OmniSocials client = OmniSocials.builder()
    .apiKey("omsk_live_...")
    .baseUrl("https://api.omnisocials.com/v1") // default
    .timeout(Duration.ofSeconds(30))           // per-request timeout (default 30s)
    .maxRetries(2)                             // retries on 429 / 5xx / network errors (default 2)
    .build();
```

## Notes

- Params via `Params.builder().put(...).build()` or `Params.of(k, v, ...)`; unlike `Map.of`, null values are allowed and serialize to JSON `null`
- Responses are Jackson `JsonNode`
- Webhook verification uses `MessageDigest.isEqual` (constant time)
- Source: [github.com/OmniSocials/omnisocials-java](https://github.com/OmniSocials/omnisocials-java)
