KashuDocs

Quickstart

In this guide you take a test payment from start to finish. You make a checkout session on your server, pay it in a local test page with Kashu's card form, read the payment back and refund it. Everything runs in test mode, so no real card is charged.

Before you start

You need three things:

Put the secret key in your shell, so the commands below can read it:

export KASHU_SECRET_KEY="kashu_sk_test_..."

Check that the key works:

curl https://api.kashupay.com/v1/account \
  -H "Authorization: Bearer $KASHU_SECRET_KEY"

You see your account, with livemode false:

{
  "object": "account",
  "business_name": "Example Shop",
  "livemode": false,
  "api_version": "2026-11-06",
  "default_store": "st_..."
}

Make a publishable key for your page

The card form runs in the buyer's browser with a publishable key. A publishable key works only on the origins it lists, and a test key may list a local page:

curl https://api.kashupay.com/v1/api_keys \
  -H "Authorization: Bearer $KASHU_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"publishable","allowed_origins":["http://localhost:8000"],"label":"Quickstart page"}'

Copy token from the response. It starts kashu_pk_test_.

Make a checkout session

A checkout session is one amount to collect. Make one for 49.99 USD:

curl https://api.kashupay.com/v1/checkout_sessions \
  -H "Authorization: Bearer $KASHU_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":4999,"currency":"USD","success_url":"http://localhost:8000/thanks.html","metadata":{"order":"1001"}}'

Amounts are in the currency's minor unit, so 4999 is 49.99 USD. The response holds the session's id, which starts cs_, and its client_secret. Copy both. The client secret appears only in this response.

Pay it in a test page

Make a folder with two files. The first, index.html, puts the card form on the page for the session named in the page's address. Replace kashu_pk_test_... with your publishable key:

<!doctype html>
<title>Pay order 1001</title>
<div id="card"></div>
<script src="https://api.kashupay.com/v1/kashu.js"></script>
<script>
  var kashu = Kashu("kashu_pk_test_...");
  var clientSecret = new URLSearchParams(location.search).get("client_secret");
  kashu.mount("#card", { clientSecret: clientSecret });
</script>

The second, thanks.html, is where the buyer lands once the session is paid:

<!doctype html>
<title>Thank you</title>
<p>Paid. Thank you for your order.</p>

Serve the folder:

python3 -m http.server 8000

Open http://localhost:8000/?client_secret= followed by the session's client secret. In the form, type the card number 4242 4242 4242 4242, a future expiry date and any three digits for the CVC. The session names no customer, so the form also asks for an email. Pay. Test mode approves every card, and Kashu.js takes you to thanks.html.

See the payment

Read the session back. Its status is now complete, and payment names the payment:

curl https://api.kashupay.com/v1/checkout_sessions/cs_... \
  -H "Authorization: Bearer $KASHU_SECRET_KEY"

Then read the payment:

curl https://api.kashupay.com/v1/payments/py_... \
  -H "Authorization: Bearer $KASHU_SECRET_KEY"
{
  "id": "py_...",
  "object": "payment",
  "amount": 4999,
  "amount_captured": 4999,
  "amount_refunded": 0,
  "currency": "USD",
  "status": "succeeded",
  "livemode": false,
  "...": "..."
}

Refund it

Refund the payment in full:

curl https://api.kashupay.com/v1/refunds \
  -H "Authorization: Bearer $KASHU_SECRET_KEY" \
  -H "Idempotency-Key: quickstart-refund-1" \
  -H "Content-Type: application/json" \
  -d '{"payment":"py_..."}'

Without an amount, a refund is for all that is left of the payment. The refund's status is succeeded, or pending while the card network answers.

A refund needs the Idempotency-Key header, because it moves money. If the request fails on the way, send it again with the same key. You get the same refund back, never a second one. Idempotency explains the rules.

Call the API from Node

The Node examples in these docs use this helper. It needs Node 18 or later and no packages. Save it with the lines after it as quickstart.mjs and run node quickstart.mjs:

async function kashu(method, path, body, { idempotencyKey } = {}) {
  const headers = { Authorization: `Bearer ${process.env.KASHU_SECRET_KEY}` };
  if (body !== undefined) headers["Content-Type"] = "application/json";
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
  const res = await fetch(`https://api.kashupay.com${path}`, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.error.message), json.error);
  return json;
}

const session = await kashu("POST", "/v1/checkout_sessions", {
  amount: 4999,
  currency: "USD",
  success_url: "http://localhost:8000/thanks.html",
});
console.log(session.id, session.status);

A request Kashu refuses throws an error that carries the API's code, param and request_id, as Errors describes.

Next steps