@sarrafjs/core
v0.2.1
Published
One payment API for Egypt and the Middle East. Core client, models, money, errors, HTTP, idempotency and webhook router.
Maintainers
Readme
@sarrafjs/core
The core of Sarraf: one payment API for Egypt and the Middle East. Paymob, Kashier and Fawry behind one typed interface: one way to create a checkout, one webhook format, one status model, one error type.
📖 Docs: sarraf-docs.vercel.app · Quick start · Accept payments with Paymob · Accept payments with Kashier
npm i @sarrafjs/core @sarrafjs/paymob # and/or @sarrafjs/kashier, @sarrafjs/fawryQuick start
import { Sarraf, toMinor } from "@sarrafjs/core";
import { paymob, paymobIntegrationIdsFromEnv } from "@sarrafjs/paymob";
const pay = new Sarraf({
providers: {
paymob: paymob({
secretKey: process.env.PAYMOB_SECRET_KEY!,
publicKey: process.env.PAYMOB_PUBLIC_KEY!,
hmacSecret: process.env.PAYMOB_HMAC_SECRET!,
integrationIds: paymobIntegrationIdsFromEnv(process.env),
}),
},
});
// 1. Checkout
const checkout = await pay.checkout.create({
provider: "paymob",
amount: { value: toMinor("250.00", "EGP"), currency: "EGP" }, // 25000
reference: "order_8841",
customer: { phone: "+201000000000" },
returnUrl: "https://shop.example/thanks",
webhookUrl: "https://shop.example/api/webhooks/paymob",
});
if (checkout.kind === "redirect") redirectTo(checkout.redirectUrl!);
else showCode(checkout.referenceCode!); // Fawry / kiosk reference codes
// 2. Webhooks: the only place an order becomes paid
const event = await pay.webhooks.verifyAndParse("paymob", { rawBody, headers, query });
if (event.type === "payment.succeeded") await markPaid(event.payment.reference);Framework helpers do the webhook part for you: @sarrafjs/express, @sarrafjs/next, @sarrafjs/nestjs.
The client
| Call | Returns | |
|---|---|---|
| checkout.create(input) | Checkout | kind: "redirect" (redirectUrl) or "reference_code" (referenceCode) |
| payments.get({ provider, providerPaymentId \| reference }) | Payment | Current status from the provider |
| payments.reconcile(...) | Payment | Same as get; use it in scheduled jobs for orders still pending |
| payments.cancel(...) | Payment | Void a same-day payment / cancel an unpaid one |
| payments.capture({ provider, providerPaymentId, amount? }) | Payment | Capture an authorization (Paymob, beta) |
| refunds.create({ provider, providerPaymentId, amount?, reason?, idempotencyKey? }) | Refund | Omit amount for a full refund |
| webhooks.verifyAndParse(provider, req) | SarrafEvent | Throws WEBHOOK_SIGNATURE_INVALID |
| webhooks.inspect(provider, req) | WebhookInspection | The exact signed string, expected vs received; never throws on mismatch |
| redirects.verify(provider, query) | RedirectResult | For the thank-you page only |
| capabilities(provider) | Capabilities | What each provider supports: "supported", "beta" or "unsupported" |
| provider(id) | the adapter | Provider-only APIs, e.g. pay.provider("paymob").extras |
The client is typed by the providers you configure: provider: "fawry" doesn't compile unless Fawry is configured, and providerOptions is narrowed to that provider's options. Beta features throw CAPABILITY_NOT_SUPPORTED unless you pass new Sarraf({ allowBeta: true, … }).
What's inside
- Models:
Checkout,Payment,Refund,SarrafEvent(PaymentEvent | CardSavedEvent), with one status vocabulary for every provider. Narrow events withevent.type,isPaymentEventorassertPaymentEvent; name one withEventOf<"payment.succeeded"> - Money: integer minor units with string math.
toMinor/toMajorStringhandle 3-decimal KWD/BHD/OMR/JOD. A bare number doesn't compile (Money) - Errors:
SarrafErrorwith stablecodes,provider,providerCode,retryableanddocsUrl.TIMEOUT_UNKNOWN_OUTCOMEmarks money calls that timed out and may have happened (Errors) - HTTP: timeouts; retries only for safe calls (never for money-moving ones); redacted debug logs and hooks
- Idempotency: pass
idempotencyKeyto money calls.MemoryStoreby default,RedisStorefrom@sarrafjs/core/redisfor more than one server (bring your own ioredis / node-redis client) (Idempotency) - Webhooks: constant-time signature checks over the raw body, stable
event.idfor dedupe (Webhooks) - Conformance:
runConformanceTests()from@sarrafjs/core/conformance, for adapter authors
Server-side only: importing it in a browser throws, so secret keys can't leak into client bundles. Requires Node.js 22+.
Not affiliated with any payment provider. You use your own provider accounts; money flows customer → provider → you. Part of Sarraf. Apache-2.0.
