Cost+Docs

Java / Kotlin

Official Kotlin/Java SDK for the Cost+ payment gateway

Official Kotlin SDK for the Cost+ payment gateway, fully interoperable with Java. Simplifies the HPP (Hosted Payment Page) redirect flow, HMAC payload signing, and webhook verification.

Features

  • Kotlin-first, Java-friendly — data classes, coroutines, null safety; fully usable from Java
  • kotlinx.serialization — no Gson/Jackson dependency; automatic snake_case/camelCase mapping
  • HMAC-SHA256 signature generation and constant-time verification
  • Webhook parsing + API-based order verification
  • Suspend functions for non-blocking IO via Kotlin coroutines
  • Injectable HttpClient for easy testing with mock transports
  • Targets Java 17+

Requirements

Installation

Gradle (Kotlin DSL)

dependencies {
    implementation("io.nopayn:nopayn-sdk:1.0.0")
}

Gradle (Groovy)

dependencies {
    implementation 'io.nopayn:nopayn-sdk:1.0.0'
}

Maven

<dependency>
    <groupId>io.nopayn</groupId>
    <artifactId>nopayn-sdk</artifactId>
    <version>1.0.0</version>
</dependency>

Quick Start (Kotlin)

1. Initialise the Client

import io.nopayn.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val nopayn = NoPaynClient(
        NoPaynConfig(
            apiKey = "your-api-key",
            merchantId = "your-project",
        )
    )
}

2. Create a Payment and Redirect to the HPP

val result = nopayn.generatePaymentUrl(
    CreateOrderParams(
        amount = 1295,            // €12.95 in cents
        currency = "EUR",
        merchantOrderId = "ORDER-001",
        description = "Premium Widget",
        returnUrl = "https://shop.example.com/success",
        failureUrl = "https://shop.example.com/failure",
        webhookUrl = "https://shop.example.com/webhook",
        locale = "en-GB",
        expirationPeriod = "PT30M",
    )
)

println(result.orderUrl)    // HPP URL
println(result.paymentUrl)  // Direct payment method URL
println(result.signature)   // HMAC-SHA256 signature

3. Handle the Webhook

// In your HTTP handler (Ktor, Spring, etc.)
val rawBody: String = request.body()
val verified = nopayn.verifyWebhook(rawBody)

println(verified.order.status)  // "completed", "cancelled", etc.
println(verified.isFinal)       // true when the order won't change

if (verified.order.status == "completed") {
    // Fulfil the order
}

Quick Start (Java)

import io.nopayn.*;
import kotlinx.coroutines.BuildersKt;
import kotlinx.coroutines.Dispatchers;

public class Example {
    public static void main(String[] args) throws Exception {
        NoPaynClient client = new NoPaynClient(
            new NoPaynConfig("your-api-key", "your-project", "https://api.nopayn.co.uk")
        );

        Order order = BuildersKt.runBlocking(
            Dispatchers.getIO(),
            (scope, continuation) -> client.createOrder(
                new CreateOrderParams(
                    1295, "EUR", "ORDER-001", "Premium Widget",
                    "https://shop.example.com/success",
                    "https://shop.example.com/failure",
                    "https://shop.example.com/webhook",
                    "en-GB", null, "PT30M"
                ),
                continuation
            )
        );

        System.out.println("Order ID: " + order.getId());
        System.out.println("Order URL: " + order.getOrderUrl());

        // Signature utilities work synchronously
        String sig = client.generateSignature(1295, "EUR", order.getId());
        boolean valid = client.verifySignature(1295, "EUR", order.getId(), sig);
    }
}

API Reference

NoPaynClient(config, httpClient?)

ParameterTypeRequiredDefault
config.apiKeyStringYes
config.merchantIdStringYes
config.baseUrlStringNohttps://api.nopayn.co.uk
httpClientjava.net.http.HttpClientNoDefault client

client.createOrder(params): Order (suspend)

Creates an order via POST /v1/orders/.

ParameterTypeRequiredDescription
amountIntYesAmount in smallest currency unit (cents)
currencyStringYesISO 4217 code (EUR, GBP, USD, NOK, SEK)
merchantOrderIdString?NoYour internal order reference
descriptionString?NoOrder description
returnUrlString?NoRedirect after successful payment
failureUrlString?NoRedirect on cancel/expiry/error
webhookUrlString?NoAsync status-change notifications
localeString?NoHPP language (en-GB, de-DE, nl-NL, etc.)
paymentMethodsList<String>?NoFilter HPP methods
expirationPeriodString?NoISO 8601 duration (PT30M)

Available payment methods: credit-card, apple-pay, google-pay, vipps-mobilepay

client.getOrder(orderId): Order (suspend)

Fetches order details via GET /v1/orders/{id}/.

client.createRefund(orderId, amount, description?): Refund (suspend)

Issues a full or partial refund via POST /v1/orders/{id}/refunds/.

client.generatePaymentUrl(params): PaymentUrlResult (suspend)

Convenience method that creates an order and returns:

PaymentUrlResult(
    orderId: String,     // NoPayn order UUID
    orderUrl: String,    // HPP URL
    paymentUrl: String?, // Direct payment URL (first transaction)
    signature: String,   // HMAC-SHA256 of amount:currency:orderId
    order: Order,        // Full order object
)

client.generateSignature(amount, currency, orderId): String

Generates an HMAC-SHA256 hex signature. The canonical message is $amount:$currency:$orderId, signed with the API key.

client.verifySignature(amount, currency, orderId, signature): Boolean

Constant-time verification of an HMAC-SHA256 signature.

client.verifyWebhook(rawBody): VerifiedWebhook (suspend)

Parses the webhook body, then calls GET /v1/orders/{id}/ to verify the actual status.

Standalone HMAC Utilities

import io.nopayn.NoPaynSignature

val sig = NoPaynSignature.generate("your-api-key", 1295, "EUR", "order-uuid")
val ok  = NoPaynSignature.verify("your-api-key", 1295, "EUR", "order-uuid", sig)

From Java:

String sig = NoPaynSignature.generate("your-api-key", 1295, "EUR", "order-uuid");
boolean ok = NoPaynSignature.verify("your-api-key", 1295, "EUR", "order-uuid", sig);

Error Handling

import io.nopayn.*

try {
    nopayn.createOrder(CreateOrderParams(amount = 100, currency = "EUR"))
} catch (e: ApiException) {
    println(e.statusCode)  // 401, 400, etc.
    println(e.errorBody)   // Raw API error response
} catch (e: NoPaynException) {
    println(e.message)     // Network or parsing error
}

Order Statuses

StatusFinal?Description
newNoOrder created
processingNoPayment in progress
completedYesPayment successful — deliver the goods
cancelledYesPayment cancelled by customer
expiredYesPayment link timed out
errorYesTechnical failure

Webhook Best Practices

  1. Always verify via the API — the webhook payload only contains the order ID, never the status. The SDK's verifyWebhook() does this automatically.
  2. Return HTTP 200 to acknowledge receipt. Any other code triggers up to 10 retries (2 minutes apart).
  3. Implement a backup poller — for orders older than 10 minutes that haven't reached a final status, poll getOrder() as a safety net.
  4. Be idempotent — you may receive the same webhook multiple times.

Test Cards

Use these cards in Cost+ test mode (sandbox website):

CardNumberNotes
Visa (success)4111 1111 1111 1111Any CVV
Mastercard (success)5544 3300 0003 7Any CVV
Visa (declined)4111 1111 1111 1105Do Not Honor
Visa (insufficient funds)4111 1111 1111 1151Insufficient Funds

Use any future expiry date and any 3-digit CVC.

Demo App

A Docker-based demo app (Ktor) is included in the GitHub repository for testing the full payment flow.

Support

Need help? Reach out to our support team at support@costplus.io.

On this page