Skip to content

Repository files navigation

pingen2-sdk-java

The official Java SDK for using the Pingen API

Features

  • OAuth2 client credentials authentication with automatic token refresh
  • Full support for letters, batches, emails, e-bills, webhooks, organisations, and user
  • Automatic 3-step file upload handling
  • Flexible filtering, sorting, and pagination via CollectionParams and Filter
  • Built on Java 17 with minimal runtime dependencies
  • Thread-safe — a single Pingen instance can be shared across threads
  • Typed exception hierarchy for authentication, validation, and rate-limit errors
  • Production and staging environments

Requirements

  • Java 17 or higher
  • Maven

Installation

<dependency>
<groupId>com.pingen.sdk</groupId>
<artifactId>pingen2-sdk-java</artifactId>
<version>2.0.0</version>
</dependency>

Quick Start

importcom.pingen.sdk.Pingen;
importcom.pingen.sdk.models.letter.*;
Pingenpingen = Pingen.builder()
.clientId("your-client-id")
.clientSecret("your-client-secret")
.build();
StringorgId = pingen.organisations()
.getCollection()
.getItems()
.get(0)
.getId();
varletter = pingen.letters(orgId).create(
LetterCreateRequest.builder()
.filePath("invoice.pdf")
.fileOriginalName("invoice.pdf")
.addressPosition(AddressPosition.LEFT)
.autoSend(true)
.build()
);
System.out.println("Created letter: " + letter.getId());

Authentication

The SDK uses the OAuth2 client credentials grant. Obtain your Client ID and Client Secret by creating a Developer App in your Pingen account (User profile → API Access).

Environments

// Production (default)Pingenpingen = Pingen.builder()
.clientId("your-client-id")
.clientSecret("your-client-secret")
.build();
// Staging — separate credentials requiredPingenpingen = Pingen.builder()
.clientId("your-staging-client-id")
.clientSecret("your-staging-client-secret")
.staging()
.build();

Staging requires a separate account at https://identity-staging.pingen.com. Letters sent in staging are simulated and never printed.

Custom Timeouts

Pingenpingen = Pingen.builder()
.clientId("your-client-id")
.clientSecret("your-client-secret")
.connectTimeout(Duration.ofSeconds(10))
.requestTimeout(Duration.ofMinutes(3))
.build();

Organisations

// List all organisationsvarorgs = pingen.organisations().getCollection();
for (varorg : orgs.getItems()) {
System.out.println(org.getId() + ": " + org.getAttributes().getName());
}
// Get a specific organisationvarorg = pingen.organisations().get("org-uuid");

Letters

Create

Provide either a file path or raw bytes. The SDK handles the 3-step upload internally.

// From a file pathvarletter = pingen.letters(orgId).create(
LetterCreateRequest.builder()
.filePath("invoice.pdf") // or Path.of(...)
.fileOriginalName("invoice.pdf")
.addressPosition(AddressPosition.LEFT) // LEFT or RIGHT
.autoSend(true)
.build()
);
// From bytesbyte[] pdf = Files.readAllBytes(Path.of("document.pdf"));
varletter = pingen.letters(orgId).create(
LetterCreateRequest.builder()
.fileBytes(pdf)
.fileOriginalName("document.pdf")
.addressPosition(AddressPosition.RIGHT)
.autoSend(false)
.build()
);

Optional parameters on LetterCreateRequest.Builder:

MethodTypeDescription
deliveryProduct(DeliveryProduct)enumFAST, CHEAP, BULK, PREMIUM, REGISTERED
printMode(PrintMode)enumSIMPLEX, DUPLEX
printSpectrum(PrintSpectrum)enumCOLOR, GRAYSCALE
metaData(LetterMetaData)objectProgrammatic recipient/sender address
additionalAttribute(String, Object)key/valuePass-through API attributes

MetaData (programmatic addresses)

varmeta = LetterMetaData.builder()
.recipient(LetterMetaData.AddressEntry.builder()
.name("Jane Doe")
.street("Main Street")
.number("1")
.zip("8000")
.city("Zurich")
.country("CH")
.build())
.sender(LetterMetaData.AddressEntry.builder()
.name("Acme AG")
.street("Business Road")
.number("42")
.zip("3000")
.city("Bern")
.country("CH")
.build())
.build();

Send (manual)

When autoSend is false, send the letter explicitly:

varsent = pingen.letters(orgId).send(
"letter-uuid",
LetterSendRequest.builder()
.deliveryProduct(DeliveryProduct.CHEAP)
.printMode(PrintMode.SIMPLEX)
.printSpectrum(PrintSpectrum.GRAYSCALE)
.build()
);

Other operations

// Retrievevarletter = pingen.letters(orgId).get("letter-uuid"); // Optional<Resource<Letter>>// Cancel (only possible before submission)pingen.letters(orgId).cancel("letter-uuid");
// Deletepingen.letters(orgId).delete("letter-uuid");
// Download the original PDFStringurl = pingen.letters(orgId).getFile("letter-uuid");
// Lifecycle events for a specific lettervarevents = pingen.letters(orgId).getEvents("letter-uuid");
// Scan image for a specific eventStringimageUrl = pingen.letters(orgId).getEventImage("letter-uuid", "event-uuid");

Price calculator

varresult = pingen.letters(orgId).calculatePrice(
LetterPriceCalculatorRequest.builder()
.country("CH")
.paperTypes(List.of("normal"))
.printMode(PrintMode.SIMPLEX)
.printSpectrum(PrintSpectrum.GRAYSCALE)
.deliveryProduct(DeliveryProduct.CHEAP)
.build()
);
System.out.println(result.getAttributes().getPrice() + " " + result.getAttributes().getCurrency());

Global delivery event feeds

// Letters that were delivered / sent / had issues / became undeliverablevardelivered = pingen.letters(orgId).getDeliveredEvents();
varsent = pingen.letters(orgId).getSentEvents();
varissues = pingen.letters(orgId).getIssueEvents();
varundelivered = pingen.letters(orgId).getUndeliverableEvents();

Batches

Batches let you upload many letters at once in a ZIP or merged PDF.

Create

varbatch = pingen.batches(orgId).create(
BatchCreateRequest.builder()
.filePath("monthly-invoices.zip")
.fileOriginalName("monthly-invoices.zip")
.name("Monthly Invoices June")
.icon(BatchIcon.RECEIPT)
.addressPosition(AddressPosition.LEFT)
.groupingType(GroupingType.ZIP)
.groupingOptionsSplitType(BatchGroupingSplitType.FILE)
.build()
);

For merged PDFs, choose a split strategy:

BatchCreateRequest.builder()
...
.groupingType(GroupingType.MERGE)
.groupingOptionsSplitType(BatchGroupingSplitType.PAGE)
.groupingOptionsSplitSize(2) // 2 pages per letter
.groupingOptionsSplitPosition(BatchGroupingSplitPosition.FIRST_PAGE)
.build();

BatchGroupingSplitType values: FILE, PAGE, CUSTOM, QR_INVOICE

Send

varsent = pingen.batches(orgId).send(
"batch-uuid",
BatchSendRequest.builder()
.addDeliveryProduct("CH", DeliveryProduct.CHEAP)
.addDeliveryProduct("DE", DeliveryProduct.FAST)
.printMode(PrintMode.SIMPLEX)
.printSpectrum(PrintSpectrum.GRAYSCALE)
.build()
);

Other operations

// Retrievevarbatch = pingen.batches(orgId).get("batch-uuid"); // Optional<Resource<Batch>>// Update name/iconpingen.batches(orgId).update("batch-uuid",
BatchUpdateRequest.builder()
.name("New Name")
.icon(BatchIcon.CAMPAIGN)
.build()
);
// Cancelpingen.batches(orgId).cancel("batch-uuid");
// Delete (batch only, keep letters)pingen.batches(orgId).deleteWithoutLetters("batch-uuid");
// Delete batch and all its letterspingen.batches(orgId).deleteWithLetters("batch-uuid");
// Lifecycle eventsvarevents = pingen.batches(orgId).getEvents("batch-uuid");
// Statistics (letter counts by country, region, group)varstats = pingen.batches(orgId).getStatistics("batch-uuid");

Emails

Send PDF documents as emails. The recipient address is specified in EmailMetaData.

varemail = pingen.emails(orgId).create(
EmailCreateRequest.builder()
.filePath("document.pdf")
.fileOriginalName("document.pdf")
.autoSend(true)
.metaData(
EmailMetaData.builder()
.senderName("Acme AG")
.recipientEmail("jane.doe@example.com")
.recipientName("Jane Doe")
.replyEmail("noreply@acme.com")
.replyName("Acme AG")
.subject("Your invoice")
.content("Please find your invoice attached.")
.build()
)
.build()
);
// Cancelpingen.emails(orgId).cancel("email-uuid");
// Deletepingen.emails(orgId).delete("email-uuid");
// DownloadStringurl = pingen.emails(orgId).getFile("email-uuid");
// Eventsvarevents = pingen.emails(orgId).getEvents("email-uuid");

E-Bills

Send Swiss e-bills (SIX e-invoicing network). The invoice metadata is required.

varebill = pingen.ebills(orgId).create(
EBillCreateRequest.builder()
.filePath("invoice.pdf")
.fileOriginalName("invoice.pdf")
.autoSend(false)
.metaData(
EBillMetaData.builder()
.invoiceNumber("INV-2024-001")
.invoiceDate(LocalDate.of(2024, 6, 1))
.invoiceDueDate(LocalDate.of(2024, 6, 30))
.recipientIdentifier("41010560425610173") // Swiss e-bill participant ID
.build()
)
.build()
);
// Send (when autoSend is false)pingen.ebills(orgId).send("ebill-uuid");
// Cancel / deletepingen.ebills(orgId).cancel("ebill-uuid");
pingen.ebills(orgId).delete("ebill-uuid");
// DownloadStringurl = pingen.ebills(orgId).getFile("ebill-uuid");
// Events and event imagesvarevents = pingen.ebills(orgId).getEvents("ebill-uuid");
StringimageUrl = pingen.ebills(orgId).getEventImage("ebill-uuid", "event-uuid");

Webhooks

varwebhook = pingen.webhooks(orgId).create(
WebhookCreateRequest.builder()
.url("https://your-domain.com/webhooks/pingen")
.eventCategory(WebhookEventCategory.DELIVERED)
.signingKey("your-20-to-32-char-key")
.build()
);
// Available event categories// WebhookEventCategory.ISSUES// WebhookEventCategory.SENT// WebhookEventCategory.UNDELIVERABLE// WebhookEventCategory.DELIVERED// WebhookEventCategory.CHANNEL_SUBSCRIPTIONSvarwebhooks = pingen.webhooks(orgId).getCollection();
varwh = pingen.webhooks(orgId).get("webhook-uuid");
pingen.webhooks(orgId).delete("webhook-uuid");

User

// Authenticated user's profilevaruser = pingen.user().get();
System.out.println(user.getAttributes().getEmail());
// Organisation associations (role per org)varassociations = pingen.user().getAssociations();

Pagination, Filtering & Sorting

All collection endpoints accept a CollectionParams object.

importcom.pingen.sdk.models.common.CollectionParams;
importcom.pingen.sdk.models.common.Filter;
// PaginationCollectionParamsparams = CollectionParams.builder()
.page(2, 50) // page number, items per page
.build();
// SortingCollectionParamsparams = CollectionParams.builder()
.sort("created_at") // ascending
.sortDesc("updated_at") // descending; multiple calls accumulate
.build();
// Full-text searchCollectionParamsparams = CollectionParams.builder()
.search("invoice")
.build();
// FilteringCollectionParamsparams = CollectionParams.builder()
.filter(Filter.eq("status", "sent"))
.build();
varletters = pingen.letters(orgId).getCollection(params);

Filter expressions

// Simple equalityFilter.eq("status", "sent")
// ComparatorsFilter.gt("created_at", "2024-01-01")
Filter.lt("price_value", 5)
Filter.gte("price_value", 1)
Filter.lte("price_value", 10)
Filter.notEq("status", "cancelled")
Filter.approx("address", "Zurich")
// Logical combinationsFilter.and(Filter.eq("status", "sent"), Filter.gt("created_at", "2024-01-01"))
Filter.or(Filter.eq("status", "sent"), Filter.eq("status", "delivered"))

Iterating through pages

intpage = 1;
PagedResponse<Letter> response;
do {
response = pingen.letters(orgId).getCollection(
CollectionParams.builder().page(page, 20).build()
);
for (varitem : response.getItems()) {
process(item);
}
page++;
} while (response.hasNext());
// Pagination metadataresponse.getTotal(); // total items across all pagesresponse.getCurrentPage(); // current page numberresponse.getLastPage(); // last page numberresponse.getPageLimit(); // items per pageresponse.hasNext();
response.hasPrev();

Error Handling

try {
varletter = pingen.letters(orgId).create(request);
} catch (AuthenticationExceptione) {
// HTTP 401 — invalid or expired credentialsSystem.err.println("Auth failed: " + e.getMessage());
System.err.println("Request ID: " + e.getRequestId());
} catch (ValidationExceptione) {
// HTTP 422 — request payload rejected by the APISystem.err.println("Validation error: " + e.getMessage());
System.err.println("Response body: " + e.getResponseBody());
} catch (RateLimitExceptione) {
// HTTP 429 — too many requestsSystem.err.println("Rate limited. Retry after: " + e.getRetryAfter() + "s");
System.err.println("Limit resets at: " + e.getRateLimitReset());
} catch (ApiExceptione) {
// Other HTTP error (4xx / 5xx)System.err.println("API error " + e.getStatusCode() + ": " + e.getMessage());
System.err.println("Request ID: " + e.getRequestId());
} catch (PingenExceptione) {
// Network error, interrupted request, or JSON parse failureSystem.err.println("SDK error: " + e.getMessage());
}

All exceptions carry an X-Request-Id header value (via getRequestId()) that you can include in support requests.

Staging Simulation

In the staging environment, the lifecycle of a letter is determined by the fileOriginalName:

Filename patternSimulated outcome
*_simulate_undeliverable.*Letter becomes undeliverable
*_simulate_unprintable.*Letter is rejected by the print centre
*_simulate_cancellable.*Letter stops in a cancellable state
Any other nameLetter is delivered successfully

Logging

The SDK uses SLF4J for debug-level logging (token acquisition, request URLs, HTTP status codes). Add any SLF4J-compatible implementation to your project — no specific one is required.

<!-- Example: Logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>

Building from Source

git clone https://github.com/pingencom/pingen2-sdk-java.git
cd pingen2-sdk-java
# Build and run unit tests
mvn clean install
# Run integration tests (requires credentials in environment)
mvn test -DexcludedGroups="" -Dgroups=integration
# Generate Javadoc
mvn javadoc:javadoc

Runtime Dependencies

DependencyPurpose
jackson-databindJSON serialisation / deserialisation
jackson-datatype-jsr310Java 8 date/time support for Jackson
slf4j-apiLogging façade (no implementation bundled)

About

The official Java SDK for using the Pingen API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages