@siyegs/pay-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts","../src/providers/paystack.ts","../src/internal.ts","../src/providers/flutterwave.ts","../src/fallback.ts"],"sourcesContent":["export { createPayClient } from \"./client\";\nexport { createFallbackClient } from \"./fallback\";\nexport { PayKitError, isRetryableError } from \"./errors\";\nexport type { PayKitErrorCode, PayKitErrorOptions } from \"./errors\";\nexport type {\n Currency,\n FallbackClient,\n FallbackClientConfig,\n FallbackInitializeResult,\n FallbackProviderConfig,\n InitializeParams,\n InitializeResult,\n PayClient,\n PayClientConfig,\n PaymentProvider,\n PaymentStatus,\n ProviderName,\n RefundOptions,\n RefundResult,\n RefundStatus,\n VerifyResult,\n WebhookEvent,\n WebhookEventType,\n} from \"./types\";\n","import { randomUUID } from \"node:crypto\";\nimport { PayKitError } from \"./errors\";\nimport { createPaystackProvider } from \"./providers/paystack\";\nimport { createFlutterwaveProvider } from \"./providers/flutterwave\";\nimport type {\n PayClient,\n PayClientConfig,\n PaymentProvider,\n ProviderContext,\n} from \"./types\";\n\nfunction resolveProvider(config: PayClientConfig, ctx: ProviderContext): PaymentProvider {\n switch (config.provider) {\n case \"paystack\":\n return createPaystackProvider(ctx);\n case \"flutterwave\":\n return createFlutterwaveProvider(ctx);\n default:\n throw new PayKitError(`Unknown provider: ${String(config.provider)}`, {\n code: \"config_error\",\n });\n }\n}\n\n/**\n * Create a payment client bound to a single provider.\n *\n * @example\n * const pay = createPayClient({ provider: \"paystack\", secretKey: process.env.PAYSTACK_SECRET_KEY! });\n * const { authorizationUrl, reference } = await pay.initialize({ amount: 500000, email: \"a@b.com\" });\n */\nexport function createPayClient(config: PayClientConfig): PayClient {\n if (!config.secretKey) {\n throw new PayKitError(\"`secretKey` is required\", { code: \"config_error\" });\n }\n\n const fetchImpl = config.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new PayKitError(\n \"No fetch implementation found. Use Node >= 18 or pass `config.fetch`.\",\n { code: \"config_error\" },\n );\n }\n\n const ctx: ProviderContext = {\n secretKey: config.secretKey,\n webhookSecret: config.webhookSecret,\n baseUrl: config.baseUrl,\n fetch: fetchImpl,\n generateReference:\n config.generateReference ?? (() => `pk_${randomUUID().replace(/-/g, \"\")}`),\n };\n\n const provider = resolveProvider(config, ctx);\n\n return {\n provider: provider.name,\n initialize: (params) => provider.initialize(params),\n verify: (reference) => provider.verify(reference),\n refund: (reference, options) => provider.refund(reference, options),\n webhooks: {\n construct: (rawBody, signature) => provider.constructWebhookEvent(rawBody, signature),\n },\n };\n}\n","export type PayKitErrorCode =\n | \"provider_error\"\n | \"network_error\"\n | \"invalid_signature\"\n | \"config_error\"\n | \"verification_failed\";\n\nexport interface PayKitErrorOptions {\n code: PayKitErrorCode;\n provider?: string;\n statusCode?: number;\n raw?: unknown;\n cause?: unknown;\n}\n\n/**\n * Whether an error is worth retrying on another provider: network failures and\n * outage-like HTTP statuses (5xx, 429). Client errors (4xx) are not retryable -\n * they would fail the same way everywhere.\n */\nexport function isRetryableError(err: unknown): boolean {\n if (!(err instanceof PayKitError)) return false;\n if (err.code === \"network_error\") return true;\n if (typeof err.statusCode === \"number\") {\n return err.statusCode >= 500 || err.statusCode === 429;\n }\n return false;\n}\n\n/** Single error type surfaced by pay-kit, with a machine-readable `code`. */\nexport class PayKitError extends Error {\n readonly code: PayKitErrorCode;\n readonly provider?: string;\n readonly statusCode?: number;\n readonly raw?: unknown;\n\n constructor(message: string, options: PayKitErrorOptions) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = \"PayKitError\";\n this.code = options.code;\n this.provider = options.provider;\n this.statusCode = options.statusCode;\n this.raw = options.raw;\n }\n}\n","import { createHmac } from \"node:crypto\";\nimport { PayKitError } from \"../errors\";\nimport { providerRequest, safeEqual } from \"../internal\";\nimport type {\n InitializeParams,\n InitializeResult,\n PaymentProvider,\n PaymentStatus,\n ProviderContext,\n RefundOptions,\n RefundResult,\n RefundStatus,\n VerifyResult,\n WebhookEvent,\n WebhookEventType,\n} from \"../types\";\n\nconst PAYSTACK_BASE = \"https://api.paystack.co\";\n\n/** Paystack uses subunits (kobo) natively - matches pay-kit's canonical unit. */\nfunction mapStatus(raw: unknown): PaymentStatus {\n switch (raw) {\n case \"success\":\n return \"success\";\n case \"failed\":\n return \"failed\";\n case \"abandoned\":\n return \"abandoned\";\n default:\n return \"pending\";\n }\n}\n\nfunction mapEventType(event: unknown): WebhookEventType {\n return typeof event === \"string\" && event.length > 0 ? event : \"unknown\";\n}\n\nfunction mapRefundStatus(raw: unknown): RefundStatus {\n switch (raw) {\n case \"processed\":\n case \"success\":\n return \"processed\";\n case \"failed\":\n return \"failed\";\n default:\n return \"pending\";\n }\n}\n\nexport function createPaystackProvider(ctx: ProviderContext): PaymentProvider {\n const base = ctx.baseUrl ?? PAYSTACK_BASE;\n\n return {\n name: \"paystack\",\n\n async initialize(params: InitializeParams): Promise<InitializeResult> {\n const reference = params.reference ?? ctx.generateReference();\n const body = await providerRequest(ctx, \"paystack\", `${base}/transaction/initialize`, {\n method: \"POST\",\n body: JSON.stringify({\n amount: params.amount,\n email: params.email,\n currency: params.currency ?? \"NGN\",\n reference,\n callback_url: params.callbackUrl,\n metadata: params.metadata,\n }),\n });\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n return {\n reference: String(data.reference ?? reference),\n authorizationUrl: String(data.authorization_url ?? \"\"),\n accessCode: data.access_code ? String(data.access_code) : undefined,\n raw: body,\n };\n },\n\n async verify(reference: string): Promise<VerifyResult> {\n const body = await providerRequest(\n ctx,\n \"paystack\",\n `${base}/transaction/verify/${encodeURIComponent(reference)}`,\n { method: \"GET\" },\n );\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const customer = (data.customer ?? {}) as Record<string, unknown>;\n return {\n reference: String(data.reference ?? reference),\n status: mapStatus(data.status),\n amount: Number(data.amount ?? 0),\n currency: String(data.currency ?? \"\"),\n paidAt: data.paid_at ? String(data.paid_at) : undefined,\n channel: data.channel ? String(data.channel) : undefined,\n customer: { email: customer.email ? String(customer.email) : undefined },\n raw: body,\n };\n },\n\n async refund(reference: string, options?: RefundOptions): Promise<RefundResult> {\n const body = await providerRequest(ctx, \"paystack\", `${base}/refund`, {\n method: \"POST\",\n body: JSON.stringify({\n transaction: reference,\n ...(options?.amount !== undefined ? { amount: options.amount } : {}),\n }),\n });\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const transaction = (data.transaction ?? {}) as Record<string, unknown>;\n return {\n reference: String(transaction.reference ?? reference),\n status: mapRefundStatus(data.status),\n amount: data.amount !== undefined ? Number(data.amount) : undefined,\n raw: body,\n };\n },\n\n constructWebhookEvent(rawBody: string, signature: string): WebhookEvent {\n const expected = createHmac(\"sha512\", ctx.secretKey).update(rawBody).digest(\"hex\");\n if (!signature || !safeEqual(expected, signature)) {\n throw new PayKitError(\"Invalid Paystack webhook signature\", {\n code: \"invalid_signature\",\n provider: \"paystack\",\n });\n }\n\n let event: Record<string, unknown>;\n try {\n event = JSON.parse(rawBody) as Record<string, unknown>;\n } catch (err) {\n throw new PayKitError(\"Malformed Paystack webhook body\", {\n code: \"provider_error\",\n provider: \"paystack\",\n cause: err,\n });\n }\n\n const data = (event.data ?? {}) as Record<string, unknown>;\n return {\n type: mapEventType(event.event),\n reference: String(data.reference ?? \"\"),\n status: data.status ? mapStatus(data.status) : undefined,\n amount: data.amount !== undefined ? Number(data.amount) : undefined,\n currency: data.currency ? String(data.currency) : undefined,\n raw: event,\n };\n },\n };\n}\n","import { timingSafeEqual } from \"node:crypto\";\nimport { PayKitError } from \"./errors\";\nimport type { ProviderContext, ProviderName } from \"./types\";\n\n/**\n * Authenticated JSON request to a provider, with normalized error handling.\n * Both Paystack and Flutterwave use `Authorization: Bearer <secret>` and signal\n * application-level failure in the body (`status: false` / `status: \"error\"`),\n * so we treat those as errors even on HTTP 200.\n */\nexport async function providerRequest(\n ctx: ProviderContext,\n provider: ProviderName,\n url: string,\n init: RequestInit,\n): Promise<Record<string, unknown>> {\n let res: Response;\n try {\n res = await ctx.fetch(url, {\n ...init,\n headers: {\n Authorization: `Bearer ${ctx.secretKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...(init.headers ?? {}),\n },\n });\n } catch (err) {\n throw new PayKitError(`Network error calling ${provider}`, {\n code: \"network_error\",\n provider,\n cause: err,\n });\n }\n\n let body: Record<string, unknown> = {};\n try {\n body = (await res.json()) as Record<string, unknown>;\n } catch {\n body = {};\n }\n\n if (!res.ok || body.status === false || body.status === \"error\") {\n const message =\n typeof body.message === \"string\"\n ? body.message\n : `${provider} request failed (${res.status})`;\n throw new PayKitError(message, {\n code: \"provider_error\",\n provider,\n statusCode: res.status,\n raw: body,\n });\n }\n\n return body;\n}\n\n/** Constant-time comparison of two hex/string signatures. */\nexport function safeEqual(a: string, b: string): boolean {\n const ba = Buffer.from(a);\n const bb = Buffer.from(b);\n if (ba.length !== bb.length) return false;\n return timingSafeEqual(ba, bb);\n}\n","import { PayKitError } from \"../errors\";\nimport { providerRequest, safeEqual } from \"../internal\";\nimport type {\n InitializeParams,\n InitializeResult,\n PaymentProvider,\n PaymentStatus,\n ProviderContext,\n RefundOptions,\n RefundResult,\n RefundStatus,\n VerifyResult,\n WebhookEvent,\n WebhookEventType,\n} from \"../types\";\n\nconst FLUTTERWAVE_BASE = \"https://api.flutterwave.com\";\n\n/**\n * Flutterwave works in major currency units (naira, not kobo), so pay-kit\n * converts to/from its canonical subunit representation at the boundary.\n */\nfunction toMajor(subunits: number): number {\n return subunits / 100;\n}\nfunction toSubunits(major: unknown): number {\n return Math.round(Number(major ?? 0) * 100);\n}\n\nfunction mapStatus(raw: unknown): PaymentStatus {\n switch (raw) {\n case \"successful\":\n case \"success\":\n return \"success\";\n case \"failed\":\n return \"failed\";\n default:\n return \"pending\";\n }\n}\n\nfunction mapEventType(status: PaymentStatus): WebhookEventType {\n if (status === \"success\") return \"charge.success\";\n if (status === \"failed\") return \"charge.failed\";\n return \"unknown\";\n}\n\nfunction mapRefundStatus(raw: unknown): RefundStatus {\n switch (raw) {\n case \"completed\":\n case \"successful\":\n case \"success\":\n case \"processed\":\n return \"processed\";\n case \"failed\":\n return \"failed\";\n default:\n return \"pending\";\n }\n}\n\nexport function createFlutterwaveProvider(ctx: ProviderContext): PaymentProvider {\n const base = ctx.baseUrl ?? FLUTTERWAVE_BASE;\n\n return {\n name: \"flutterwave\",\n\n async initialize(params: InitializeParams): Promise<InitializeResult> {\n const reference = params.reference ?? ctx.generateReference();\n const body = await providerRequest(ctx, \"flutterwave\", `${base}/v3/payments`, {\n method: \"POST\",\n body: JSON.stringify({\n tx_ref: reference,\n amount: toMajor(params.amount),\n currency: params.currency ?? \"NGN\",\n redirect_url: params.callbackUrl,\n customer: { email: params.email },\n meta: params.metadata,\n }),\n });\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n return {\n reference,\n authorizationUrl: String(data.link ?? \"\"),\n raw: body,\n };\n },\n\n async verify(reference: string): Promise<VerifyResult> {\n const body = await providerRequest(\n ctx,\n \"flutterwave\",\n `${base}/v3/transactions/verify_by_reference?tx_ref=${encodeURIComponent(reference)}`,\n { method: \"GET\" },\n );\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const customer = (data.customer ?? {}) as Record<string, unknown>;\n return {\n reference: String(data.tx_ref ?? reference),\n status: mapStatus(data.status),\n amount: toSubunits(data.amount),\n currency: String(data.currency ?? \"\"),\n paidAt: data.created_at ? String(data.created_at) : undefined,\n channel: data.payment_type ? String(data.payment_type) : undefined,\n customer: { email: customer.email ? String(customer.email) : undefined },\n raw: body,\n };\n },\n\n async refund(reference: string, options?: RefundOptions): Promise<RefundResult> {\n // Flutterwave refunds are keyed by the numeric transaction id, not tx_ref,\n // so resolve the id from the reference first.\n const verifyBody = await providerRequest(\n ctx,\n \"flutterwave\",\n `${base}/v3/transactions/verify_by_reference?tx_ref=${encodeURIComponent(reference)}`,\n { method: \"GET\" },\n );\n const verifyData = (verifyBody.data ?? {}) as Record<string, unknown>;\n const id = verifyData.id;\n if (id === undefined || id === null) {\n throw new PayKitError(\n `No Flutterwave transaction found for reference \"${reference}\"`,\n { code: \"provider_error\", provider: \"flutterwave\", raw: verifyBody },\n );\n }\n\n const body = await providerRequest(\n ctx,\n \"flutterwave\",\n `${base}/v3/transactions/${encodeURIComponent(String(id))}/refund`,\n {\n method: \"POST\",\n body: JSON.stringify(\n options?.amount !== undefined ? { amount: toMajor(options.amount) } : {},\n ),\n },\n );\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const refunded = data.amount_refunded ?? data.amount;\n return {\n reference,\n status: mapRefundStatus(data.status),\n amount: refunded !== undefined ? toSubunits(refunded) : undefined,\n raw: body,\n };\n },\n\n constructWebhookEvent(rawBody: string, signature: string): WebhookEvent {\n // Flutterwave sends the \"Secret hash\" verbatim in the `verif-hash` header.\n if (!ctx.webhookSecret) {\n throw new PayKitError(\n \"Flutterwave webhook verification requires `webhookSecret` (your Secret hash)\",\n { code: \"config_error\", provider: \"flutterwave\" },\n );\n }\n if (!signature || !safeEqual(ctx.webhookSecret, signature)) {\n throw new PayKitError(\"Invalid Flutterwave webhook signature\", {\n code: \"invalid_signature\",\n provider: \"flutterwave\",\n });\n }\n\n let event: Record<string, unknown>;\n try {\n event = JSON.parse(rawBody) as Record<string, unknown>;\n } catch (err) {\n throw new PayKitError(\"Malformed Flutterwave webhook body\", {\n code: \"provider_error\",\n provider: \"flutterwave\",\n cause: err,\n });\n }\n\n const data = (event.data ?? {}) as Record<string, unknown>;\n const status = mapStatus(data.status);\n return {\n type: mapEventType(status),\n reference: String(data.tx_ref ?? \"\"),\n status,\n amount: data.amount !== undefined ? toSubunits(data.amount) : undefined,\n currency: data.currency ? String(data.currency) : undefined,\n raw: event,\n };\n },\n };\n}\n","import { createPayClient } from \"./client\";\nimport { PayKitError, isRetryableError } from \"./errors\";\nimport type {\n FallbackClient,\n FallbackClientConfig,\n FallbackInitializeResult,\n InitializeParams,\n PayClient,\n ProviderName,\n} from \"./types\";\n\n/**\n * Create a resilient client that tries multiple providers in order.\n *\n * `initialize` falls through to the next provider on outage-like failures\n * (network errors, HTTP 5xx/429) and returns the provider that succeeded.\n * Route `verify`/`refund`/`webhooks` for that transaction back to the same\n * provider - a charge started on Paystack can only be verified on Paystack.\n *\n * @example\n * const pay = createFallbackClient({\n * providers: [\n * { provider: \"paystack\", secretKey: process.env.PAYSTACK_SECRET_KEY! },\n * { provider: \"flutterwave\", secretKey: process.env.FLW_SECRET_KEY! },\n * ],\n * });\n * const { reference, provider } = await pay.initialize({ amount: 500000, email: \"a@b.com\" });\n * // persist { reference, provider }, then later:\n * const result = await pay.verify(provider, reference);\n */\nexport function createFallbackClient(config: FallbackClientConfig): FallbackClient {\n if (!config.providers || config.providers.length === 0) {\n throw new PayKitError(\"createFallbackClient requires at least one provider\", {\n code: \"config_error\",\n });\n }\n\n const clients = new Map<ProviderName, PayClient>();\n const order: ProviderName[] = [];\n\n for (const entry of config.providers) {\n if (clients.has(entry.provider)) continue; // ignore duplicates, keep first\n clients.set(\n entry.provider,\n createPayClient({\n provider: entry.provider,\n secretKey: entry.secretKey,\n webhookSecret: entry.webhookSecret,\n baseUrl: entry.baseUrl,\n fetch: config.fetch,\n generateReference: config.generateReference,\n }),\n );\n order.push(entry.provider);\n }\n\n function getClient(provider: ProviderName): PayClient {\n const client = clients.get(provider);\n if (!client) {\n throw new PayKitError(\n `Provider \"${provider}\" is not configured on this fallback client`,\n { code: \"config_error\" },\n );\n }\n return client;\n }\n\n return {\n async initialize(params: InitializeParams): Promise<FallbackInitializeResult> {\n let lastError: unknown;\n for (const provider of order) {\n try {\n const result = await getClient(provider).initialize(params);\n return { ...result, provider };\n } catch (err) {\n lastError = err;\n // A client error (4xx) would fail the same way everywhere - stop.\n if (!isRetryableError(err)) throw err;\n // Otherwise fall through to the next provider.\n }\n }\n throw (\n lastError ??\n new PayKitError(\"All providers failed to initialize the payment\", {\n code: \"provider_error\",\n })\n );\n },\n\n verify: (provider, reference) => getClient(provider).verify(reference),\n\n refund: (provider, reference, options) => getClient(provider).refund(reference, options),\n\n webhooks: {\n construct: (provider, rawBody, signature) =>\n getClient(provider).webhooks.construct(rawBody, signature),\n },\n\n client: getClient,\n };\n}\n"],"mappings":"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","names":["import_node_crypto","import_node_crypto","mapStatus","mapEventType","mapRefundStatus"]}
@@ -0,0 +1,196 @@
1
+ /** Supported providers. */
2
+ type ProviderName = "paystack" | "flutterwave";
3
+ /** ISO-4217 currency codes commonly supported across African rails. */
4
+ type Currency = "NGN" | "USD" | "GHS" | "KES" | "ZAR" | (string & {});
5
+ /** Normalized payment status across providers. */
6
+ type PaymentStatus = "success" | "failed" | "pending" | "abandoned";
7
+ /** Normalized webhook event type. Providers map their native events to these. */
8
+ type WebhookEventType = "charge.success" | "charge.failed" | "transfer.success" | "transfer.failed" | "unknown" | (string & {});
9
+ interface InitializeParams {
10
+ /**
11
+ * Amount in the smallest currency unit (subunits) - kobo for NGN, cents for USD.
12
+ * pay-kit uses subunits everywhere (Stripe-style) to avoid float rounding bugs,
13
+ * and converts per-provider under the hood.
14
+ */
15
+ amount: number;
16
+ /** Customer email (required by both providers). */
17
+ email: string;
18
+ currency?: Currency;
19
+ /** Optional custom reference. One is generated if omitted. */
20
+ reference?: string;
21
+ /** URL the provider redirects to after payment. */
22
+ callbackUrl?: string;
23
+ /** Arbitrary metadata echoed back on verify/webhook. */
24
+ metadata?: Record<string, unknown>;
25
+ }
26
+ interface InitializeResult {
27
+ /** Reference to persist and later verify against. */
28
+ reference: string;
29
+ /** Hosted checkout URL to redirect the customer to. */
30
+ authorizationUrl: string;
31
+ /** Provider access code, when available (Paystack). */
32
+ accessCode?: string;
33
+ /** Raw provider response, for anything pay-kit does not normalize. */
34
+ raw: unknown;
35
+ }
36
+ interface VerifyResult {
37
+ reference: string;
38
+ status: PaymentStatus;
39
+ /** Amount in subunits (kobo/cents). */
40
+ amount: number;
41
+ currency: string;
42
+ paidAt?: string;
43
+ channel?: string;
44
+ customer?: {
45
+ email?: string;
46
+ };
47
+ raw: unknown;
48
+ }
49
+ interface WebhookEvent {
50
+ type: WebhookEventType;
51
+ reference: string;
52
+ status?: PaymentStatus;
53
+ /** Amount in subunits (kobo/cents), when present on the event. */
54
+ amount?: number;
55
+ currency?: string;
56
+ raw: unknown;
57
+ }
58
+ /** Normalized refund status across providers. */
59
+ type RefundStatus = "pending" | "processed" | "failed";
60
+ interface RefundOptions {
61
+ /** Amount to refund in subunits. Omit for a full refund. */
62
+ amount?: number;
63
+ }
64
+ interface RefundResult {
65
+ /** Reference of the original transaction being refunded. */
66
+ reference: string;
67
+ status: RefundStatus;
68
+ /** Refunded amount in subunits, when reported by the provider. */
69
+ amount?: number;
70
+ raw: unknown;
71
+ }
72
+ interface PaymentProvider {
73
+ readonly name: ProviderName;
74
+ initialize(params: InitializeParams): Promise<InitializeResult>;
75
+ verify(reference: string): Promise<VerifyResult>;
76
+ refund(reference: string, options?: RefundOptions): Promise<RefundResult>;
77
+ constructWebhookEvent(rawBody: string, signature: string): WebhookEvent;
78
+ }
79
+ interface PayClientConfig {
80
+ provider: ProviderName;
81
+ /** Provider secret key. Server-side only - never expose to the browser. */
82
+ secretKey: string;
83
+ /**
84
+ * Webhook verification secret. Flutterwave requires this ("Secret hash").
85
+ * Paystack verifies webhooks with the secretKey, so this is optional there.
86
+ */
87
+ webhookSecret?: string;
88
+ /** Override the API base URL (useful for tests / proxies). */
89
+ baseUrl?: string;
90
+ /** Inject a fetch implementation (defaults to global fetch). */
91
+ fetch?: typeof fetch;
92
+ /** Override reference generation. */
93
+ generateReference?: () => string;
94
+ }
95
+ interface PayClient {
96
+ readonly provider: ProviderName;
97
+ initialize(params: InitializeParams): Promise<InitializeResult>;
98
+ verify(reference: string): Promise<VerifyResult>;
99
+ /** Refund a transaction in full, or partially with `options.amount` (subunits). */
100
+ refund(reference: string, options?: RefundOptions): Promise<RefundResult>;
101
+ webhooks: {
102
+ /**
103
+ * Verify a raw webhook body against its signature header and return a
104
+ * normalized event. Throws PayKitError("invalid_signature") on mismatch.
105
+ */
106
+ construct(rawBody: string, signature: string): WebhookEvent;
107
+ };
108
+ }
109
+ /** One provider entry for a fallback client. Providers are tried in order. */
110
+ interface FallbackProviderConfig {
111
+ provider: ProviderName;
112
+ secretKey: string;
113
+ webhookSecret?: string;
114
+ baseUrl?: string;
115
+ }
116
+ interface FallbackClientConfig {
117
+ /** Providers tried in order until one initializes successfully. */
118
+ providers: FallbackProviderConfig[];
119
+ fetch?: typeof fetch;
120
+ generateReference?: () => string;
121
+ }
122
+ /** `initialize` result plus the provider that actually handled the charge. */
123
+ interface FallbackInitializeResult extends InitializeResult {
124
+ provider: ProviderName;
125
+ }
126
+ interface FallbackClient {
127
+ /**
128
+ * Try each configured provider in order until one succeeds. Only outage-like
129
+ * failures (network errors, HTTP 5xx, 429) trigger a fallback; a 4xx fails
130
+ * fast. The result's `provider` field tells you which provider to route
131
+ * verify/refund/webhooks to for this transaction.
132
+ */
133
+ initialize(params: InitializeParams): Promise<FallbackInitializeResult>;
134
+ verify(provider: ProviderName, reference: string): Promise<VerifyResult>;
135
+ refund(provider: ProviderName, reference: string, options?: RefundOptions): Promise<RefundResult>;
136
+ webhooks: {
137
+ construct(provider: ProviderName, rawBody: string, signature: string): WebhookEvent;
138
+ };
139
+ /** Access the underlying single-provider client (e.g. after fallback routing). */
140
+ client(provider: ProviderName): PayClient;
141
+ }
142
+
143
+ /**
144
+ * Create a payment client bound to a single provider.
145
+ *
146
+ * @example
147
+ * const pay = createPayClient({ provider: "paystack", secretKey: process.env.PAYSTACK_SECRET_KEY! });
148
+ * const { authorizationUrl, reference } = await pay.initialize({ amount: 500000, email: "a@b.com" });
149
+ */
150
+ declare function createPayClient(config: PayClientConfig): PayClient;
151
+
152
+ /**
153
+ * Create a resilient client that tries multiple providers in order.
154
+ *
155
+ * `initialize` falls through to the next provider on outage-like failures
156
+ * (network errors, HTTP 5xx/429) and returns the provider that succeeded.
157
+ * Route `verify`/`refund`/`webhooks` for that transaction back to the same
158
+ * provider - a charge started on Paystack can only be verified on Paystack.
159
+ *
160
+ * @example
161
+ * const pay = createFallbackClient({
162
+ * providers: [
163
+ * { provider: "paystack", secretKey: process.env.PAYSTACK_SECRET_KEY! },
164
+ * { provider: "flutterwave", secretKey: process.env.FLW_SECRET_KEY! },
165
+ * ],
166
+ * });
167
+ * const { reference, provider } = await pay.initialize({ amount: 500000, email: "a@b.com" });
168
+ * // persist { reference, provider }, then later:
169
+ * const result = await pay.verify(provider, reference);
170
+ */
171
+ declare function createFallbackClient(config: FallbackClientConfig): FallbackClient;
172
+
173
+ type PayKitErrorCode = "provider_error" | "network_error" | "invalid_signature" | "config_error" | "verification_failed";
174
+ interface PayKitErrorOptions {
175
+ code: PayKitErrorCode;
176
+ provider?: string;
177
+ statusCode?: number;
178
+ raw?: unknown;
179
+ cause?: unknown;
180
+ }
181
+ /**
182
+ * Whether an error is worth retrying on another provider: network failures and
183
+ * outage-like HTTP statuses (5xx, 429). Client errors (4xx) are not retryable -
184
+ * they would fail the same way everywhere.
185
+ */
186
+ declare function isRetryableError(err: unknown): boolean;
187
+ /** Single error type surfaced by pay-kit, with a machine-readable `code`. */
188
+ declare class PayKitError extends Error {
189
+ readonly code: PayKitErrorCode;
190
+ readonly provider?: string;
191
+ readonly statusCode?: number;
192
+ readonly raw?: unknown;
193
+ constructor(message: string, options: PayKitErrorOptions);
194
+ }
195
+
196
+ export { type Currency, type FallbackClient, type FallbackClientConfig, type FallbackInitializeResult, type FallbackProviderConfig, type InitializeParams, type InitializeResult, type PayClient, type PayClientConfig, PayKitError, type PayKitErrorCode, type PayKitErrorOptions, type PaymentProvider, type PaymentStatus, type ProviderName, type RefundOptions, type RefundResult, type RefundStatus, type VerifyResult, type WebhookEvent, type WebhookEventType, createFallbackClient, createPayClient, isRetryableError };
@@ -0,0 +1,196 @@
1
+ /** Supported providers. */
2
+ type ProviderName = "paystack" | "flutterwave";
3
+ /** ISO-4217 currency codes commonly supported across African rails. */
4
+ type Currency = "NGN" | "USD" | "GHS" | "KES" | "ZAR" | (string & {});
5
+ /** Normalized payment status across providers. */
6
+ type PaymentStatus = "success" | "failed" | "pending" | "abandoned";
7
+ /** Normalized webhook event type. Providers map their native events to these. */
8
+ type WebhookEventType = "charge.success" | "charge.failed" | "transfer.success" | "transfer.failed" | "unknown" | (string & {});
9
+ interface InitializeParams {
10
+ /**
11
+ * Amount in the smallest currency unit (subunits) - kobo for NGN, cents for USD.
12
+ * pay-kit uses subunits everywhere (Stripe-style) to avoid float rounding bugs,
13
+ * and converts per-provider under the hood.
14
+ */
15
+ amount: number;
16
+ /** Customer email (required by both providers). */
17
+ email: string;
18
+ currency?: Currency;
19
+ /** Optional custom reference. One is generated if omitted. */
20
+ reference?: string;
21
+ /** URL the provider redirects to after payment. */
22
+ callbackUrl?: string;
23
+ /** Arbitrary metadata echoed back on verify/webhook. */
24
+ metadata?: Record<string, unknown>;
25
+ }
26
+ interface InitializeResult {
27
+ /** Reference to persist and later verify against. */
28
+ reference: string;
29
+ /** Hosted checkout URL to redirect the customer to. */
30
+ authorizationUrl: string;
31
+ /** Provider access code, when available (Paystack). */
32
+ accessCode?: string;
33
+ /** Raw provider response, for anything pay-kit does not normalize. */
34
+ raw: unknown;
35
+ }
36
+ interface VerifyResult {
37
+ reference: string;
38
+ status: PaymentStatus;
39
+ /** Amount in subunits (kobo/cents). */
40
+ amount: number;
41
+ currency: string;
42
+ paidAt?: string;
43
+ channel?: string;
44
+ customer?: {
45
+ email?: string;
46
+ };
47
+ raw: unknown;
48
+ }
49
+ interface WebhookEvent {
50
+ type: WebhookEventType;
51
+ reference: string;
52
+ status?: PaymentStatus;
53
+ /** Amount in subunits (kobo/cents), when present on the event. */
54
+ amount?: number;
55
+ currency?: string;
56
+ raw: unknown;
57
+ }
58
+ /** Normalized refund status across providers. */
59
+ type RefundStatus = "pending" | "processed" | "failed";
60
+ interface RefundOptions {
61
+ /** Amount to refund in subunits. Omit for a full refund. */
62
+ amount?: number;
63
+ }
64
+ interface RefundResult {
65
+ /** Reference of the original transaction being refunded. */
66
+ reference: string;
67
+ status: RefundStatus;
68
+ /** Refunded amount in subunits, when reported by the provider. */
69
+ amount?: number;
70
+ raw: unknown;
71
+ }
72
+ interface PaymentProvider {
73
+ readonly name: ProviderName;
74
+ initialize(params: InitializeParams): Promise<InitializeResult>;
75
+ verify(reference: string): Promise<VerifyResult>;
76
+ refund(reference: string, options?: RefundOptions): Promise<RefundResult>;
77
+ constructWebhookEvent(rawBody: string, signature: string): WebhookEvent;
78
+ }
79
+ interface PayClientConfig {
80
+ provider: ProviderName;
81
+ /** Provider secret key. Server-side only - never expose to the browser. */
82
+ secretKey: string;
83
+ /**
84
+ * Webhook verification secret. Flutterwave requires this ("Secret hash").
85
+ * Paystack verifies webhooks with the secretKey, so this is optional there.
86
+ */
87
+ webhookSecret?: string;
88
+ /** Override the API base URL (useful for tests / proxies). */
89
+ baseUrl?: string;
90
+ /** Inject a fetch implementation (defaults to global fetch). */
91
+ fetch?: typeof fetch;
92
+ /** Override reference generation. */
93
+ generateReference?: () => string;
94
+ }
95
+ interface PayClient {
96
+ readonly provider: ProviderName;
97
+ initialize(params: InitializeParams): Promise<InitializeResult>;
98
+ verify(reference: string): Promise<VerifyResult>;
99
+ /** Refund a transaction in full, or partially with `options.amount` (subunits). */
100
+ refund(reference: string, options?: RefundOptions): Promise<RefundResult>;
101
+ webhooks: {
102
+ /**
103
+ * Verify a raw webhook body against its signature header and return a
104
+ * normalized event. Throws PayKitError("invalid_signature") on mismatch.
105
+ */
106
+ construct(rawBody: string, signature: string): WebhookEvent;
107
+ };
108
+ }
109
+ /** One provider entry for a fallback client. Providers are tried in order. */
110
+ interface FallbackProviderConfig {
111
+ provider: ProviderName;
112
+ secretKey: string;
113
+ webhookSecret?: string;
114
+ baseUrl?: string;
115
+ }
116
+ interface FallbackClientConfig {
117
+ /** Providers tried in order until one initializes successfully. */
118
+ providers: FallbackProviderConfig[];
119
+ fetch?: typeof fetch;
120
+ generateReference?: () => string;
121
+ }
122
+ /** `initialize` result plus the provider that actually handled the charge. */
123
+ interface FallbackInitializeResult extends InitializeResult {
124
+ provider: ProviderName;
125
+ }
126
+ interface FallbackClient {
127
+ /**
128
+ * Try each configured provider in order until one succeeds. Only outage-like
129
+ * failures (network errors, HTTP 5xx, 429) trigger a fallback; a 4xx fails
130
+ * fast. The result's `provider` field tells you which provider to route
131
+ * verify/refund/webhooks to for this transaction.
132
+ */
133
+ initialize(params: InitializeParams): Promise<FallbackInitializeResult>;
134
+ verify(provider: ProviderName, reference: string): Promise<VerifyResult>;
135
+ refund(provider: ProviderName, reference: string, options?: RefundOptions): Promise<RefundResult>;
136
+ webhooks: {
137
+ construct(provider: ProviderName, rawBody: string, signature: string): WebhookEvent;
138
+ };
139
+ /** Access the underlying single-provider client (e.g. after fallback routing). */
140
+ client(provider: ProviderName): PayClient;
141
+ }
142
+
143
+ /**
144
+ * Create a payment client bound to a single provider.
145
+ *
146
+ * @example
147
+ * const pay = createPayClient({ provider: "paystack", secretKey: process.env.PAYSTACK_SECRET_KEY! });
148
+ * const { authorizationUrl, reference } = await pay.initialize({ amount: 500000, email: "a@b.com" });
149
+ */
150
+ declare function createPayClient(config: PayClientConfig): PayClient;
151
+
152
+ /**
153
+ * Create a resilient client that tries multiple providers in order.
154
+ *
155
+ * `initialize` falls through to the next provider on outage-like failures
156
+ * (network errors, HTTP 5xx/429) and returns the provider that succeeded.
157
+ * Route `verify`/`refund`/`webhooks` for that transaction back to the same
158
+ * provider - a charge started on Paystack can only be verified on Paystack.
159
+ *
160
+ * @example
161
+ * const pay = createFallbackClient({
162
+ * providers: [
163
+ * { provider: "paystack", secretKey: process.env.PAYSTACK_SECRET_KEY! },
164
+ * { provider: "flutterwave", secretKey: process.env.FLW_SECRET_KEY! },
165
+ * ],
166
+ * });
167
+ * const { reference, provider } = await pay.initialize({ amount: 500000, email: "a@b.com" });
168
+ * // persist { reference, provider }, then later:
169
+ * const result = await pay.verify(provider, reference);
170
+ */
171
+ declare function createFallbackClient(config: FallbackClientConfig): FallbackClient;
172
+
173
+ type PayKitErrorCode = "provider_error" | "network_error" | "invalid_signature" | "config_error" | "verification_failed";
174
+ interface PayKitErrorOptions {
175
+ code: PayKitErrorCode;
176
+ provider?: string;
177
+ statusCode?: number;
178
+ raw?: unknown;
179
+ cause?: unknown;
180
+ }
181
+ /**
182
+ * Whether an error is worth retrying on another provider: network failures and
183
+ * outage-like HTTP statuses (5xx, 429). Client errors (4xx) are not retryable -
184
+ * they would fail the same way everywhere.
185
+ */
186
+ declare function isRetryableError(err: unknown): boolean;
187
+ /** Single error type surfaced by pay-kit, with a machine-readable `code`. */
188
+ declare class PayKitError extends Error {
189
+ readonly code: PayKitErrorCode;
190
+ readonly provider?: string;
191
+ readonly statusCode?: number;
192
+ readonly raw?: unknown;
193
+ constructor(message: string, options: PayKitErrorOptions);
194
+ }
195
+
196
+ export { type Currency, type FallbackClient, type FallbackClientConfig, type FallbackInitializeResult, type FallbackProviderConfig, type InitializeParams, type InitializeResult, type PayClient, type PayClientConfig, PayKitError, type PayKitErrorCode, type PayKitErrorOptions, type PaymentProvider, type PaymentStatus, type ProviderName, type RefundOptions, type RefundResult, type RefundStatus, type VerifyResult, type WebhookEvent, type WebhookEventType, createFallbackClient, createPayClient, isRetryableError };