@afriex/sdk 2.0.0 → 3.0.1

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.
package/README.md CHANGED
@@ -66,22 +66,39 @@ const balances = await afriex.balance.getBalance({
66
66
  currencies: ["USD", "NGN"],
67
67
  });
68
68
 
69
- // Webhook Verification (only if webhookPublicKey provided)
70
- if (afriex.webhooks) {
71
- const isValid = afriex.webhooks.verify(payload, signature);
72
- }
69
+ // Checkout - hosted payment page
70
+ const session = await afriex.checkout.createSession({
71
+ amount: 10000, // minor units, minimum 100
72
+ currency: "USD",
73
+ merchantReference: "order-123",
74
+ redirectUrl: "https://example.com/checkout/complete",
75
+ customer: {
76
+ name: "John Doe",
77
+ email: "john@example.com",
78
+ phone: "+1234567890",
79
+ countryCode: "US",
80
+ },
81
+ });
82
+ // session.checkoutUrl
83
+
84
+ // Webhook verification (requires webhookPublicKey)
85
+ const isValid = afriex.webhooks.verify(payload, signature);
73
86
  ```
74
87
 
75
88
  ## Available Services
76
89
 
77
- | Service | Description |
78
- | ----------------------- | -------------------------------------------- |
79
- | `afriex.customers` | Customer CRUD and KYC management |
80
- | `afriex.transactions` | Create and track transactions |
81
- | `afriex.paymentMethods` | Bank, mobile money, crypto, virtual accounts |
82
- | `afriex.balance` | Organization wallet balances |
83
- | `afriex.rates` | Exchange rates and conversions |
84
- | `afriex.webhooks` | Webhook signature verification (optional) |
90
+ | Service | Description |
91
+ | ----------------------- | ---------------------------------------------------------- |
92
+ | `afriex.customers` | Customer CRUD, KYC updates, and document verification |
93
+ | `afriex.transactions` | Create, authorize, list, and track transactions |
94
+ | `afriex.paymentMethods` | Bank, mobile money, crypto wallets, virtual & pool accounts |
95
+ | `afriex.balance` | Organization wallet balances and sandbox top-ups |
96
+ | `afriex.rates` | Exchange rates and conversions |
97
+ | `afriex.checkout` | Hosted checkout sessions (card, virtual account, momo) |
98
+ | `afriex.webhooks` | Webhook signature verification and sandbox test triggers |
99
+
100
+ Every service is always available on the client. `afriex.webhooks.verify()` returns `false`
101
+ unless a `webhookPublicKey` was provided; `afriex.webhookVerifier` is set only when one was.
85
102
 
86
103
  ## Configuration
87
104
 
@@ -89,7 +106,40 @@ if (afriex.webhooks) {
89
106
  interface AfriexSDKConfig {
90
107
  apiKey: string; // Required - Your Afriex API key
91
108
  environment?: "staging" | "production"; // Default: 'production'
92
- webhookPublicKey?: string; // Optional - Afriex's public key for webhooks
109
+ webhookPublicKey?: string; // Optional - Afriex's PEM public key for webhook verification
110
+ customConfig?: { timeout?: number }; // Optional - override timeout
111
+ logLevel?: LogLevel; // Default: LogLevel.ERROR
112
+ enableLogging?: boolean; // Default: true
113
+ retryConfig?: {
114
+ maxRetries: number;
115
+ retryDelay: number;
116
+ retryableStatusCodes: number[]; // Default: [408, 429, 500, 502, 503, 504]
117
+ };
118
+ }
119
+ ```
120
+
121
+ `staging` points at `https://sandbox.api.afriex.com/api/v1`, `production` at
122
+ `https://api.afriex.com/api/v1`.
123
+
124
+ ## Error Handling
125
+
126
+ All errors extend `AfriexError` and are re-exported from this package:
127
+
128
+ ```typescript
129
+ import { AfriexError, ApiError, ValidationError, RateLimitError, NetworkError } from "@afriex/sdk";
130
+
131
+ try {
132
+ await afriex.transactions.create(request);
133
+ } catch (error) {
134
+ if (error instanceof ValidationError) {
135
+ // Request failed local validation before being sent
136
+ } else if (error instanceof RateLimitError) {
137
+ // 429 - back off and retry
138
+ } else if (error instanceof ApiError) {
139
+ // Non-2xx response from the API
140
+ } else if (error instanceof NetworkError) {
141
+ // Connection/timeout failure
142
+ }
93
143
  }
94
144
  ```
95
145
 
@@ -97,19 +147,34 @@ interface AfriexSDKConfig {
97
147
 
98
148
  For smaller bundle sizes, install packages individually:
99
149
 
100
- | Package | Description |
101
- | ------------------------- | ----------------------------- |
102
- | `@afriex/core` | Base client and configuration |
103
- | `@afriex/customers` | Customer management |
104
- | `@afriex/transactions` | Transaction handling |
105
- | `@afriex/payment-methods` | Payment methods |
106
- | `@afriex/balance` | Balance queries |
107
- | `@afriex/rates` | Exchange rates |
108
- | `@afriex/webhooks` | Webhook verification |
150
+ | Package | Description |
151
+ | ------------------------- | ------------------------------------------------------ |
152
+ | `@afriex/core` | Base client, config, HTTP layer, errors, shared types |
153
+ | `@afriex/customers` | Customer management and KYC |
154
+ | `@afriex/transactions` | Transaction handling |
155
+ | `@afriex/payment-methods` | Payment methods, institutions, virtual & pool accounts |
156
+ | `@afriex/balance` | Balance queries and sandbox top-ups |
157
+ | `@afriex/rates` | Exchange rates and conversions |
158
+ | `@afriex/checkout` | Hosted checkout sessions |
159
+ | `@afriex/webhooks` | Webhook verification and test triggers |
160
+
161
+ Each service package depends only on `@afriex/core`, so you can mix and match.
162
+
163
+ ## AI agents
164
+
165
+ Every `@afriex/*` package ships [TanStack Intent](https://tanstack.com/intent) skills under
166
+ `skills/`, so coding agents get grounded guidance on this SDK instead of guessing at the API.
167
+ If you use an AI agent, run:
168
+
169
+ ```bash
170
+ npx @tanstack/intent@latest install
171
+ ```
172
+
173
+ List what is available with `npx @tanstack/intent@latest list`.
109
174
 
110
175
  ## Documentation
111
176
 
112
- Full documentation available at [afriex-sdk-docs.vercel.app](https://afriex-sdk-docs.vercel.app)
177
+ Full documentation available at [docs.afriex.com](https://docs.afriex.com/sdk/introduction)
113
178
 
114
179
  ## License
115
180
 
package/dist/index.d.ts CHANGED
@@ -29,9 +29,9 @@ export { BalanceService } from "@afriex/balance";
29
29
  export { RateService } from "@afriex/rates";
30
30
  export { CheckoutService } from "@afriex/checkout";
31
31
  export { WebhookVerifier, WebhookService } from "@afriex/webhooks";
32
- export type { Customer, CreateCustomerRequest, UpdateCustomerKycRequest, ListCustomersParams, CustomerListResponse, } from "@afriex/customers";
33
- export type { Transaction, CreateTransactionRequest, ListTransactionsParams, TransactionListResponse, TransactionListType, TransactionChannel, TransactionType, TransactionStatus, TransactionMeta, } from "@afriex/transactions";
34
- export type { PaymentMethod, CreatePaymentMethodRequest, ListPaymentMethodsParams, PaymentMethodListResponse, PaymentChannel, Institution, ResolveAccountParams, ResolveAccountResponse, GetInstitutionsParams, CryptoWalletResponse, GetCryptoWalletParams, ListVirtualAccountsParams, CreateVirtualAccountParams, ListPoolAccountsParams, VirtualAccountListResponse, PoolAccountResponse, } from "@afriex/payment-methods";
32
+ export type { Customer, CreateCustomerRequest, UpdateCustomerRequest, UpdateCustomerKycRequest, VerifyCustomerRequest, ListCustomersParams, CustomerListResponse, } from "@afriex/customers";
33
+ export type { Transaction, CreateTransactionRequest, AuthorizeTransactionRequest, ListTransactionsParams, TransactionListResponse, TransactionListType, TransactionChannel, TransactionType, TransactionStatus, TransactionMeta, TransactionMetaResponse, TransactionFailureCode, TransactionFailureReason, } from "@afriex/transactions";
34
+ export type { PaymentMethod, CreatePaymentMethodRequest, ListPaymentMethodsParams, PaymentMethodListResponse, PaymentChannel, CreatablePaymentChannel, PaymentMethodStatus, CardBrand, CardExpiration, Institution, InstitutionListChannel, ResolveAccountParams, ResolveAccountResponse, GetInstitutionsParams, CryptoWalletResponse, GetCryptoWalletParams, ListVirtualAccountsParams, CreateVirtualAccountParams, ListPoolAccountsParams, VirtualAccountListResponse, PoolAccountResponse, } from "@afriex/payment-methods";
35
35
  export type { BalanceResponse, GetBalanceParams } from "@afriex/balance";
36
36
  export type { RatesResponse, GetRatesParams } from "@afriex/rates";
37
37
  export type { CheckoutSession, CreateCheckoutSessionRequest, CheckoutCustomer, CheckoutChannel, } from "@afriex/checkout";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afriex/sdk",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Official TypeScript SDK for the Afriex Business API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,18 +20,19 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@afriex/core": "1.3.0",
24
- "@afriex/customers": "1.4.0",
25
- "@afriex/transactions": "1.4.0",
26
- "@afriex/payment-methods": "2.0.0",
27
- "@afriex/balance": "1.3.0",
28
- "@afriex/rates": "1.4.0",
29
- "@afriex/checkout": "2.0.0",
30
- "@afriex/webhooks": "1.4.0"
23
+ "@afriex/core": "2.0.1",
24
+ "@afriex/customers": "2.0.1",
25
+ "@afriex/transactions": "2.0.1",
26
+ "@afriex/payment-methods": "3.0.1",
27
+ "@afriex/balance": "1.4.1",
28
+ "@afriex/rates": "1.4.2",
29
+ "@afriex/checkout": "2.0.2",
30
+ "@afriex/webhooks": "1.5.1"
31
31
  },
32
32
  "devDependencies": {
33
- "rimraf": "^5.0.5",
34
- "typescript": "^5.3.3"
33
+ "@tanstack/intent": "^0.3.6",
34
+ "rimraf": "^6.1.3",
35
+ "typescript": "^7.0.2"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "typescript": ">=5.0.0"
@@ -39,7 +40,8 @@
39
40
  "files": [
40
41
  "dist",
41
42
  "README.md",
42
- "!dist/**/*.map"
43
+ "!dist/**/*.map",
44
+ "skills"
43
45
  ],
44
46
  "publishConfig": {
45
47
  "access": "public"
@@ -51,7 +53,8 @@
51
53
  "money-transfer",
52
54
  "payments",
53
55
  "fintech",
54
- "typescript"
56
+ "typescript",
57
+ "tanstack-intent"
55
58
  ],
56
59
  "license": "MIT",
57
60
  "scripts": {
@@ -0,0 +1,329 @@
1
+ ---
2
+ name: afriex-go-live
3
+ description: >
4
+ Pre-deploy security and correctness checklist for an Afriex Business API
5
+ integration built on @afriex/afriex. Verifies API key handling, Environment
6
+ selection, retry and timeout policy, transaction idempotency keys, webhook
7
+ signature verification against webhookPublicKey with raw bodies, at-least-once
8
+ delivery handling, sandbox-only endpoints, and error-code handling. Load
9
+ before shipping an Afriex integration to production, during a payments
10
+ security review, or when auditing an existing Afriex integration.
11
+ metadata:
12
+ type: security
13
+ library: '@afriex/sdk'
14
+ library_version: '3.0.1'
15
+ requires:
16
+ - 'afriex-sdk'
17
+ sources:
18
+ - 'Afri-exchange/afriex-sdk:packages/sdk/src/index.ts'
19
+ - 'Afri-exchange/afriex-sdk:packages/core/src/config/Config.ts'
20
+ - 'Afri-exchange/afriex-sdk:packages/webhooks/src/WebhookService.ts'
21
+ - 'Afri-exchange/afriex-sdk:packages/transactions/src/TransactionService.ts'
22
+ ---
23
+
24
+ # Afriex SDK — Go-Live Checklist
25
+
26
+ Run through each section before pointing an integration at
27
+ `Environment.PRODUCTION`. This skill builds on `afriex-sdk`; read it first for
28
+ the configuration surface these checks refer to.
29
+
30
+ ## Credential Checks
31
+
32
+ ### Check: the API key comes from the environment, never source
33
+
34
+ Expected:
35
+
36
+ ```ts
37
+ import { AfriexSDK, Environment } from "@afriex/sdk";
38
+
39
+ const apiKey = process.env.AFRIEX_API_KEY;
40
+ if (!apiKey) {
41
+ throw new Error("AFRIEX_API_KEY is not set");
42
+ }
43
+
44
+ export const afriex = new AfriexSDK({ apiKey, environment: Environment.PRODUCTION });
45
+ ```
46
+
47
+ Fail condition: an `apiKey` string literal, a key in a committed `.env`, or a
48
+ key reaching client-side bundles.
49
+ Fix: move the key to a server-side secret store and rotate the exposed one.
50
+
51
+ ### Check: production and sandbox keys are distinct variables
52
+
53
+ Expected:
54
+
55
+ ```ts
56
+ import { AfriexSDK, Environment } from "@afriex/sdk";
57
+
58
+ const isProduction = process.env.NODE_ENV === "production";
59
+
60
+ export const afriex = new AfriexSDK({
61
+ apiKey: (isProduction ? process.env.AFRIEX_API_KEY : process.env.AFRIEX_SANDBOX_API_KEY)!,
62
+ environment: isProduction ? Environment.PRODUCTION : Environment.STAGING,
63
+ });
64
+ ```
65
+
66
+ Fail condition: one `AFRIEX_API_KEY` variable reused across environments, or
67
+ `environment` omitted entirely.
68
+ Fix: separate the variables — `Config` defaults `environment` to
69
+ `Environment.PRODUCTION`, so an omitted field silently targets live money.
70
+
71
+ ## Transport Checks
72
+
73
+ ### Check: retries are explicitly configured
74
+
75
+ Expected:
76
+
77
+ ```ts
78
+ import { AfriexSDK, Environment } from "@afriex/sdk";
79
+
80
+ export const afriex = new AfriexSDK({
81
+ apiKey: process.env.AFRIEX_API_KEY!,
82
+ environment: Environment.PRODUCTION,
83
+ retryConfig: {
84
+ maxRetries: 3,
85
+ retryDelay: 1000,
86
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504],
87
+ },
88
+ });
89
+ ```
90
+
91
+ Fail condition: no `retryConfig` — `DEFAULT_CONFIG` sets `maxRetries: 0`, so a
92
+ single 503 fails the request outright.
93
+ Fix: supply `retryConfig`, and make sure every retried write carries a stable
94
+ `meta.idempotencyKey`.
95
+
96
+ ### Check: request logging does not leak request bodies
97
+
98
+ Expected:
99
+
100
+ ```ts
101
+ import { AfriexSDK, Environment, LogLevel } from "@afriex/sdk";
102
+
103
+ export const afriex = new AfriexSDK({
104
+ apiKey: process.env.AFRIEX_API_KEY!,
105
+ environment: Environment.PRODUCTION,
106
+ enableLogging: true,
107
+ logLevel: LogLevel.ERROR,
108
+ });
109
+ ```
110
+
111
+ Fail condition: `logLevel: LogLevel.DEBUG` in production, which logs request
112
+ and response metadata for every call.
113
+ Fix: keep production at `LogLevel.ERROR` and scrub customer data from any
114
+ handler that serializes `ApiError.details`.
115
+
116
+ ## Money-Movement Checks
117
+
118
+ ### Check: every transaction carries a stable idempotency key
119
+
120
+ Expected:
121
+
122
+ ```ts
123
+ import { AfriexSDK } from "@afriex/sdk";
124
+ import { randomUUID } from "node:crypto";
125
+
126
+ const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY! });
127
+
128
+ export async function payout(orderId: string, idempotencyKey: string) {
129
+ return afriex.transactions.create({
130
+ customerId: "cus_123",
131
+ destinationId: "pm_456",
132
+ sourceAmount: "100.00",
133
+ sourceCurrency: "USD",
134
+ destinationAmount: "150000.00",
135
+ destinationCurrency: "NGN",
136
+ meta: { idempotencyKey, reference: orderId },
137
+ });
138
+ }
139
+
140
+ export const keyForOrder = (orderId: string) => `${orderId}:${randomUUID()}`;
141
+ ```
142
+
143
+ Fail condition: `idempotencyKey: randomUUID()` generated inside a retry loop or
144
+ inside the request handler, so each attempt is a new key.
145
+ Fix: persist the key with the order before the first attempt and reuse it for
146
+ every retry.
147
+
148
+ ### Check: settlement is driven by webhooks, not by create() resolving
149
+
150
+ Expected:
151
+
152
+ ```ts
153
+ import type { TransactionWebhookPayload } from "@afriex/sdk";
154
+
155
+ const TERMINAL = new Set(["SUCCESS", "FAILED", "CANCELLED", "REJECTED", "REFUNDED"]);
156
+
157
+ export function isSettled(event: TransactionWebhookPayload): boolean {
158
+ return TERMINAL.has(event.data.status);
159
+ }
160
+ ```
161
+
162
+ Fail condition: order state marked paid immediately after
163
+ `transactions.create()` resolves.
164
+ Fix: treat `PENDING` and `PROCESSING` as in-flight and settle on
165
+ `TRANSACTION.UPDATED`.
166
+
167
+ ### Check: no sandbox-only calls run in production
168
+
169
+ Expected:
170
+
171
+ ```ts
172
+ import { AfriexSDK, Environment } from "@afriex/sdk";
173
+
174
+ const afriex = new AfriexSDK({
175
+ apiKey: process.env.AFRIEX_SANDBOX_API_KEY!,
176
+ environment: Environment.STAGING,
177
+ });
178
+
179
+ export async function seedTestWallet(): Promise<void> {
180
+ if (process.env.NODE_ENV === "production") {
181
+ return;
182
+ }
183
+ await afriex.balance.topUpSandbox({ amount: 10000, currency: "USD" });
184
+ await afriex.webhooks.triggerTestWebhook({
185
+ event: "TRANSACTION.UPDATED",
186
+ entityId: "507f1f77bcf86cd799439011",
187
+ });
188
+ }
189
+ ```
190
+
191
+ Fail condition: `topUpSandbox` or `triggerTestWebhook` reachable on a
192
+ production code path — both answer `403 FORBIDDEN` there.
193
+ Fix: guard them behind an environment check or keep them in test-only modules.
194
+
195
+ ## Webhook Checks
196
+
197
+ ### Check: the endpoint verifies signatures against the raw body
198
+
199
+ Expected:
200
+
201
+ ```ts
202
+ import express from "express";
203
+ import { AfriexSDK, WEBHOOK_SIGNATURE_HEADER } from "@afriex/sdk";
204
+
205
+ const webhookPublicKey = process.env.AFRIEX_WEBHOOK_PUBLIC_KEY;
206
+ if (!webhookPublicKey) {
207
+ throw new Error("AFRIEX_WEBHOOK_PUBLIC_KEY is not set");
208
+ }
209
+
210
+ const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY!, webhookPublicKey });
211
+ const app = express();
212
+
213
+ app.post(
214
+ "/webhooks/afriex",
215
+ express.raw({ type: "application/json" }),
216
+ (req, res) => {
217
+ try {
218
+ const event = afriex.webhooks.verifyAndParse(
219
+ req.body.toString("utf8"),
220
+ req.header(WEBHOOK_SIGNATURE_HEADER) ?? ""
221
+ );
222
+ console.log(event.event);
223
+ res.sendStatus(200);
224
+ } catch {
225
+ res.sendStatus(400);
226
+ }
227
+ }
228
+ );
229
+ ```
230
+
231
+ Fail condition: `express.json()` applied to the webhook route, or
232
+ `JSON.stringify(req.body)` passed to `verify`.
233
+ Fix: mount `express.raw` on the webhook path and verify the exact bytes.
234
+
235
+ ### Check: a missing public key fails closed
236
+
237
+ Expected:
238
+
239
+ ```ts
240
+ import { AfriexSDK } from "@afriex/sdk";
241
+
242
+ const webhookPublicKey = process.env.AFRIEX_WEBHOOK_PUBLIC_KEY;
243
+ if (!webhookPublicKey) {
244
+ throw new Error("AFRIEX_WEBHOOK_PUBLIC_KEY is not set");
245
+ }
246
+
247
+ export const afriex = new AfriexSDK({
248
+ apiKey: process.env.AFRIEX_API_KEY!,
249
+ webhookPublicKey,
250
+ });
251
+ ```
252
+
253
+ Fail condition: the handler logs a warning and processes the payload when
254
+ `verify` returns `false`.
255
+ Fix: assert the key at boot and use `verifyAndParse`, which throws on a missing
256
+ key instead of returning `false`.
257
+
258
+ ### Check: handlers are idempotent across redeliveries
259
+
260
+ Expected:
261
+
262
+ ```ts
263
+ import type { TransactionWebhookPayload } from "@afriex/sdk";
264
+
265
+ export async function onTransaction(
266
+ event: TransactionWebhookPayload,
267
+ seen: { has(key: string): Promise<boolean>; add(key: string): Promise<void> }
268
+ ): Promise<void> {
269
+ const key = `${event.data.transactionId}:${event.data.status}`;
270
+ if (await seen.has(key)) {
271
+ return;
272
+ }
273
+ await seen.add(key);
274
+ console.log("processing", key);
275
+ }
276
+ ```
277
+
278
+ Fail condition: side effects run on every delivery with no deduplication key.
279
+ Fix: deduplicate on `transactionId` plus `status` in durable storage.
280
+
281
+ ## Common Security Mistakes
282
+
283
+ ### HIGH Returning ApiError.message to end users as an error code
284
+
285
+ Wrong:
286
+
287
+ ```ts
288
+ import { ApiError } from "@afriex/sdk";
289
+
290
+ export function toClientError(error: unknown): { code: string } {
291
+ return { code: error instanceof ApiError ? error.message : "UNKNOWN" };
292
+ }
293
+ ```
294
+
295
+ Correct:
296
+
297
+ ```ts
298
+ import { ApiError, AfriexErrorCode } from "@afriex/sdk";
299
+
300
+ export function toClientError(error: unknown): { code: string; message: string } {
301
+ if (error instanceof ApiError) {
302
+ return {
303
+ code: error.errorCode ?? AfriexErrorCode.UNKNOWN_ERROR,
304
+ message: error.details?.friendlyMessage ?? "Payment could not be completed",
305
+ };
306
+ }
307
+ return { code: AfriexErrorCode.UNKNOWN_ERROR, message: "Payment could not be completed" };
308
+ }
309
+ ```
310
+
311
+ `ApiError.message` falls back through `friendlyMessage`, `errorMessage`, and
312
+ `error`, so it can surface internal technical detail as a client-facing code
313
+ and it changes without a version bump.
314
+
315
+ Source: packages/core/src/errors/ApiError.ts
316
+
317
+ ## Pre-Deploy Summary
318
+
319
+ - [ ] API key read from a secret store; no key in source or client bundles
320
+ - [ ] Separate sandbox and production keys; `environment` set explicitly
321
+ - [ ] `retryConfig` configured — retries are off by default
322
+ - [ ] `logLevel` is `LogLevel.ERROR` in production
323
+ - [ ] Every `transactions.create` carries a persisted, reused `meta.idempotencyKey`
324
+ - [ ] Order state settles on `TRANSACTION.UPDATED`, not on `create()` resolving
325
+ - [ ] `topUpSandbox` and `triggerTestWebhook` are unreachable in production
326
+ - [ ] Webhook route uses a raw body parser and `verifyAndParse`
327
+ - [ ] `AFRIEX_WEBHOOK_PUBLIC_KEY` asserted at boot
328
+ - [ ] Webhook handlers deduplicate on `transactionId` plus `status`
329
+ - [ ] Client-facing errors use `errorCode`, not `ApiError.message`
@@ -0,0 +1,272 @@
1
+ ---
2
+ name: afriex-sdk
3
+ description: >
4
+ Entry point for the Afriex Business API TypeScript SDK (@afriex/sdk) — the
5
+ AfriexSDK / Afriex facade and its customers, transactions, paymentMethods,
6
+ balance, rates, checkout, and webhooks services. Covers installation,
7
+ AfriexSDKConfig including webhookPublicKey, choosing sandbox vs production,
8
+ the end-to-end customer → payment method → transaction → webhook payout flow,
9
+ and which sub-skill covers each service. Load when starting an Afriex
10
+ integration, sending or collecting cross-border payments in TypeScript, or
11
+ deciding which Afriex service to call.
12
+ metadata:
13
+ type: core
14
+ library: '@afriex/sdk'
15
+ library_version: '3.0.1'
16
+ sources:
17
+ - 'Afri-exchange/afriex-sdk:packages/sdk/src/index.ts'
18
+ - 'Afri-exchange/afriex-sdk:packages/core/src/config/Config.ts'
19
+ - 'Afri-exchange/afriex-sdk:README.md'
20
+ ---
21
+
22
+ # Afriex SDK
23
+
24
+ `@afriex/sdk` is the umbrella package for the Afriex Business API. `AfriexSDK`
25
+ extends `AfriexClient` from `@afriex/core` and constructs every service against
26
+ one shared `HttpClient`, so a single instance carries the API key, environment,
27
+ retry policy, and logging for the whole integration. Individual services also
28
+ ship as standalone `@afriex/*` packages when only part of the surface is
29
+ needed.
30
+
31
+ ## Setup
32
+
33
+ ```ts
34
+ import { AfriexSDK, Environment } from "@afriex/sdk";
35
+
36
+ export const afriex = new AfriexSDK({
37
+ apiKey: process.env.AFRIEX_API_KEY!,
38
+ environment:
39
+ process.env.NODE_ENV === "production"
40
+ ? Environment.PRODUCTION
41
+ : Environment.STAGING,
42
+ webhookPublicKey: process.env.AFRIEX_WEBHOOK_PUBLIC_KEY,
43
+ retryConfig: {
44
+ maxRetries: 3,
45
+ retryDelay: 1000,
46
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504],
47
+ },
48
+ });
49
+ ```
50
+
51
+ Install with `npm install @afriex/sdk`. Create one instance at module scope and
52
+ import it where needed — each construction builds a fresh HTTP client.
53
+
54
+ ## Services
55
+
56
+ | Need to... | Use | Skill |
57
+ | ------------------------------------------------- | ----------------------- | --------------------------------- |
58
+ | Register senders and recipients, submit KYC | `afriex.customers` | `afriex-customers` |
59
+ | Add bank, mobile money, or virtual account rails | `afriex.paymentMethods` | `afriex-payment-methods` |
60
+ | Send payouts, collect deposits, swap currencies | `afriex.transactions` | `afriex-transactions` |
61
+ | Read wallet balances, fund a sandbox wallet | `afriex.balance` | `afriex-balance` |
62
+ | Quote exchange rates | `afriex.rates` | `afriex-rates` |
63
+ | Host a payment page | `afriex.checkout` | `afriex-checkout` |
64
+ | Verify inbound events, replay test events | `afriex.webhooks` | `afriex-webhooks` |
65
+ | Configure transport, errors, retries | `AfriexClient`, errors | `afriex-core` |
66
+ | Ship to production safely | — | `afriex-go-live` |
67
+
68
+ ## Core Patterns
69
+
70
+ ### End-to-end payout
71
+
72
+ ```ts
73
+ import { AfriexSDK, Environment } from "@afriex/sdk";
74
+ import { randomUUID } from "node:crypto";
75
+
76
+ const afriex = new AfriexSDK({
77
+ apiKey: process.env.AFRIEX_API_KEY!,
78
+ environment: Environment.STAGING,
79
+ });
80
+
81
+ const customer = await afriex.customers.create({
82
+ fullName: "Ada Lovelace",
83
+ email: "ada@example.com",
84
+ phone: "+2348012345678",
85
+ countryCode: "NG",
86
+ });
87
+
88
+ const institutions = await afriex.paymentMethods.getInstitutions({
89
+ channel: "BANK_ACCOUNT",
90
+ countryCode: "NG",
91
+ });
92
+
93
+ const destination = await afriex.paymentMethods.create({
94
+ channel: "BANK_ACCOUNT",
95
+ customerId: customer.customerId,
96
+ accountName: customer.name,
97
+ accountNumber: "0123456789",
98
+ countryCode: "NG",
99
+ institution: {
100
+ institutionId: institutions[0].institutionId,
101
+ institutionCode: institutions[0].institutionCode,
102
+ institutionName: institutions[0].institutionName,
103
+ },
104
+ });
105
+
106
+ const transaction = await afriex.transactions.create({
107
+ customerId: customer.customerId,
108
+ destinationId: destination.paymentMethodId,
109
+ sourceAmount: "100.00",
110
+ sourceCurrency: "USD",
111
+ destinationAmount: "150000.00",
112
+ destinationCurrency: "NGN",
113
+ meta: { idempotencyKey: randomUUID(), reference: "order_9981" },
114
+ });
115
+
116
+ console.log(transaction.transactionId, transaction.status);
117
+ ```
118
+
119
+ The transaction resolves as `PENDING` or `PROCESSING`; settlement arrives as a
120
+ `TRANSACTION.UPDATED` webhook.
121
+
122
+ ### Import only the services you use
123
+
124
+ ```ts
125
+ import { AfriexClient, Environment } from "@afriex/core";
126
+ import { RateService } from "@afriex/rates";
127
+
128
+ const client = new AfriexClient({
129
+ apiKey: process.env.AFRIEX_API_KEY!,
130
+ environment: Environment.STAGING,
131
+ });
132
+
133
+ const rates = new RateService(client.getHttpClient());
134
+ console.log(await rates.getRate("USD", "NGN"));
135
+ ```
136
+
137
+ Every service takes the `HttpClient` returned by `client.getHttpClient()`.
138
+
139
+ ### Handle errors once, at the edge
140
+
141
+ ```ts
142
+ import { AfriexSDK, ApiError, RateLimitError, ValidationError } from "@afriex/sdk";
143
+
144
+ const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY! });
145
+
146
+ export async function safeGetTransaction(id: string) {
147
+ try {
148
+ return await afriex.transactions.get(id);
149
+ } catch (error) {
150
+ if (error instanceof RateLimitError) {
151
+ throw new Error(`rate limited, retry after ${error.retryAfter ?? 1}s`);
152
+ }
153
+ if (error instanceof ValidationError) {
154
+ throw new Error(`bad request: ${JSON.stringify(error.fields)}`);
155
+ }
156
+ if (error instanceof ApiError) {
157
+ throw new Error(`afriex ${error.statusCode} ${error.errorCode}`);
158
+ }
159
+ throw error;
160
+ }
161
+ }
162
+ ```
163
+
164
+ `@afriex/sdk` re-exports everything from `@afriex/core`, so error classes and
165
+ `Environment` come from the same import.
166
+
167
+ ## Common Mistakes
168
+
169
+ ### HIGH Calling afriex.webhookVerifier without configuring a public key
170
+
171
+ Wrong:
172
+
173
+ ```ts
174
+ import { AfriexSDK } from "@afriex/sdk";
175
+
176
+ const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY! });
177
+
178
+ export function handle(rawBody: string, signature: string) {
179
+ return afriex.webhookVerifier!.verifyAndParse(rawBody, signature);
180
+ }
181
+ ```
182
+
183
+ Correct:
184
+
185
+ ```ts
186
+ import { AfriexSDK } from "@afriex/sdk";
187
+
188
+ const afriex = new AfriexSDK({
189
+ apiKey: process.env.AFRIEX_API_KEY!,
190
+ webhookPublicKey: process.env.AFRIEX_WEBHOOK_PUBLIC_KEY!,
191
+ });
192
+
193
+ export function handle(rawBody: string, signature: string) {
194
+ return afriex.webhooks.verifyAndParse(rawBody, signature);
195
+ }
196
+ ```
197
+
198
+ `webhookVerifier` is assigned only when `webhookPublicKey` is present, so the
199
+ non-null assertion throws `Cannot read properties of undefined` at the first
200
+ delivery; `afriex.webhooks` always exists and is the same object when the key is
201
+ configured.
202
+
203
+ Source: packages/sdk/src/index.ts (`if (config.webhookPublicKey) this.webhookVerifier = this.webhooks`)
204
+
205
+ ### HIGH Constructing a new AfriexSDK per request
206
+
207
+ Wrong:
208
+
209
+ ```ts
210
+ import { AfriexSDK, Environment } from "@afriex/sdk";
211
+
212
+ export async function getBalances() {
213
+ const afriex = new AfriexSDK({
214
+ apiKey: process.env.AFRIEX_API_KEY!,
215
+ environment: Environment.PRODUCTION,
216
+ });
217
+ return afriex.balance.getBalance();
218
+ }
219
+ ```
220
+
221
+ Correct:
222
+
223
+ ```ts
224
+ import { AfriexSDK, Environment } from "@afriex/sdk";
225
+
226
+ const afriex = new AfriexSDK({
227
+ apiKey: process.env.AFRIEX_API_KEY!,
228
+ environment: Environment.PRODUCTION,
229
+ });
230
+
231
+ export async function getBalances() {
232
+ return afriex.balance.getBalance();
233
+ }
234
+ ```
235
+
236
+ Each `AfriexSDK` builds a fresh `ky` instance and seven service objects, so
237
+ per-request construction discards connection reuse and multiplies allocation
238
+ under load.
239
+
240
+ Source: packages/sdk/src/index.ts (constructor)
241
+
242
+ ### MEDIUM Omitting environment and defaulting to production
243
+
244
+ Wrong:
245
+
246
+ ```ts
247
+ import { AfriexSDK } from "@afriex/sdk";
248
+
249
+ const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY! });
250
+ ```
251
+
252
+ Correct:
253
+
254
+ ```ts
255
+ import { AfriexSDK, Environment } from "@afriex/sdk";
256
+
257
+ const afriex = new AfriexSDK({
258
+ apiKey: process.env.AFRIEX_API_KEY!,
259
+ environment:
260
+ process.env.NODE_ENV === "production"
261
+ ? Environment.PRODUCTION
262
+ : Environment.STAGING,
263
+ });
264
+ ```
265
+
266
+ `Config` defaults `environment` to `Environment.PRODUCTION`, so an integration
267
+ test written without the field points at the live API and moves real money.
268
+
269
+ Source: packages/core/src/config/Config.ts (environment default)
270
+
271
+ See also: afriex-go-live/SKILL.md — the pre-deploy checklist for this
272
+ configuration.