Official server-side Java SDK for the PayLink payment integration API. It wraps every integration endpoint with an idiomatic, typed API and computes the order-sensitive HMAC-SHA256 signatures for you, so you never have to build them by hand. Ships a Spring Boot starter for one-line dependency injection.
- Checkouts (
invoices().create) - Payment operations (
payments().voidInvoice/refund/settle/reverseAuthorization/checkStatus) - Server-to-server card charges (
vcc().charge) - Card tokenization (
cards().tokenize/charge/revoke) - Recurring mandates (
recurring().create/status/cancel/pause/resume) - Webhook signature verification (
webhooks().verify)
Server-side only. Signing uses your secret
hashToken. Never ship it to a browser, mobile, or desktop client.
- Java 17+
- The core
paylink-corehas one dependency (Gson); the Spring Boot starter adds Spring.
Maven — core:
<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.1.0</version>
</dependency>Spring Boot apps — use the starter instead (it pulls in the core):
<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>Gradle:
implementation 'com.getpayin.paylink:paylink-core:0.1.0'
// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.1.0'import com.getpayin.paylink.PaylinkClient;
import com.getpayin.paylink.model.CreateInvoiceParams;
import com.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClient paylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResult checkout = paylink.invoices().create(
new CreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan")
.orderAmount("250.00") // pass money as strings for an exact wire form
.currency("USD"));
// Redirect the payer to the hosted checkout:
response.sendRedirect(checkout.checkoutUrl());Both credentials are issued in the PayLink dashboard under Settings → Payment
Integrations. publicToken is sent on every request; hashToken is the secret used
only to sign — it never leaves your server.
With the starter on the classpath, set the credentials and inject PaylinkClient —
constructor injection is the idiomatic way:
paylink.public-token=pub_...
paylink.hash-token=secret_...
# optional: paylink.base-url, paylink.timeout=30s, paylink.max-retries=2import com.getpayin.paylink.PaylinkClient;
import com.getpayin.paylink.model.CreateInvoiceParams;
import org.springframework.stereotype.Service;
@Service
public class CheckoutService {
private final PaylinkClient paylink;
public CheckoutService(PaylinkClient paylink) {
this.paylink = paylink;
}
public String startCheckout() {
return paylink.invoices().create(
new CreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD"))
.checkoutUrl();
}
}The auto-configured bean backs off if you define your own PaylinkClient, and it uses a
Transport bean if you provide one (for example one backed by Spring's HTTP client).
paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResult status = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]
// Refunds are idempotent when you pass an idempotency key — safe to retry:
RefundResult refund = paylink.payments().refund(12345, "10.50", "refund-order-1234");TokenizeCardResult vaulted = paylink.cards().tokenize(
new TokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
new ChargeCardParams()
.cardToken(vaulted.token()).initiator("merchant")
.firstName("Jane").lastName("Doe")
.currency("USD").price("100.00").product("Monthly rebill")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().revoke(vaulted.token());For US and CA billing addresses, also pass the state fields the API requires:
usState + postalCode (US) or canadaState + postalCode (CA).
CreateRecurringResult mandate = paylink.recurring().create(
new CreateRecurringParams()
.firstName("Sam").lastName("Doe").email("sam@example.com")
.orderTitle("Gold subscription").orderAmount("250.00").currency("USD")
.cadenceInterval("month").cadenceCount(1).totalCycles(12)
.consentText("I authorise recurring monthly charges."),
"sub-signup-42");
paylink.recurring().status(mandate.mandateId());
paylink.recurring().pause(mandate.mandateId());
paylink.recurring().resume(mandate.mandateId());
paylink.recurring().cancel(mandate.mandateId());Pass an idempotency key to make a retried write safe — the SDK sends it as the
Idempotency-Key header and the server returns the original result instead of charging,
refunding, or creating a second time. Keys are scoped per integration and capped at 64
characters. Honored on invoices().create, vcc().charge, cards().charge,
payments().refund, and recurring().create.
Reusing a key with a different request is rejected as a conflict: a
PaylinkApiException whose isIdempotencyConflict() is true (HTTP 409).
Pass the raw request body to verify(). It recomputes the signature with your
hashToken and compares in constant time.
import com.getpayin.paylink.exception.PaylinkSignatureException;
import com.getpayin.paylink.webhook.WebhookEvent;
import com.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEvent event = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureException e) {
// reject — do not trust this payload
}PayLink webhook signatures carry no timestamp, so verification does not protect against replay. Pair it with your own idempotency keyed on
invoice_id.
Every failure extends PaylinkException:
| Exception | When |
|---|---|
PaylinkConfigurationException |
Invalid client configuration (missing tokens, non-positive timeout). |
PaylinkApiException |
The API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags. |
PaylinkSignatureException |
A webhook signature did not verify. |
PaylinkConnectionException |
Network failure or timeout (no HTTP response). |
Every integration endpoint is rate limited server-side, so 429s are an expected condition
under burst traffic. The SDK retries transient failures — 429, 5xx, connection errors,
and timeouts — with exponential backoff and full jitter, honoring the server's
Retry-After header when present.
A request is only ever replayed when replaying it cannot double-charge: GETs, any call
you pass an idempotency key to, and pure reads such as payments().checkStatus. A bare
vcc().charge or cards().charge is never replayed. Tune with .maxRetries(...) (0
disables). The timeout applies to each attempt.
Signatures are computed over the exact bytes sent on the wire. To avoid any
floating-point ambiguity, pass monetary amounts as strings (e.g. "10.50").
Implement com.getpayin.paylink.http.Transport for a proxy-aware or pooled client, a
framework's HTTP client, or a mock in tests, and set it with .transport(...) (or expose
it as a Spring bean). The default uses the JDK's HttpClient.
The full HTTP API — endpoints, fields, error codes, and test cards — is documented in the PayLink API reference: https://pay.getpayin.com/docs/payment_integration/index.html
See CONTRIBUTING.md — in particular the note on signed-field ordering, which must stay in lockstep with the server.
Security issues: see SECURITY.md. Please do not open a public issue for a vulnerability.
MIT