Java SDK

Create Qasimha checkouts, confirm orders and verify webhooks from a Java backend with com.qasimha:qasimha-sdk.

com.qasimha:qasimha-sdk is the Java server SDK for the Qasimha merchant API. Calls are blocking, return typed models, and raise typed (unchecked) exceptions.

Requirements

ItemRequirement
RuntimeJava 11 or later. Works on Spring Boot 2.x and 3.x, javax and jakarta servlet containers
HTTP clientThe JDK's java.net.http.HttpClient
DependenciesJackson (jackson-databind, jackson-datatype-jsr310) only
KeysA secret key (sk_test_... for UAT). Optional: a publishable key (pk_test_...) for plan quotes, and a webhook secret (whsec_...)
API releaseReturn URLs, per-merchant webhook endpoints, getByReference and getPlans need the merchant API release of 2026-09-25 or later

Install

<dependency>
  <groupId>com.qasimha</groupId>
  <artifactId>qasimha-sdk</artifactId>
  <version>0.1.0</version>
</dependency>
implementation("com.qasimha:qasimha-sdk:0.1.0")

Configure

Create keys in the Merchant Portal under Settings → API Keys. Keep them in environment variables:

QASIMHA_API_KEY=sk_test_...
QASIMHA_PUBLISHABLE_KEY=pk_test_...
QASIMHA_WEBHOOK_SECRET=whsec_...

Build one client per API key and share it; it is thread-safe.

import com.qasimha.sdk.QasimhaClient;

QasimhaClient client = QasimhaClient.builder()
    .apiKey(System.getenv("QASIMHA_API_KEY"))
    .webhookSecret(System.getenv("QASIMHA_WEBHOOK_SECRET"))
    .publishableKey(System.getenv("QASIMHA_PUBLISHABLE_KEY"))
    .language("en")                       // "ar" or "en"
    .userAgentSuffix("my-store/1.4.0")
    .build();

In Spring Boot, expose the client as a @Bean built the same way from @Value properties.

The SDK picks the environment from the key prefix: sk_test_ uses UAT (sandbox), sk_live_ uses production. See Environments.

Builder methodDefaultNotes
.apiKey(String)—Required. sk_test_... or sk_live_...
.environment(Environment)from key prefixEnvironment.SANDBOX or Environment.PRODUCTION. A contradicting value throws ConfigurationException
.baseUrl(String)per environmentTest-server override. https:// only; http:// only for localhost / 127.0.0.1
.timeout(Duration)30 secondsPer attempt
.maxRetries(int)20–5; retries after the first attempt
.webhookSecret(String)—For client.webhooks()
.publishableKey(String)—For checkout().getPlans(...); same environment as the API key
.language(String)"en""ar" or "en"
.userAgentSuffix(String)—Appended to the SDK User-Agent
.logger(...)none(level, message, fields) callback
.httpClient(HttpClient)built-inYour own client; build it with HttpClient.Redirect.NEVER

Create a checkout session

Use BigDecimal from a string (new BigDecimal("1302.10")), never the double constructor. The amounts below add up: items 899.00 + 135.00 + 120.00 = 1154.00, plus tax 173.10, plus shipping 25.00, minus discount 50.00 = 1302.10. The Item builder computes each line's totalAmount when you leave it unset.

import com.qasimha.sdk.exception.DuplicateOrderReferenceException;
import com.qasimha.sdk.exception.OrderDeclinedException;
import com.qasimha.sdk.model.*;
import java.math.BigDecimal;

CreateSessionRequest request = CreateSessionRequest.builder()
    .orderReferenceId("ORDER-12345")                 // your own id, unique per order
    .totalAmount(new BigDecimal("1302.10"))
    .taxAmount(new BigDecimal("173.10"))
    .shippingAmount(new BigDecimal("25.00"))
    .discountAmount(new BigDecimal("50.00"))
    .description("Order #12345")
    .addItem(Item.builder()
        .referenceId("SKU-1").name("Wireless Headphones").type(Item.TYPE_PHYSICAL)
        .sku("SKU-1").quantity(1).unitPrice(new BigDecimal("899.00")).build())
    .addItem(Item.builder()
        .referenceId("SKU-2").name("USB-C Cable").type(Item.TYPE_PHYSICAL)
        .sku("SKU-2").quantity(3).unitPrice(new BigDecimal("45.00")).build())
    .addItem(Item.builder()
        .referenceId("SKU-3").name("Extended Warranty").type(Item.TYPE_DIGITAL)
        .sku("SKU-3").quantity(1).unitPrice(new BigDecimal("120.00")).build())
    .consumer(Consumer.builder()
        .firstName("Noura").lastName("Al-Shammari")
        .phoneNumber("+9665XXXXXXXX").email("[email protected]").build())
    .shippingAddress(Address.builder()                // required by the API
        .firstName("Noura").lastName("Al-Shammari")
        .line1("King Fahd Rd").city("Riyadh").countryCode("SA").build())
    .merchantUrl(MerchantUrl.builder()
        .success("https://store.example/checkout/return?order=12345")
        .failure("https://store.example/checkout/failed?order=12345")
        .cancel("https://store.example/cart")
        .build())
    .build();

try {
  Session session = client.checkout().createSession(request);
  saveQasimhaOrderId("ORDER-12345", session.getOrderId());   // every later call uses this id
  redirect(session.getPaymentUrl());
} catch (OrderDeclinedException e) {
  showDeclined();                                            // e.getOrderId() was not approved
} catch (DuplicateOrderReferenceException e) {
  redirect(e.getExistingPaymentUrl());                       // double submit: reopen the existing checkout
}

Defaults: paymentType pay_by_instalments, instalments 4, currency SAR, country SA. The SDK checks the basket before sending, to the halala, and throws ValidationException naming the line:

  • item.totalAmount = quantity × unitPrice − item.discountAmount
  • totalAmount = Σ item.totalAmount + taxAmount + shippingAmount − discountAmount

merchantUrl sends the shopper back to success, failure or cancel after the hosted checkout, which expires after two hours. See Checkout flows.

Confirm the order on the return route

The hosted checkout sends the shopper to your merchantUrl with orderId, orderReferenceId and status appended. These parameters are not signed. Read the order before you mark it paid:

@GetMapping("/checkout/return")
public String checkoutReturn(@RequestParam String orderId) {
  Order order = client.orders().get(orderId);
  if (OrderStatus.CONFIRMED.equals(order.getStatus())) {
    markPaid(order.getOrderReferenceId());            // first instalment paid
    return "redirect:/thank-you";
  }
  return "redirect:/checkout?payment=failed";
}

If you only hold your own reference, use client.orders().getByReference("ORDER-12345"). It returns the same Order, matches exactly (case-sensitive), and throws NotFoundException for an unknown reference. See Return URLs.

Handle webhooks

Verify every delivery with the raw request bytes exactly as received. A body that a framework parsed and re-serialised will not verify. Then confirm state with orders().get(...) before you act.

javax.servlet works the same way.

import com.qasimha.sdk.exception.WebhookSignatureException;
import com.qasimha.sdk.model.WebhookEvent;
import jakarta.servlet.http.*;

public class QasimhaWebhookServlet extends HttpServlet {
  private final QasimhaClient client = /* the shared client */;

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
    byte[] rawBody = request.getInputStream().readAllBytes();
    Map<String, String> headers = new HashMap<>();
    Collections.list(request.getHeaderNames()).forEach(n -> headers.put(n, request.getHeader(n)));

    WebhookEvent event;
    try {
      event = client.webhooks().constructEvent(rawBody, headers);
    } catch (WebhookSignatureException e) {
      response.setStatus(400);
      return;
    }
    response.setStatus(200);           // acknowledge promptly, process afterwards
    enqueue(event);                    // de-duplicate on event.getId()
  }
}

On failure, getReason() is MISSING_HEADER, MALFORMED_HEADER, TIMESTAMP_OUTSIDE_TOLERANCE or SIGNATURE_MISMATCH. The tolerance is 300 seconds by default. During a secret rotation, the old and new secrets both verify.

WebhookEvent carries id, type, orderId, orderReferenceId, status, amount, currency, occurredAt and getRaw(). If orderId is empty, confirm with orders().getByReference(event.getOrderReferenceId()). A service that only receives webhooks can verify without a full client: new Webhooks(System.getenv("QASIMHA_WEBHOOK_SECRET")).constructEvent(rawBody, headers). See Webhooks and Webhook signatures.

Fulfil, refund and cancel

// on shipment: omit shippedAmount to fulfil everything outstanding
client.orders().fulfill(orderId, FulfillRequest.builder()
    .shippedAmount(new BigDecimal("1200.00"))
    .shippingCompany("Aramex").trackingNumber("ARX123").build());

// after fulfilment; order.getRefundableAmount() is what can still be refunded
client.orders().refund(orderId, RefundRequest.of(new BigDecimal("300.00"), "Customer return"));

// voids an order that has not been fulfilled
client.orders().cancel(orderId);

capture(orderId) is only for New / Approved orders. A hosted-checkout order is already Confirmed, and capture answers 400 INVALID_STATUS. See Order lifecycle.

Quote plans with the publishable key

checkout().getPlans(...) returns the plans your policy offers for an amount. It sends the publishable key (X-Public-Key) only, never the secret key.

import com.qasimha.sdk.GetPlansOptions;
import com.qasimha.sdk.model.Plan;
import com.qasimha.sdk.model.Plans;

Plans quote = client.checkout().getPlans(new BigDecimal("1200.00"),
    GetPlansOptions.defaults().language("ar"));

if (quote.isOffered()) {          // false outside [minAmount, maxAmount] or with no active policy
  for (Plan plan : quote.getPlans()) {
    render(plan);                 // getInstalments(), getInstalmentAmount(), getFeesIncludingVat(), getTotalPayable()
  }
}

Without a publishable key on the client or in GetPlansOptions, the call throws ConfigurationException before any request. The platform caches a quote for one minute. See Plans widget.

Errors

Every failure is an unchecked QasimhaException with getCode(), getHttpStatus(), getRequestId(), getErrors() (field errors) and getRawEnvelope().

ExceptionWhen
ConfigurationExceptionClient misconfigured, or an invalid argument before sending
ValidationExceptionAny 400, or a client-side basket, amount or endpoint check (isClientSide())
AuthenticationException401: missing, invalid or revoked key (API key, or publishable key on getPlans)
PermissionException403: the key lacks the scope
NotFoundException404
DuplicateOrderReferenceException409 DUPLICATE_ORDER_REFERENCE; getExistingOrderId(), getExistingPaymentUrl()
ConflictExceptionAny other 409 (MERCHANT_SUSPENDED, NOT_OPEN, REQUEST_IN_PROGRESS)
OrderDeclinedException422 ORDER_DECLINED; getOrderId()
RateLimitException429 after retries; getRetryAfterSeconds()
ServerException5xx
ApiExceptionAnything else, including a 3xx or a 2xx with success: false
ConnectionExceptionDNS, TLS, socket or timeout failure (cause attached)
WebhookSignatureExceptionWebhook verification failed; see getReason()

Retries: connection failures, 429, 502, 503, 504 and 409 REQUEST_IN_PROGRESS, up to maxRetries. Every POST, PUT and DELETE carries a UUID v4 Idempotency-Key; RequestOptions.withIdempotencyKey(...) supplies your own. See Errors and Idempotency.

Logging and security

QasimhaClient.builder()
    .apiKey(apiKey)
    .logger((level, message, fields) -> log.atLevel(level.name()).log("{} {}", message, fields))
    .build();

Fields are method, path, status, durationMs, requestId, attempt, code and, on a retry, delayMs. Nothing is logged unless you pass a logger.

  • Keep the secret key and webhook secret in a secrets manager. Never send them to a browser or mobile app.
  • The SDK never logs keys, webhook secrets, query strings, order references in a by-reference lookup, or customer data. client.maskedKey() returns sk_live_…AbCd, and toString() shows masked keys only.
  • TLS only. Certificate checks cannot be disabled, and redirects are never followed.
  • Proxies use the JDK's standard settings (https.proxyHost / https.proxyPort, or ProxySelector.getDefault()).
  • No card data passes through your server; customers pay on the hosted checkout, so you stay in PCI DSS SAQ-A scope.

Next steps


Did this page help you?