Skip to content

Java SDK

Zero-dependency Java SDK for the Synapse API. Published as tech.pyrx:synapse on Maven Central. Uses only java.net.http.HttpClient (no external dependencies).

Requires Java 11+.


Installation

Maven

xml
<dependency>
<groupId>tech.pyrx</groupId>
<artifactId>synapse</artifactId>
<version>0.1.0</version>
</dependency>

Gradle

groovy
implementation 'tech.pyrx:synapse:0.1.0'

Quick Start

java
import tech.pyrx.synapse.*;
import tech.pyrx.synapse.model.*;
 
import java.util.Map;
 
public class Main {
public static void main(String[] args) {
SynapseClient client = new SynapseClient(
new SynapseConfig()
.apiKey("psk_live_your_api_key")
.workspaceId("your_workspace_id")
);
 
// Track an event
TrackResponse result = client.track(
TrackParams.builder()
.externalId("user_123")
.eventName("purchase_completed")
.attributes(Map.of(
"order_id", "ord_456",
"amount", 99.99,
"currency", "USD"
))
.build()
);
System.out.println(result.getEventId()); // "evt_8f14e45f-..."
 
// Identify a contact
ContactResponse contact = client.identify(
IdentifyParams.builder()
.externalId("user_123")
.email("[email protected]")
.firstName("Jane")
.lastName("Doe")
.properties(Map.of("plan", "pro", "signup_source", "website"))
.tags(java.util.List.of("paying", "beta-tester"))
.build()
);
System.out.println(contact.getEmail()); // "[email protected]"
 
// Send a transactional email
SendResponse send = client.send(
SendParams.builder()
.templateSlug("order-confirmation")
.to(Map.of(
"user_id", "user_123",
"email", "[email protected]",
"first_name", "Jane"
))
.attributes(Map.of("order_id", "ord_456"))
.build()
);
System.out.println(send.getStatus()); // "sent"
}
}
Tip

Get your API key and workspace ID from the dashboard at Settings > API Keys.


Configuration

java
SynapseClient client = new SynapseClient(
new SynapseConfig()
.apiKey("psk_live_xxx") // Required. API key from workspace settings.
.workspaceId("ws_xxx") // Required. Your workspace ID.
.baseUrl("https://...") // Default: https://synapse-api.pyrx.tech
.timeoutSeconds(30) // Default: 30
.maxRetries(3) // Default: 3. Set to 0 to disable retries.
);
ParameterTypeDefaultDescription
apiKeyStringrequiredYour Synapse API key (psk_live_* or psk_test_*)
workspaceIdStringrequiredYour workspace identifier
baseUrlStringhttps://synapse-api.pyrx.techAPI base URL
timeoutSecondsint30Request timeout in seconds
maxRetriesint3Retry count for 429/5xx errors. Set to 0 to disable.

Environment detection: The SDK detects test or live from your API key prefix (psk_test_* vs psk_live_*), available via client.getEnvironment().

Retry behavior: The SDK automatically retries on 429, 500, 502, 503, and 504 with exponential backoff and jitter (capped at 30s). On 429, uses the Retry-After header when present. Network errors (ConnectException, HttpTimeoutException) are also retried. Client errors (400, 401, 403, 404, 422) are never retried.

AutoCloseable: SynapseClient implements AutoCloseable, so you can use it with try-with-resources.


Track Events

Single Event

java
TrackResponse result = client.track(
TrackParams.builder()
.externalId("user_123")
.eventName("purchase_completed")
.attributes(Map.of(
"order_id", "ord_456",
"amount", 99.99,
"currency", "USD"
))
.contact(Map.of(
"email", "[email protected]",
"first_name", "Jane"
))
.idempotencyKey("purchase_ord_456") // optional, prevents duplicate processing
.occurredAt("2026-04-29T10:30:00Z") // optional, defaults to now
.build()
);
 
System.out.println(result.getEventId()); // "evt_8f14e45f-..."
System.out.println(result.getStatus()); // "accepted"
ParameterTypeRequiredDescription
externalIdStringYesYour unique user identifier
eventNameStringYesEvent name (e.g., purchase_completed)
attributesMapNoArbitrary key-value event data
contactMapNoContact fields to upsert alongside the event
idempotencyKeyStringNoPrevents duplicate processing (7-day TTL)
occurredAtStringNoISO 8601 timestamp. Defaults to server time.

Batch Events

Track up to 50 events in a single request.

java
BatchTrackResponse result = client.trackBatch(
new TrackBatchParams(java.util.List.of(
TrackParams.builder()
.externalId("user_1").eventName("page_view")
.attributes(Map.of("page", "/pricing")).build(),
TrackParams.builder()
.externalId("user_2").eventName("page_view")
.attributes(Map.of("page", "/docs")).build(),
TrackParams.builder()
.externalId("user_1").eventName("button_clicked")
.attributes(Map.of("button", "upgrade")).build()
))
);
 
System.out.println(result.getAccepted()); // 3
System.out.println(result.getRejected()); // 0

Identify Contacts

Single Contact

Create or update (upsert) a contact by externalId.

java
ContactResponse contact = client.identify(
IdentifyParams.builder()
.externalId("user_123")
.email("[email protected]")
.firstName("Jane")
.lastName("Doe")
.phone("+1234567890")
.timezone("America/New_York")
.locale("en-US")
.properties(Map.of("plan", "pro", "signup_source", "website"))
.tags(java.util.List.of("paying", "beta-tester"))
.build()
);
 
System.out.println(contact.getId()); // UUID
System.out.println(contact.getExternalId()); // "user_123"
System.out.println(contact.getEmail()); // "[email protected]"

Batch Identify

Upsert up to 1,000 contacts in a single request.

java
BulkContactResponse result = client.identifyBatch(
new IdentifyBatchParams(
java.util.List.of(
IdentifyParams.builder()
.externalId("user_1").email("[email protected]").firstName("Alice").build(),
IdentifyParams.builder()
.externalId("user_2").email("[email protected]").firstName("Bob").build()
),
"merge" // "merge" | "skip" | "replace"
)
);
 
System.out.println(result.getTotal()); // 2
System.out.println(result.getCreated()); // 1
System.out.println(result.getUpdated()); // 1

Send Transactional Email

Send a one-off email using an NLT template, without a flow.

java
SendResponse result = client.send(
SendParams.builder()
.templateSlug("otp-verification")
.to(Map.of(
"user_id", "user_123",
"email", "[email protected]",
"first_name", "Jane"
))
.attributes(Map.of(
"otp_code", "847293",
"expiry_minutes", 10
))
.idempotencyKey("otp_user_123_" + System.currentTimeMillis() / 1000)
.build()
);
 
System.out.println(result.getStatus()); // "sent" or "suppressed"
System.out.println(result.getEmailLogId()); // "el_8f14e45f-..."
Note

Requires a data-scoped API key (or higher). The template must exist in your workspace.


Contact Management

The client.contacts sub-client provides full CRUD operations. Requires a management or full scoped API key.

List Contacts

java
ContactListResponse result = client.contacts.list(
ContactListParams.builder()
.search("jane")
.page(1)
.perPage(25)
.sortBy("created_at")
.sortOrder("desc")
.build()
);
 
System.out.println(result.getMeta().getTotal()); // 142
System.out.println(result.getMeta().getTotalPages()); // 6
 
for (ContactResponse c : result.getData()) {
System.out.println(c.getEmail() + " " + c.getFirstName());
}

Get a Contact

java
ContactResponse contact = client.contacts.get("contact_uuid");

Update a Contact

java
ContactResponse updated = client.contacts.update("user_123",
ContactUpdateParams.builder()
.email("[email protected]")
.addTags(java.util.List.of("vip"))
.removeTags(java.util.List.of("trial"))
.build()
);

Delete a Contact

java
client.contacts.delete("user_123");

Template Management

The client.templates sub-client manages email templates. Requires a management or full scoped API key.

List Templates

java
java.util.List<TemplateResponse> templates = client.templates.list();

Get a Template

java
TemplateResponse template = client.templates.get("welcome-email");

Create a Template

java
TemplateResponse created = client.templates.create(
TemplateCreateParams.builder()
.name("Welcome Email")
.slug("welcome-email")
.subject("Welcome, [first name of contact]!")
.bodyHtml("<h1>Welcome!</h1><p>Thanks for joining.</p>")
.senderName("PYRX Team")
.fromEmail("[email protected]")
.build()
);

Update a Template

java
TemplateResponse updated = client.templates.update("welcome-email",
TemplateUpdateParams.builder()
.subject("Welcome aboard, [first name of contact]!")
.build()
);

Preview with Sample Data

java
TemplatePreviewResponse preview = client.templates.preview("welcome-email",
TemplatePreviewParams.builder()
.contact(Map.of("first_name", "Jane", "email", "[email protected]"))
.triggerEvent(Map.of("order_id", "ord_123"))
.build()
);
 
System.out.println(preview.getSubject()); // Rendered subject
System.out.println(preview.getHtml()); // Rendered HTML
System.out.println(preview.isSuppressed()); // false
System.out.println(preview.getSuppressedReason()); // null

Delete a Template

java
client.templates.delete("old-template");

Webhook Verification

Verify incoming webhook signatures to ensure requests are authentically from Synapse. This is a static method on the Webhooks class -- no client instance needed.

java
import tech.pyrx.synapse.Webhooks;
 
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
 
// In your webhook endpoint handler:
String payload = request.getReader().lines()
.collect(java.util.stream.Collectors.joining());
 
Map<String, String> headers = Map.of(
"svix-id", request.getHeader("svix-id"),
"svix-timestamp", request.getHeader("svix-timestamp"),
"svix-signature", request.getHeader("svix-signature")
);
 
String secret = System.getenv("SYNAPSE_WEBHOOK_SECRET"); // e.g. "whsec_..."
 
try {
Map<String, Object> event = Webhooks.verify(payload, headers, secret);
System.out.println(event.get("type")); // e.g. "email.delivered"
} catch (IllegalArgumentException e) {
// Invalid signature, expired timestamp, or missing headers
System.out.println("Webhook rejected: " + e.getMessage());
}

The verification checks:

  • All three svix-* headers are present
  • The timestamp is within 5 minutes (replay attack protection)
  • The HMAC-SHA256 signature matches (supports multiple signatures for key rotation)

You can pass true as the fourth argument to Webhooks.verify() to disable the timestamp check (useful for testing only).


Error Handling

The SDK provides typed exception classes for every failure mode. All errors extend SynapseError (an unchecked RuntimeException).

java
import tech.pyrx.synapse.errors.*;
 
try {
client.track(
TrackParams.builder()
.externalId("u1")
.eventName("test")
.build()
);
} catch (SynapsePlanLimitError e) {
System.out.printf("Plan limit: %s (%d/%d)%n",
e.getLimitType(), e.getCurrent(), e.getMaximum());
System.out.println("Current plan: " + e.getPlan());
} catch (SynapseRateLimitError e) {
System.out.printf("Rate limited. Retry after %.0fs%n", e.getRetryAfter());
} catch (SynapseValidationError e) {
for (var err : e.getErrors()) {
System.out.println(err.getField() + ": " + err.getMessage());
}
} catch (SynapseAuthError e) {
System.out.println("Authentication failed: " + e.getMessage());
} catch (SynapseError e) {
System.out.printf("API error %d: %s%n", e.getStatus(), e.getMessage());
}

Error Types

Error ClassHTTP StatusFieldsWhen
SynapseErrorAnygetStatus(), getMessage(), getCode(), getRequestId()Base type for all API errors
SynapseAuthError401, 403getMessage()Invalid or expired API key, scope mismatch
SynapseValidationError422getErrors() with getField() + getMessage()Request body validation failed
SynapseRateLimitError429getRetryAfter() (seconds)Rate limit exceeded (auto-retried)
SynapsePlanLimitError403getLimitType(), getCurrent(), getMaximum(), getPlan()Plan limit reached

Environment Variables

For production deployments, load credentials from environment variables.

java
import tech.pyrx.synapse.*;
 
SynapseClient client = new SynapseClient(
new SynapseConfig()
.apiKey(System.getenv("SYNAPSE_API_KEY"))
.workspaceId(System.getenv("SYNAPSE_WORKSPACE_ID"))
);
bash
export SYNAPSE_API_KEY=psk_live_a1b2c3d4e5f67890abcdef1234567890
export SYNAPSE_WORKSPACE_ID=your_workspace_id

Full Method Reference

MethodDescriptionRequired Scope
client.track(params)Track a single eventdata
client.trackBatch(params)Track up to 50 eventsdata
client.identify(params)Upsert a single contactdata
client.identifyBatch(params)Upsert up to 1,000 contactsdata
client.send(params)Send a transactional emaildata
client.contacts.list(params)List contacts with paginationmanagement
client.contacts.get(id)Get a single contactmanagement
client.contacts.update(id, params)Update a contactmanagement
client.contacts.delete(id)Delete a contactmanagement
client.templates.list()List all templatesmanagement
client.templates.get(slug)Get a template by slugmanagement
client.templates.create(params)Create a templatemanagement
client.templates.update(slug, params)Update a templatemanagement
client.templates.preview(slug, params)Preview rendered templatemanagement
client.templates.delete(slug)Delete a templatemanagement
Webhooks.verify(payload, headers, secret)Verify webhook signature--

Framework Examples

Spring Boot

java
// src/main/java/com/example/config/SynapseConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.pyrx.synapse.*;
 
@Configuration
public class SynapseConfig {
@Bean
public SynapseClient synapseClient() {
return new SynapseClient(
new tech.pyrx.synapse.SynapseConfig()
.apiKey(System.getenv("SYNAPSE_API_KEY"))
.workspaceId(System.getenv("SYNAPSE_WORKSPACE_ID"))
);
}
}
java
// src/main/java/com/example/controller/SignupController.java
import org.springframework.web.bind.annotation.*;
import tech.pyrx.synapse.*;
import tech.pyrx.synapse.model.*;
 
import java.util.Map;
 
@RestController
public class SignupController {
 
private final SynapseClient synapse;
 
public SignupController(SynapseClient synapse) {
this.synapse = synapse;
}
 
@PostMapping("/signup")
public Map<String, Boolean> signup(@RequestBody SignupRequest req) {
// Identify the new user
synapse.identify(
IdentifyParams.builder()
.externalId(req.getUserId())
.email(req.getEmail())
.properties(Map.of("plan", req.getPlan()))
.tags(java.util.List.of("new-signup"))
.build()
);
 
// Track the signup event (triggers flows)
synapse.track(
TrackParams.builder()
.externalId(req.getUserId())
.eventName("user_signed_up")
.attributes(Map.of("plan", req.getPlan(), "source", "web"))
.build()
);
 
return Map.of("success", true);
}
}

Webhook Endpoint (Spring Boot)

java
import org.springframework.web.bind.annotation.*;
import tech.pyrx.synapse.Webhooks;
 
import java.util.Map;
 
@RestController
public class WebhookController {
 
@PostMapping("/webhooks/synapse")
public void handleWebhook(
@RequestBody String payload,
@RequestHeader("svix-id") String svixId,
@RequestHeader("svix-timestamp") String svixTimestamp,
@RequestHeader("svix-signature") String svixSignature) {
 
Map<String, String> headers = Map.of(
"svix-id", svixId,
"svix-timestamp", svixTimestamp,
"svix-signature", svixSignature
);
 
Map<String, Object> event = Webhooks.verify(
payload, headers, System.getenv("SYNAPSE_WEBHOOK_SECRET")
);
 
System.out.println("Webhook received: " + event.get("type"));
}
}