Java SDK
The norq Java package provides a client for sending and linting notification templates from Java applications.
Install
Coming soon. The
norqpackage is not yet published to Maven Central. See norq.sh for updates.
The SDK runs entirely in-process — no norq binary or daemon is invoked at runtime.
Quick start
import sh.norq.Norq;
import sh.norq.Types.*;
import java.util.Map;
public class Example {
public static void main(String[] args) {
Norq norq = Norq.builder()
.projectDir("./my-project")
.build();
SendMultiResult result = norq.send("transactional/welcome",
new SendOpts()
.to(new Recipient().email("alice@example.com"))
.data(Map.of("user", Map.of("first_name", "Alice")))
);
for (SendResult r : result.getResults()) {
System.out.printf("%s: success=%b%n", r.getChannel(), r.isSuccess());
}
}
}Builder pattern
Norq norq = Norq.builder()
.projectDir("./my-project")
.strict(true)
.recipientResolver(myResolver)
.build();| Option | Type | Description |
|---|---|---|
projectDir |
String |
Path to the norq project directory. Required — templates are read from this directory and compiled in-process. |
strict |
boolean |
Reject sends that reference missing data or unknown pipes. Default true. |
recipientResolver |
RecipientResolver |
Resolves user ID strings to Recipient objects |
API
send(notification, opts)
Compile and deliver a notification.
SendMultiResult result = norq.send("transactional/welcome", new SendOpts()
.to(new Recipient()
.email("user@example.com")
.cc(List.of("audit@example.com"))) // email-channel CC/BCC
.data(Map.of("user", Map.of("first_name", "Gaurav")))
.channels(List.of("email"))
.dryRun(false)
.idempotencyKey("order-shipped-ORD-123")); // optional — see below
// result.getResults() -> List<SendResult> (one per channel)
// result.getSkipped() -> List<SkippedChannel> (channels that were skipped and why)idempotencyKey(...) is forwarded to providers that honour it (Resend, SuprSend) as the Idempotency-Key: header. When null or empty, the SDK generates a UUID v4 per send and applies it to every channel; passing a stable user-derived key (order-shipped-ORD-123) extends dedup across retries and processes.
Recipient.cc(...) / bcc(...) apply to the email channel only; providers that support them (Resend) include them in the request body, others ignore.
SendOpts.attachments(List<Attachment>) accepts Attachment builders: new Attachment().filename("invoice.pdf").content(byte[]).contentType("application/pdf").contentId("logo").disposition("inline"). The SDK base64-encodes byte[] automatically. contentType is optional — falls back to filename-extension inference for the curated whitelist. Use disposition("inline") + contentId(...) to embed images, then reference them as <img src="cid:logo">. Provider caps: Resend 40 MB total, SendGrid 30 MB. See the Attachments reference for the full whitelist.
batch(notification, channel)
Create a batch for sending a single channel template to many recipients. The template is prepared once, then each add() call renders with per-recipient data. Requests are buffered and auto-flushed at the provider’s max batch size.
Batch batch = norq.batch("transactional/welcome", "email");
for (User user : users) {
batch.add(user.getEmail(), Map.of("user", Map.of("first_name", user.getName())));
}
List<SendResult> results = batch.flush();lint(notification)
List<LintResult> results = norq.lint(null); // all
List<LintResult> results = norq.lint("transactional/welcome"); // one
for (LintResult result : results) {
for (LintDiagnostic diag : result.getDiagnostics()) {
System.out.printf("%s: %s [%s]%n",
diag.getSeverity(), diag.getMessage(), diag.getRule());
}
}RecipientResolver
To send by user ID instead of explicit recipient details, configure a RecipientResolver:
Norq norq = Norq.builder()
.projectDir("./my-project")
.recipientResolver(userId -> {
User user = db.getUser(userId);
return new Recipient()
.email(user.getEmail())
.phone(user.getPhone());
})
.build();
norq.send("transactional/welcome",
new SendOpts().to("user-123").data(Map.of("user", Map.of("first_name", "Alice"))));Error handling
import sh.norq.NorqException;
try {
norq.send("transactional/welcome", opts);
} catch (NorqException e) {
System.err.println("Norq error: " + e.getMessage());
}Sending push notifications
Push delivery is per-token. A PushRecipient holds every device the user has registered, possibly across ios, android, and web. The SDK fans the send out one HTTP call per token and emits one SendResult per token in result.getResults().
Token shape
PushRecipient push = new PushRecipient().tokens(List.of(
new PushToken("fcm-android-token", "android"),
new PushToken("apns-ios-token", "ios"),
new PushToken(
"https://updates.push.services.mozilla.com/wpush/v2/...",
"web"
).keys(new PushVapidKeys("BNc...", "tBHI...")),
new PushToken("ExponentPushToken[xxx]", "ios").provider("expo")
));| Field (builder) | Required | Notes |
|---|---|---|
new PushToken(token, platform) |
yes | platform is "ios", "android", or "web". |
.provider(name) |
no | Pin this token to a specific provider (e.g. "expo"); overrides routing.push.<platform>. |
.keys(new PushVapidKeys(p256dh, auth)) |
iff web |
Captured from the browser’s PushSubscription. |
APNs tokens additionally accept environment (sandbox | production) — pass it through the underlying JSON if you need per-token sandbox routing.
Mixed-platform recipient
SendMultiResult result = norq.send("transactional/order-shipped", new SendOpts()
.to(new Recipient().push(push))
.data(Map.of("user", Map.of("first_name", "Gaurav")))
.channels(List.of("push")));Per-token result fields
Each push SendResult carries two extra fields:
| Field | Meaning |
|---|---|
getTokenIndex() |
Index into the original pushRecipient.getTokens() list. |
getTokenStatus() |
Normalised cross-provider status: "invalid" (FCM UNREGISTERED, APNs BadDeviceToken, Web Push 404/410) or "transient" (5xx, rate limits). null on success. |
Reaction guide
getTokenStatus() |
What it means | What to do |
|---|---|---|
"invalid" |
Token is permanently dead. | Drop from DB; don’t retry. |
"transient" |
Provider had a temporary failure. | Retry with backoff; token still good. |
null on success |
Delivery accepted. | No action. |
for (SendResult r : result.getResults()) {
if (!"push".equals(r.getChannel())) continue;
PushToken token = recipient.getPush().getTokens().get(r.getTokenIndex());
if ("invalid".equals(r.getTokenStatus())) {
db.deletePushToken(userId, token.getToken());
} else if ("transient".equals(r.getTokenStatus())) {
retryQueue.enqueue(userId, token, Instant.now().plusSeconds(60));
}
}See Push provider errors for the full set of per-token failure modes.
Codegen
norq codegen --lang java --out src/main/java/com/example/NorqTypes.javaGenerates typed Java classes for each notification’s data shape.