API Reference · Payins
Create a payin
Start a collection and get a checkout link. Klustar does not redirect the customer for you: read data.checkout_url from the response and send them there straight away, because the link is not stored and cannot be fetched again. Call this from your server, not a browser. Sign every request with HMAC-SHA256. The signed fields are amount, currency, customerEmail, customerName, gateway, narration, notifyUrl, paymentMethod, redirectUrl and reference; any other JSON field is ignored and must not be signed. If no provider verdict has arrived 10 minutes after creation, the pay-in closes as TIMED_OUT and is not checked again, even if the customer pays later.
https://payment.klustar.io/payment/api/payinsHeaders
X-Tenant-IDThe environment of this call: test or live, ignoring case and surrounding spaces. It is not an account identifier, nothing issues it, and it is not signed. On Create a payin and Query payin status a missing or mistyped value means test, with no error. Account enquiry and Query payout status refuse a missing value and treat a mistyped one as test. live needs approval: until then a correctly signed live request gets 403 LIVE_NOT_ENABLED.
X-API-KEYPublic API key from the merchant dashboard. The same key serves test and live.
X-Merchant-IDYour App ID, shown beside your API key on Profile & Settings → API Keys.
X-NonceA new value on every request (UUID recommended), including a retry after a failure. A nonce is recorded before the signature is checked and stays used, so a refused request burns it.
X-TimestampUnix epoch seconds when the request is signed, digits only. Must be within ±300s of server time.
X-SignatureLowercase hex HMAC-SHA256 of the signature base string, using your secret key.
Content-TypeMust be application/json. Any other type gets a 502.
Defaults to application/json
X-API-VersionContract version: 1 (legacy, no paymentMethod) or 2 (paymentMethod required and signed). Optional, and there is no default - when omitted the version is inferred from whether paymentMethod is present, so sending version 1 alongside paymentMethod is rejected with 400.
Body params
referenceYour reference for this attempt, echoed in webhooks. Generate a new one for every attempt, for example ORDER-<UUID>. Use only A-Za-z0-9_-, and 5 to 50 characters: Korapay refuses anything else without naming the field, and that uses the reference up. It must not start with KLU-. A reference you have used before in the same environment is refused with 409 DUPLICATE_REFERENCE: failed attempts count, and references differing only in case count as the same. Do not reuse one between test and live or copy one from a sample: Korapay also refuses a reference any other Klustar merchant has used, and a shared reference can stop a result being applied.
amountAmount in major units (naira, not kobo), sent as a JSON string of plain digits with at most 2 decimal places and no leading zeros, such as "2000" or "2000.50". It is signed as that exact text, so "2000.50" and "2000.5" sign differently, though "02000" signs as 2000 and fails. A JSON number also binds, but your language may format it differently from what you signed. In test mode Korapay refuses card amounts outside NGN 100 to 1,000,000: below NGN 100 with 409, and above it, at 0 or below 0 with 422.
currencyISO currency code. Currently NGN.
customerNameCustomer full name, as a string. Send it on every pay-in: Korapay, which handles every sandbox pay-in, refuses a pay-in without one.
customerEmailCustomer email address. Send it on every pay-in: Korapay, which handles every sandbox pay-in, refuses a pay-in without one.
redirectUrlWhere the customer returns after payment. Must be https, at most 1000 characters, with a host and no credentials in the URL. Send it on every pay-in: a live bank transfer through Hydrogen is refused without it.
notifyUrlLeave this out. It does not set your webhook: Klustar sends webhooks only to the URL saved in Profile & Settings → API Keys. If you do send it, it must be signed, and a live provider may treat it as the customer return address.
gatewayLeave this out unless told otherwise. Pins the provider: KORAPAY, PALMPAY or HYDROGENPAY. It overrides paymentMethod routing and removes failover. Under test, PALMPAY and HYDROGENPAY are refused with HTTP 200 and PALMPAY_SANDBOX_DISABLED or HYDROGEN_SANDBOX_DISABLED, and the pay-in closes FAILED.
paymentMethodCARD or BANK_TRANSFER, case-sensitive. Send it on every pay-in, and include it in the signed canonical body. Under test, every pay-in without a gateway goes to Korapay, showing that method at checkout. Under live, CARD goes to Korapay and BANK_TRANSFER to Hydrogen or PalmPay with failover, unless your business has a saved bank-transfer provider (Profile & Settings → Settlement), which pins that provider and removes failover even with no gateway. Without paymentMethod the provider is chosen by a legacy rotation with no failover.
narrationShort description shown on the payment flow, at most 255 bytes. Korapay refuses =, % and +, and that uses the reference up. Use letters, digits, spaces and simple punctuation.
Request body
{
"reference": "ORDER-<new UUID for every attempt>",
"amount": "2000",
"currency": "NGN",
"customerName": "Jane Doe",
"customerEmail": "jane@example.com",
"redirectUrl": "https://merchant.example/return",
"paymentMethod": "CARD",
"narration": "Order #1042"
}Responses
Send the customer to data.checkout_url. This is Korapay, which handles every card pay-in and every sandbox pay-in. The response is the provider's own, so under live a BANK_TRANSFER pay-in differs: PalmPay answers success and checkoutUrl, and Hydrogen answers data.url with virtual account details.
{
"status": true,
"message": "Charge created successfully",
"data": {
"reference": "ORDER-8f14e45f-ceea-467d-a5b3-0c1f7b2d9e11",
"checkout_url": "https://test-checkout.korapay.com/KPY-PI-.../pay"
}
}{
"status": true,
"message": "Charge created successfully",
"data": {
"reference": "ORDER-8f14e45f-ceea-467d-a5b3-0c1f7b2d9e11",
"checkout_url": "https://test-checkout.korapay.com/KPY-PI-.../pay"
}
}