Skip to content

Kashier

Terminal window
npm i @sarrafjs/kashier

Building an app? Start with the step-by-step guide: Accept payments with Kashier (Express and Next.js).

Option Used for
merchantId Your MID (MID-xxx-xxx)
apiKey Payment API key: signs the hosted-page hash and verifies redirect and webhook signatures
secretKey Server APIs: order lookup, refunds, Payment Sessions (beta)
mode "test" or "live". Test and live MIDs and keys are different
amountFormat "fixed" (250.00, default) or "trimmed" (250)
currencies Currencies your MID accepts. Default ["EGP", "USD"]
urls Override the checkout, api and fep base URLs (proxies, tests). Defaults are in KASHIER_URLS
Feature Without secretKey With secretKey
Hosted checkout, webhooks, redirect verification ✅ supported ✅ supported
payments.get / reconcile, refunds, voids unsupported ✅ supported
Methods card, wallet ✅ supported ✅ supported
Methods installments, bnpl (valU) beta beta
Payment Sessions (flow: "session") unsupported beta
Capture (payments.capture) unsupported unsupported

Beta features need new Sarraf({ allowBeta: true, … }).

Sarraf builds a signed URL to https://checkout.kashier.io/:

hash = HMAC-SHA256(apiKey, "/?payment=" + merchantId + "." + orderId + "." + amount + "." + currency)

✅ Confirmed against the Kashier sandbox: a checkout signed this way was paid successfully. The hosted page does not check the hash when it loads, only when the customer presses Pay, so a wrong hash shows up as a failed payment, not a broken page.

The same amount string goes into the hash and the URL. One mismatched character gives “invalid hash” on the hosted page. orderId (your reference) must be unique per attempt.

methods maps to allowedMethods: card, wallet, installments → bank_installments, bnpl → valu (the last two are beta).

Input Sent to Kashier as
reference orderId (it comes back as merchantOrderId in webhooks). Unique per attempt
returnUrl / webhookUrl merchantRedirect / serverWebhook
locale display (en by default, or ar)
metadata metaData (JSON)
providerOptions.allowedMethods Raw allowedMethods, overriding methods
providerOptions.extraParams Extra query params, e.g. brandColor
providerOptions.customerReference customerReference, also appended to the hash (saved cards). ⚠️ unconfirmed

checkout.payment.providerPaymentId is your reference at checkout time. Kashier’s own order id arrives in the webhook (kashierOrderId) and is what refunds use.

await pay.checkout.create({ provider: "kashier", /* … */ providerOptions: { flow: "session" } });

This uses POST /v3/payment/sessions with both the Authorization: <secretKey> and api-key: <apiKey> headers.

Kashier POSTs { event, data } with the signature in the x-kashier-signature header.

  1. Take data.signatureKeys and sort them.
  2. Pick those keys from data.
  3. Serialize exactly like the query-string library’s stringify: strict RFC 3986 encoding (* → %2A, ( → %28, …), arrays as repeated keys, and null as a bare key.
  4. HMAC-SHA256 with the Payment API key, hex.

✅ Confirmed against a real sandbox webhook (fixture). Kashier signs amount, channel, currency, kashierOrderId, merchantOrderId, method, orderReference, status, transactionId, transactionResponseCode; channel is online | e-commerce, so the signed string contains channel=online%20%7C%20e-commerce. amount arrives as a JSON number in major units (10) and is signed as-is.

Plain encodeURIComponent does not encode * ( ) ' !, and that is the #1 reported integration bug. Sarraf uses query-string itself, and its fixtures cover special characters and Arabic text.

merchantRedirect gets query parameters plus signature. Sarraf signs every parameter except signature and mode, in the order received, as key=value pairs joined by &, using HMAC-SHA256 with the API key. Use this for the thank-you page only.

✅ Confirmed against a real sandbox redirect (fixture). Kashier sends paymentStatus, cardDataToken, maskedCard, merchantOrderId, orderId, cardBrand, orderReference, transactionId, amount, currency, mode, signature. Note that amount comes back trimmed (10) even when the checkout sent 10.00.

Kashier Sarraf
pay + SUCCESS succeeded ✅
PENDING pending
FAILURE / FAILED failed
authorize + SUCCESS requires_action
refund + SUCCESS partially_refunded / refunded ✅ (totals from the order lookup)
void + SUCCESS canceled

✅ = confirmed with a recorded sandbox fixture. The other rows still need recorded failure, authorize/capture and void events.

Needs secretKey. Look payments up by your reference:

const payment = await pay.payments.get({ provider: "kashier", reference: "order_8841" });

✅ Confirmed in the sandbox: GET https://test-api.kashier.io/v3/payment/orders?search=<merchantOrderId>. Kashier’s search is a partial match on your reference, so Sarraf picks the order whose merchantOrderId matches exactly. It does not accept Kashier’s own order id, so payments.get({ providerPaymentId }) is rejected with INVALID_REQUEST.

A refunded order stays CAPTURED at Kashier; Sarraf derives partially_refunded / refunded from totalRefundedAmount against the order amount.

Need secretKey. providerPaymentId is the Kashier order id (kashierOrderId in webhooks, the redirect’s orderId), not your reference.

// Partial refund
await pay.refunds.create({ provider: "kashier", providerPaymentId, amount: { value: 300, currency: "EGP" } });
// Full refund of whatever is left: pass your reference too
await pay.refunds.create({ provider: "kashier", providerPaymentId, reference: "order_8841" });

✅ Partial and full refunds confirmed in the sandbox: PUT /v3/orders/:orderId with { apiOperation: "REFUND", reason, transaction: { amount } } and the secret key in Authorization. The refund outcome is response.result; response.status is the order’s new state (PARTIALLY_REFUNDED / REFUNDED).

Kashier’s docs call amount optional, but the sandbox answers HTTP 500 to a refund without one, and nothing is refunded. This was checked on fresh, never-refunded orders with all three documented shapes: transaction: {}, no transaction at all, and transaction: { targetTransactionId } pointing at the pay transaction. So for a full refund Sarraf looks the order up by reference and sends the remaining amount. Without reference it throws INVALID_REQUEST instead of guessing.

✅ Confirmed in the sandbox: Kashier’s refund webhook carries only this refund’s amount, not the order total or earlier refunds. With secretKey configured, Sarraf looks the order up when a refund webhook arrives, so event.payment has the real totals (partially_refunded, amountRefunded). Without secretKey it logs a warning, and event.payment describes only the refund.

Needs secretKey. Cancels a payment before Kashier settles it, which is the same day only; after that, refund instead.

const payment = await pay.payments.cancel({ provider: "kashier", reference: "order_8841" });
// payment.status === "canceled"

✅ Confirmed in the sandbox (fixture). Sarraf looks the order up by reference, finds the successful pay transaction and sends PUT /v3/orders/:orderId with { apiOperation: "VOID", transaction: { targetTransactionId, amount } }. Kashier’s docs say a full void can omit transaction; Sarraf always sends both fields, because the same “optional” fields make refunds fail with HTTP 500. It then returns the order as Kashier reports it (CANCELLED → canceled). Kashier also sends a void webhook, which becomes payment.canceled.

A void that Kashier does not accept throws PAYMENT_DECLINED.

@sarrafjs/kashier/testing builds Kashier payloads signed the way Kashier signs them:

import { kashierWebhookData, signKashierRedirect, signKashierWebhook } from "@sarrafjs/kashier/testing";
const webhook = signKashierWebhook("pay", kashierWebhookData({ merchantOrderId: "order_8841" }), apiKey);
await pay.webhooks.verifyAndParse("kashier", webhook); // → payment.succeeded
const query = signKashierRedirect({ paymentStatus: "SUCCESS", merchantOrderId: "order_8841" }, apiKey);
await pay.redirects.verify("kashier", query); // → authenticated: true
  • Authorize then capture (capture: "unsupported"). Kashier’s authorize webhook maps to requires_action, but there is no capture call.
  • Kiosk / reference-code payments, payment links and invoices, subscriptions, and charging saved cards without the customer. Paymob covers those today.
  • The live base URLs come from Kashier’s docs and haven’t been confirmed with a live account. If Kashier gives you different ones, override them with urls.