Getting started

Official SDKs

PHP and JavaScript/TypeScript libraries for creating payment requests and verifying webhooks.

The SDKs wrap the same HTTP API documented here: they create, retrieve and list payment requests, read your payment methods, send an Idempotency-Key when you pass one, turn error responses into exceptions, and verify webhook signatures so you never hand-roll the HMAC. Responses are the API's own JSON, field for field. Both have no dependencies and live in one open-source repository on GitHub.

PHP

PHP 8.1+ with the curl extension, on plain PHP, Laravel or WordPress alike. Published on Packagist.

composer require kasera/kasera-pay

Create a payment request

use Kasera\Pay\ApiException;
use Kasera\Pay\Client;

$kasera = new Client(getenv('KASERA_API_KEY')); // kp_test_... or kp_live_...

try {
    $tx = $kasera->createTransaction([
        'amount'      => 150000,         // whole rupiah
        'external_id' => 'order-1001',
        'checkout'    => new stdClass(), // hosted Kasera Pay Checkout
    ], 'order-1001');                    // Idempotency-Key
} catch (ApiException $e) {
    // $e->status, $e->errorCode (e.g. validation_failed), $e->fields, $e->requestId
    throw $e;
}

header('Location: ' . $tx['checkout_url']);

Verify a webhook

use Kasera\Pay\SignatureException;
use Kasera\Pay\Webhook;

try {
    $event = Webhook::constructEvent(
        file_get_contents('php://input'),            // the raw body
        $_SERVER['HTTP_KASERA_SIGNATURE_V1'] ?? '',
        getenv('KASERA_WEBHOOK_SECRET'),
    );
} catch (SignatureException) {
    http_response_code(400);
    exit;
}

if ($event['type'] === 'payment.paid') {
    // mark $event['data']['external_id'] paid; dedupe on $event['id']
}

JavaScript / TypeScript

Node 20.19+, Deno, Bun and edge runtimes, with TypeScript types generated from the API spec. Published on npm.

npm install kasera-pay

Create a payment request

import { KaseraPay, KaseraPayError } from "kasera-pay";

const kasera = new KaseraPay(process.env.KASERA_API_KEY!); // kp_test_... or kp_live_...

try {
  const tx = await kasera.createTransaction(
    { amount: 150000, external_id: "order-1001", checkout: {} },
    { idempotencyKey: "order-1001" },
  );
  // redirect the buyer to tx.checkout_url
} catch (e) {
  if (e instanceof KaseraPayError) {
    // e.status, e.code (e.g. validation_failed), e.fields, e.requestId
  }
  throw e;
}

Verify a webhook

import { constructWebhookEvent, SignatureError } from "kasera-pay";

// e.g. a Next.js route handler
export async function POST(req: Request) {
  try {
    const event = await constructWebhookEvent(
      await req.text(), // the raw body
      req.headers.get("kasera-signature-v1") ?? "",
      process.env.KASERA_WEBHOOK_SECRET!,
    );
    if (event.type === "payment.paid") {
      // mark event.data.external_id paid; dedupe on event.id
    }
    return new Response("ok");
  } catch (e) {
    if (e instanceof SignatureError) return new Response("bad signature", { status: 400 });
    throw e;
  }
}

Always pass an idempotency key

Your order id is a good key. It is the only thing that stops a retried create — a double click, a timeout, a queue redelivery — from becoming a second payment request: the same key with the same body returns the original, and the same key with a different body is refused with 409 idempotency_conflict. external_id alone does not deduplicate.

Test first

With a kp_test_ key every call works end to end and no real money moves; pay test requests with the simulate button on the checkout page. See Test mode. Going live is swapping the key.

Using another language? The API is plain HTTPS and JSON, and its OpenAPI spec is served at /v1/openapi.json for generating a client.