PayKernel is a TypeScript payment orchestration toolkit. You compose @paykernel/core (create / capture / refund / verify) with optional inbox, reconciliation, routing, stores, and HTTP adapters at the application layer. Core never depends on those packages.
All packages are published to npm as @paykernel/* (core 1.0.0, gateway-tap/gateway-myfatoorah 1.0.2, testkit 1.0.2, gateway-hesabe 0.1.1, store-d1 0.1.2, integration-http 0.1.2, other engines/stores/frameworks 0.1.x, contracts/sql-foundation 0.1.0). Install with bun add @paykernel/core or npm install @paykernel/core. Source: aashahin/paykernel.
Tested with provider sandbox accounts
The deployed payment lab completed Paymob payments and full/partial refunds and a Moyasar card payment with 3-D Secure against real provider sandbox accounts on 2026-09-11. Authenticated provider inquiries confirmed the app’s settlement totals. Stripe and PayPal also passed authentication/configuration checks.
The gateway validation matrix separates completed account flows, configuration checks, and automated simulation results for all seven gateways. It also records the remaining gaps; these results do not claim production-account testing.
Quickstart
Create a first Moyasar payment with createPaymentClient, moyasarGateway, money(), and isPaidOutcome — including declined and indeterminate branches.
Guides
Getting started (inbox + store + reconcile), outcomes, webhooks, stores, best practices.
Packages
Full map on Packages. Built-in gateways live in core; Tap, MyFatoorah, and Hesabe are extra packages, not BuiltInGatewayName.
Examples
Private hosts under Examples (never published). POST /internal/* test hooks require enableTestHooks and must not be deployed.
Honesty
- Folder
packages/observabilitypublishes as@paykernel/opentelemetry. Do not invent an npm name from the folder. @paykernel/internal-sql-store(internal/sql-store) is private. It is a BC shim over@paykernel/sql-foundation. Do not add it to an app.- Redis is optional. Local SQLite is single-host.
:memory:is one process. Turso is remote multi-host and has no/syncexport. D1 ≠ Durable Objects ≠ Turso ≠ local SQLite. Memory stores are NON-PRODUCTION. - No published adapter declares
coordinationScope: "multi-region". handleWebhookverifies and normalizes. It does not claim, lease, or set HTTP status.- Never fulfill in
onWebhookVerified. Fulfill after an inbox claim, and only when the rematched event ispayment.succeededorcapture.completedandpayment.status === "paid", bound togatewayPaymentId. - 1.0 removed
successfromGatewayPaymentResult. UseisPaidOutcome/outcome.authorized,approved, and pending are not paid. - After
outcome === "indeterminate"orreconciliationRequired, do notcreatePaymentagain. Lookup +decideReconciliationPolicyonly. - Never auto-route a second gateway after timeout / indeterminate / uncertain 5xx.
- HTTP status codes live in
@paykernel/integration-http(mapInboxOutcome), not in@paykernel/webhooks. - Example
POST /internal/*routes are unauthenticated test hooks (enableTestHooks) and must not be deployed.
If markdown and implementation disagree, code wins.
Package map
All names below are the package.json "name" fields. Extra Tap / MyFatoorah / Hesabe adapters depend only on @paykernel/core and are not BuiltInGatewayName ("moyasar" | "paypal" | "paymob" | "stripe").
Core and engines
| Package | Role |
|---|---|
@paykernel/core |
Unified SDK: client, money, outcomes, hooks, built-in Moyasar / PayPal / Paymob / Stripe |
@paykernel/webhooks |
Portable webhook inbox (claim, lease fencing, modes). No HTTP status codes |
@paykernel/reconciliation |
Safe lookup, drift, decision-only policy, store-backed schedule. No mandatory queue |
@paykernel/routing |
Select-only gateway routing. Select-time fallback is not post-attempt recovery |
@paykernel/opentelemetry |
Metrics, spans, redacting telemetry, optional OTEL bridge (./otel). No hard OTEL in core |
Extra gateways
| Package | Role |
|---|---|
@paykernel/gateway-tap |
Tap Payments adapter. Not a core built-in |
@paykernel/gateway-myfatoorah |
MyFatoorah adapter. Not a core built-in |
@paykernel/gateway-hesabe |
Hesabe adapter (KWD). Not a core built-in |
Built-ins: Moyasar, PayPal, Paymob, Stripe, custom plugins.
Stores
| Package | Role |
|---|---|
@paykernel/store-contracts |
Lease-aware contracts, StoreError, manifests |
@paykernel/sql-foundation |
Relational schemas, migrations, claim SQL templates |
@paykernel/store-postgres |
PostgreSQL durable stores; multi-host claims |
@paykernel/store-redis |
Optional Redis / Valkey / Upstash. Redis is never required |
@paykernel/store-sqlite |
Single-host SQLite (Bun / Node / better-sqlite3). Not multi-host |
@paykernel/store-turso |
Remote multi-host Turso / libSQL. Not local SQLite. No /sync |
@paykernel/store-d1 |
Multi-host Cloudflare D1. Not SQLite, not Turso, not Durable Objects |
@paykernel/store-durable-objects |
Multi-host partitioned SQLite-backed DOs. Not D1. Never one global DO |
Importing a store package does not apply DDL. Migrate explicitly. Which store: adapter selection.
Integrations and tests
| Package | Role |
|---|---|
@paykernel/integration-http |
mapInboxOutcome, processWebhookHttp (raw-body-safe) |
@paykernel/integration-hono |
Thin Hono adapter over HTTP helpers |
@paykernel/integration-elysia |
Thin Elysia adapter (parse: "none") |
@paykernel/integration-express |
Thin Express adapter (expressRawJson) |
@paykernel/integration-cloudflare-workers |
Thin Workers adapter |
@paykernel/testkit |
Mock gateway, conformance, NON-PRODUCTION memory stores |
Webhook routes must read the raw body (text()), never json(), before verify. See HTTP integration.
Not published: @paykernel/internal-sql-store (private BC shim), everything under examples/*, the monorepo root, and this docs app (apps/docs).
First payment
import { createPaymentClient, money, isPaidOutcome, moyasarGateway } from "@paykernel/core";
const client = createPaymentClient({
gateways: {
moyasar: moyasarGateway({
secretKey: process.env.MOYASAR_SECRET_KEY!,
webhookSecret: process.env.MOYASAR_WEBHOOK_SECRET,
}),
},
defaultGateway: "moyasar",
});
const result = await client.createPayment({
amount: money("100.00", "SAR"),
currency: "SAR",
orderId: "order_123",
callbackUrl: "https://example.com/callback",
moyasarSource: { type: "token", token: "token_xxx" },
});
if (isPaidOutcome(result)) {
// Paid-like settlement only (`outcome === "succeeded"` and status `paid`).
} else if (result.outcome === "indeterminate" || result.reconciliationRequired) {
// Charge may already exist. Schedule reconcile. Do not createPayment again.
} else {
// declined / pending / authorized — see /guides/outcomes
}Full branches (declined, requires_action, thrown declines): Quickstart. Production inbox + store + reconcile: Getting started.