Repository files navigation

@getpayin-tech/paylink-java

CIMaven CentralJavaLicense

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.

Requirements

  • Java 17+
  • The core paylink-core has one dependency (Gson); the Spring Boot starter adds Spring.

Install

Maven — core:

<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.2.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.2.0</version>
</dependency>

Gradle:

implementation 'com.getpayin.paylink:paylink-core:0.2.0'// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.2.0'

Quick start

importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importcom.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClientpaylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.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());

For an embedded checkout, pass .iframe(true) and render checkout.checkoutUrl() inside an <iframe> instead of redirecting:

CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD")
.iframe(true));

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='paylink_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

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.

Spring Boot (dependency injection)

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=2
importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importorg.springframework.stereotype.Service;
@ServicepublicclassCheckoutService {
privatefinalPaylinkClientpaylink;
publicCheckoutService(PaylinkClientpaylink) {
this.paylink = paylink;
}
publicStringstartCheckout() {
returnpaylink.invoices().create(
newCreateInvoiceParams()
.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).

Payment operations

paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResultstatus = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]// Refunds are idempotent when you pass an idempotency key — safe to retry:RefundResultrefund = paylink.payments().refund(12345, "10.50", "refund-order-1234");

Card tokenization

TokenizeCardResultvaulted = paylink.cards().tokenize(
newTokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
newChargeCardParams()
.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).

Recurring mandates

CreateRecurringResultmandate = paylink.recurring().create(
newCreateRecurringParams()
.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());

Idempotency

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).

Verifying webhooks

Pass the raw request body to verify(). It recomputes the signature with your hashToken and compares in constant time.

importcom.getpayin.paylink.exception.PaylinkSignatureException;
importcom.getpayin.paylink.webhook.WebhookEvent;
importcom.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEventevent = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureExceptione) {
// 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.

Error handling

Every failure extends PaylinkException:

ExceptionWhen
PaylinkConfigurationExceptionInvalid client configuration (missing tokens, non-positive timeout).
PaylinkApiExceptionThe API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags.
PaylinkSignatureExceptionA webhook signature did not verify.
PaylinkConnectionExceptionNetwork failure or timeout (no HTTP response).

Retries and rate limiting

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.

Amounts and precision

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").

Custom transport

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.

API reference

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

Contributing

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.

License

MIT

About

Official server-side Java SDK for the PayLink payment integration API — checkouts, payments, card tokens, recurring mandates, and webhook verification. Spring Boot starter included.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

@getpayin-tech/paylink-java

CIMaven CentralJavaLicense

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.

Requirements

  • Java 17+
  • The core paylink-core has one dependency (Gson); the Spring Boot starter adds Spring.

Install

Maven — core:

<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.2.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.2.0</version>
</dependency>

Gradle:

implementation 'com.getpayin.paylink:paylink-core:0.2.0'// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.2.0'

Quick start

importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importcom.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClientpaylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.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());

For an embedded checkout, pass .iframe(true) and render checkout.checkoutUrl() inside an <iframe> instead of redirecting:

CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD")
.iframe(true));

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='paylink_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

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.

Spring Boot (dependency injection)

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=2
importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importorg.springframework.stereotype.Service;
@ServicepublicclassCheckoutService {
privatefinalPaylinkClientpaylink;
publicCheckoutService(PaylinkClientpaylink) {
this.paylink = paylink;
}
publicStringstartCheckout() {
returnpaylink.invoices().create(
newCreateInvoiceParams()
.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).

Payment operations

paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResultstatus = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]// Refunds are idempotent when you pass an idempotency key — safe to retry:RefundResultrefund = paylink.payments().refund(12345, "10.50", "refund-order-1234");

Card tokenization

TokenizeCardResultvaulted = paylink.cards().tokenize(
newTokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
newChargeCardParams()
.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).

Recurring mandates

CreateRecurringResultmandate = paylink.recurring().create(
newCreateRecurringParams()
.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());

Idempotency

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).

Verifying webhooks

Pass the raw request body to verify(). It recomputes the signature with your hashToken and compares in constant time.

importcom.getpayin.paylink.exception.PaylinkSignatureException;
importcom.getpayin.paylink.webhook.WebhookEvent;
importcom.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEventevent = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureExceptione) {
// 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.

Error handling

Every failure extends PaylinkException:

ExceptionWhen
PaylinkConfigurationExceptionInvalid client configuration (missing tokens, non-positive timeout).
PaylinkApiExceptionThe API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags.
PaylinkSignatureExceptionA webhook signature did not verify.
PaylinkConnectionExceptionNetwork failure or timeout (no HTTP response).

Retries and rate limiting

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.

Amounts and precision

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").

Custom transport

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.

API reference

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

Contributing

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.

License

MIT

About

Official server-side Java SDK for the PayLink payment integration API — checkouts, payments, card tokens, recurring mandates, and webhook verification. Spring Boot starter included.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

@getpayin-tech/paylink-java

CIMaven CentralJavaLicense

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.

Requirements

  • Java 17+
  • The core paylink-core has one dependency (Gson); the Spring Boot starter adds Spring.

Install

Maven — core:

<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.2.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.2.0</version>
</dependency>

Gradle:

implementation 'com.getpayin.paylink:paylink-core:0.2.0'// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.2.0'

Quick start

importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importcom.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClientpaylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.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());

For an embedded checkout, pass .iframe(true) and render checkout.checkoutUrl() inside an <iframe> instead of redirecting:

CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD")
.iframe(true));

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='paylink_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

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.

Spring Boot (dependency injection)

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=2
importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importorg.springframework.stereotype.Service;
@ServicepublicclassCheckoutService {
privatefinalPaylinkClientpaylink;
publicCheckoutService(PaylinkClientpaylink) {
this.paylink = paylink;
}
publicStringstartCheckout() {
returnpaylink.invoices().create(
newCreateInvoiceParams()
.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).

Payment operations

paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResultstatus = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]// Refunds are idempotent when you pass an idempotency key — safe to retry:RefundResultrefund = paylink.payments().refund(12345, "10.50", "refund-order-1234");

Card tokenization

TokenizeCardResultvaulted = paylink.cards().tokenize(
newTokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
newChargeCardParams()
.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).

Recurring mandates

CreateRecurringResultmandate = paylink.recurring().create(
newCreateRecurringParams()
.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());

Idempotency

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).

Verifying webhooks

Pass the raw request body to verify(). It recomputes the signature with your hashToken and compares in constant time.

importcom.getpayin.paylink.exception.PaylinkSignatureException;
importcom.getpayin.paylink.webhook.WebhookEvent;
importcom.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEventevent = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureExceptione) {
// 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.

Error handling

Every failure extends PaylinkException:

ExceptionWhen
PaylinkConfigurationExceptionInvalid client configuration (missing tokens, non-positive timeout).
PaylinkApiExceptionThe API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags.
PaylinkSignatureExceptionA webhook signature did not verify.
PaylinkConnectionExceptionNetwork failure or timeout (no HTTP response).

Retries and rate limiting

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.

Amounts and precision

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").

Custom transport

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.

API reference

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

Contributing

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.

License

MIT

About

Official server-side Java SDK for the PayLink payment integration API — checkouts, payments, card tokens, recurring mandates, and webhook verification. Spring Boot starter included.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

@getpayin-tech/paylink-java

CIMaven CentralJavaLicense

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.

Requirements

  • Java 17+
  • The core paylink-core has one dependency (Gson); the Spring Boot starter adds Spring.

Install

Maven — core:

<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.2.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.2.0</version>
</dependency>

Gradle:

implementation 'com.getpayin.paylink:paylink-core:0.2.0'// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.2.0'

Quick start

importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importcom.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClientpaylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.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());

For an embedded checkout, pass .iframe(true) and render checkout.checkoutUrl() inside an <iframe> instead of redirecting:

CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD")
.iframe(true));

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='paylink_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

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.

Spring Boot (dependency injection)

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=2
importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importorg.springframework.stereotype.Service;
@ServicepublicclassCheckoutService {
privatefinalPaylinkClientpaylink;
publicCheckoutService(PaylinkClientpaylink) {
this.paylink = paylink;
}
publicStringstartCheckout() {
returnpaylink.invoices().create(
newCreateInvoiceParams()
.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).

Payment operations

paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResultstatus = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]// Refunds are idempotent when you pass an idempotency key — safe to retry:RefundResultrefund = paylink.payments().refund(12345, "10.50", "refund-order-1234");

Card tokenization

TokenizeCardResultvaulted = paylink.cards().tokenize(
newTokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
newChargeCardParams()
.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).

Recurring mandates

CreateRecurringResultmandate = paylink.recurring().create(
newCreateRecurringParams()
.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());

Idempotency

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).

Verifying webhooks

Pass the raw request body to verify(). It recomputes the signature with your hashToken and compares in constant time.

importcom.getpayin.paylink.exception.PaylinkSignatureException;
importcom.getpayin.paylink.webhook.WebhookEvent;
importcom.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEventevent = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureExceptione) {
// 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.

Error handling

Every failure extends PaylinkException:

ExceptionWhen
PaylinkConfigurationExceptionInvalid client configuration (missing tokens, non-positive timeout).
PaylinkApiExceptionThe API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags.
PaylinkSignatureExceptionA webhook signature did not verify.
PaylinkConnectionExceptionNetwork failure or timeout (no HTTP response).

Retries and rate limiting

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.

Amounts and precision

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").

Custom transport

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.

API reference

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

Contributing

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.

License

MIT

About

Official server-side Java SDK for the PayLink payment integration API — checkouts, payments, card tokens, recurring mandates, and webhook verification. Spring Boot starter included.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

@getpayin-tech/paylink-java

CIMaven CentralJavaLicense

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.

Requirements

  • Java 17+
  • The core paylink-core has one dependency (Gson); the Spring Boot starter adds Spring.

Install

Maven — core:

<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.2.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.2.0</version>
</dependency>

Gradle:

implementation 'com.getpayin.paylink:paylink-core:0.2.0'// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.2.0'

Quick start

importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importcom.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClientpaylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.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());

For an embedded checkout, pass .iframe(true) and render checkout.checkoutUrl() inside an <iframe> instead of redirecting:

CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD")
.iframe(true));

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='paylink_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

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.

Spring Boot (dependency injection)

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=2
importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importorg.springframework.stereotype.Service;
@ServicepublicclassCheckoutService {
privatefinalPaylinkClientpaylink;
publicCheckoutService(PaylinkClientpaylink) {
this.paylink = paylink;
}
publicStringstartCheckout() {
returnpaylink.invoices().create(
newCreateInvoiceParams()
.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).

Payment operations

paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResultstatus = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]// Refunds are idempotent when you pass an idempotency key — safe to retry:RefundResultrefund = paylink.payments().refund(12345, "10.50", "refund-order-1234");

Card tokenization

TokenizeCardResultvaulted = paylink.cards().tokenize(
newTokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
newChargeCardParams()
.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).

Recurring mandates

CreateRecurringResultmandate = paylink.recurring().create(
newCreateRecurringParams()
.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());

Idempotency

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).

Verifying webhooks

Pass the raw request body to verify(). It recomputes the signature with your hashToken and compares in constant time.

importcom.getpayin.paylink.exception.PaylinkSignatureException;
importcom.getpayin.paylink.webhook.WebhookEvent;
importcom.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEventevent = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureExceptione) {
// 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.

Error handling

Every failure extends PaylinkException:

ExceptionWhen
PaylinkConfigurationExceptionInvalid client configuration (missing tokens, non-positive timeout).
PaylinkApiExceptionThe API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags.
PaylinkSignatureExceptionA webhook signature did not verify.
PaylinkConnectionExceptionNetwork failure or timeout (no HTTP response).

Retries and rate limiting

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.

Amounts and precision

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").

Custom transport

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.

API reference

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

Contributing

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.

License

MIT

About

Official server-side Java SDK for the PayLink payment integration API — checkouts, payments, card tokens, recurring mandates, and webhook verification. Spring Boot starter included.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

@getpayin-tech/paylink-java

CIMaven CentralJavaLicense

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.

Requirements

  • Java 17+
  • The core paylink-core has one dependency (Gson); the Spring Boot starter adds Spring.

Install

Maven — core:

<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.2.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.2.0</version>
</dependency>

Gradle:

implementation 'com.getpayin.paylink:paylink-core:0.2.0'// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.2.0'

Quick start

importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importcom.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClientpaylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.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());

For an embedded checkout, pass .iframe(true) and render checkout.checkoutUrl() inside an <iframe> instead of redirecting:

CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD")
.iframe(true));

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='paylink_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

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.

Spring Boot (dependency injection)

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=2
importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importorg.springframework.stereotype.Service;
@ServicepublicclassCheckoutService {
privatefinalPaylinkClientpaylink;
publicCheckoutService(PaylinkClientpaylink) {
this.paylink = paylink;
}
publicStringstartCheckout() {
returnpaylink.invoices().create(
newCreateInvoiceParams()
.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).

Payment operations

paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResultstatus = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]// Refunds are idempotent when you pass an idempotency key — safe to retry:RefundResultrefund = paylink.payments().refund(12345, "10.50", "refund-order-1234");

Card tokenization

TokenizeCardResultvaulted = paylink.cards().tokenize(
newTokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
newChargeCardParams()
.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).

Recurring mandates

CreateRecurringResultmandate = paylink.recurring().create(
newCreateRecurringParams()
.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());

Idempotency

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).

Verifying webhooks

Pass the raw request body to verify(). It recomputes the signature with your hashToken and compares in constant time.

importcom.getpayin.paylink.exception.PaylinkSignatureException;
importcom.getpayin.paylink.webhook.WebhookEvent;
importcom.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEventevent = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureExceptione) {
// 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.

Error handling

Every failure extends PaylinkException:

ExceptionWhen
PaylinkConfigurationExceptionInvalid client configuration (missing tokens, non-positive timeout).
PaylinkApiExceptionThe API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags.
PaylinkSignatureExceptionA webhook signature did not verify.
PaylinkConnectionExceptionNetwork failure or timeout (no HTTP response).

Retries and rate limiting

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.

Amounts and precision

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").

Custom transport

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.

API reference

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

Contributing

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.

License

MIT

About

Official server-side Java SDK for the PayLink payment integration API — checkouts, payments, card tokens, recurring mandates, and webhook verification. Spring Boot starter included.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

@getpayin-tech/paylink-java

CIMaven CentralJavaLicense

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.

Requirements

  • Java 17+
  • The core paylink-core has one dependency (Gson); the Spring Boot starter adds Spring.

Install

Maven — core:

<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.2.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.2.0</version>
</dependency>

Gradle:

implementation 'com.getpayin.paylink:paylink-core:0.2.0'// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.2.0'

Quick start

importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importcom.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClientpaylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.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());

For an embedded checkout, pass .iframe(true) and render checkout.checkoutUrl() inside an <iframe> instead of redirecting:

CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD")
.iframe(true));

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='paylink_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

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.

Spring Boot (dependency injection)

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=2
importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importorg.springframework.stereotype.Service;
@ServicepublicclassCheckoutService {
privatefinalPaylinkClientpaylink;
publicCheckoutService(PaylinkClientpaylink) {
this.paylink = paylink;
}
publicStringstartCheckout() {
returnpaylink.invoices().create(
newCreateInvoiceParams()
.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).

Payment operations

paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResultstatus = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]// Refunds are idempotent when you pass an idempotency key — safe to retry:RefundResultrefund = paylink.payments().refund(12345, "10.50", "refund-order-1234");

Card tokenization

TokenizeCardResultvaulted = paylink.cards().tokenize(
newTokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
newChargeCardParams()
.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).

Recurring mandates

CreateRecurringResultmandate = paylink.recurring().create(
newCreateRecurringParams()
.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());

Idempotency

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).

Verifying webhooks

Pass the raw request body to verify(). It recomputes the signature with your hashToken and compares in constant time.

importcom.getpayin.paylink.exception.PaylinkSignatureException;
importcom.getpayin.paylink.webhook.WebhookEvent;
importcom.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEventevent = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureExceptione) {
// 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.

Error handling

Every failure extends PaylinkException:

ExceptionWhen
PaylinkConfigurationExceptionInvalid client configuration (missing tokens, non-positive timeout).
PaylinkApiExceptionThe API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags.
PaylinkSignatureExceptionA webhook signature did not verify.
PaylinkConnectionExceptionNetwork failure or timeout (no HTTP response).

Retries and rate limiting

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.

Amounts and precision

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").

Custom transport

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.

API reference

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

Contributing

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.

License

MIT

About

Official server-side Java SDK for the PayLink payment integration API — checkouts, payments, card tokens, recurring mandates, and webhook verification. Spring Boot starter included.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

@getpayin-tech/paylink-java

CIMaven CentralJavaLicense

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.

Requirements

  • Java 17+
  • The core paylink-core has one dependency (Gson); the Spring Boot starter adds Spring.

Install

Maven — core:

<dependency>
<groupId>com.getpayin.paylink</groupId>
<artifactId>paylink-core</artifactId>
<version>0.2.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.2.0</version>
</dependency>

Gradle:

implementation 'com.getpayin.paylink:paylink-core:0.2.0'// or, for Spring Boot:
implementation 'com.getpayin.paylink:paylink-spring-boot-starter:0.2.0'

Quick start

importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importcom.getpayin.paylink.model.CreateInvoiceResult;
PaylinkClientpaylink = PaylinkClient.builder()
.publicToken(System.getenv("PAYLINK_PUBLIC_TOKEN"))
.hashToken(System.getenv("PAYLINK_HASH_TOKEN")) // secret — server-side only
.build();
CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.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());

For an embedded checkout, pass .iframe(true) and render checkout.checkoutUrl() inside an <iframe> instead of redirecting:

CreateInvoiceResultcheckout = paylink.invoices().create(
newCreateInvoiceParams()
.firstName("John").lastName("Doe").email("john@example.com")
.orderTitle("Gold Plan").orderAmount("250.00").currency("USD")
.iframe(true));

Then embed the returned checkout URL on your page. The <iframe> needs allow="payment *" so Apple Pay and Google Pay work inside the frame, and your page must listen for the completion message — in iframe mode the checkout signals the parent via postMessage instead of redirecting:

<iframesrc="CHECKOUT_URL" allow="payment *"
style="width:100%;min-height:640px;border:0" title="Secure checkout"></iframe><script>addEventListener('message',function(e){if(e.origin!=='https://pay.getpayin.com')return;// your API base originif(!e.data||e.data.type!=='paylink_payment')return;window.location.href=e.data.success ? '/thank-you' : '/checkout?failed=1';});</script>

The embedding page's origin must exactly match your integration's registered Origin, or the browser blocks framing and the message never arrives.

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.

Spring Boot (dependency injection)

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=2
importcom.getpayin.paylink.PaylinkClient;
importcom.getpayin.paylink.model.CreateInvoiceParams;
importorg.springframework.stereotype.Service;
@ServicepublicclassCheckoutService {
privatefinalPaylinkClientpaylink;
publicCheckoutService(PaylinkClientpaylink) {
this.paylink = paylink;
}
publicStringstartCheckout() {
returnpaylink.invoices().create(
newCreateInvoiceParams()
.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).

Payment operations

paylink.payments().voidInvoice(12345);
paylink.payments().settle(12345, "50.00");
paylink.payments().reverseAuthorization(12345);
PaymentResultstatus = paylink.payments().checkStatus(12345);
// PaymentResult[invoiceId=12345, paidStatus=..., authCode=...]// Refunds are idempotent when you pass an idempotency key — safe to retry:RefundResultrefund = paylink.payments().refund(12345, "10.50", "refund-order-1234");

Card tokenization

TokenizeCardResultvaulted = paylink.cards().tokenize(
newTokenizeCardParams()
.firstName("Jane").lastName("Doe")
.cardNumber("4111111111111111").cardExpiryMonth("12").cardExpiryYear("2030").cardCvv("123")
.country("EG").address("1 Main St").city("Cairo"));
paylink.cards().charge(
newChargeCardParams()
.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).

Recurring mandates

CreateRecurringResultmandate = paylink.recurring().create(
newCreateRecurringParams()
.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());

Idempotency

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).

Verifying webhooks

Pass the raw request body to verify(). It recomputes the signature with your hashToken and compares in constant time.

importcom.getpayin.paylink.exception.PaylinkSignatureException;
importcom.getpayin.paylink.webhook.WebhookEvent;
importcom.getpayin.paylink.webhook.WebhookEventType;
try {
WebhookEventevent = paylink.webhooks().verify(rawRequestBody);
if (WebhookEventType.INVOICE_PAID.equals(event.event())) {
// fulfil the order
}
} catch (PaylinkSignatureExceptione) {
// 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.

Error handling

Every failure extends PaylinkException:

ExceptionWhen
PaylinkConfigurationExceptionInvalid client configuration (missing tokens, non-positive timeout).
PaylinkApiExceptionThe API returned an error. Carries status(), errors(), raw(), retryAfter(), and the isIdempotencyConflict() / isRateLimited() / isForbidden() flags.
PaylinkSignatureExceptionA webhook signature did not verify.
PaylinkConnectionExceptionNetwork failure or timeout (no HTTP response).

Retries and rate limiting

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.

Amounts and precision

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").

Custom transport

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.

API reference

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

Contributing

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.

License

MIT

About

Official server-side Java SDK for the PayLink payment integration API — checkouts, payments, card tokens, recurring mandates, and webhook verification. Spring Boot starter included.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages