221 SDKs
The official TypeScript, Python and Dart SDKs: installation, first call, creating a payment and verifying a webhook.
The SDKs are generated from the API's OpenAPI document. They verify webhook signatures and embed a snapshot of the small reference datasets (public holidays, operator prefixes, banks, geography down to the commune) for offline use.
Not published yet
The SDKs are not published yet: they will be available on npm, PyPI and pub.dev at release. The install commands below will work then. Until then, the API is called directly: see First call.
| Package | Language | Version |
|---|---|---|
@221/sdk | TypeScript | 0.2.0 |
sdk-221 (module sdk221) | Python | 0.2.0 |
sdk221 | Dart and Flutter | 0.2.0 |
The examples read API_URL (the base URL, https://apps.orvlabs.com), API_KEY (the key,
shaped sk_221_pay_test_… or sk_221_pay_live_…) and WEBHOOK_SECRET from the
environment. Payment routes require a key; data routes also work without one,
with a low quota.
Idempotency-Key is unique to each payment: sending the same value again does
not create a duplicate. A webhook is verified on the raw body, before any JSON
decoding, with the X-221-Timestamp and X-221-Signature headers; after
5 minutes the signature is rejected. A delivery can be replayed: deduplicate on
the event identifier. Field details are in 221 Pay.
TypeScript
npm install @221/sdkimport { createApiClient } from '@221/sdk';
const api = createApiClient({ baseUrl: process.env.API_URL, apiKey: process.env.API_KEY });
const { data, error } = await api.POST('/v1/payments', {
params: { header: { 'Idempotency-Key': 'order-1042' } },
body: {
amount: '5000',
currency: 'XOF',
rail: 'sn_wave',
merchant_reference: 'order-1042',
return_url: 'https://shop.example/payment/return',
customer_phone: '77 123 45 67',
},
});
if (error) throw new Error(`${error.code}: ${error.message}`);import { verifyWebhook } from '@221/sdk/webhook';
// Express: app.post('/webhooks/221', express.raw({ type: 'application/json' }), (req, res) => …)
const ok = verifyWebhook(process.env.WEBHOOK_SECRET!, req.header('X-221-Timestamp') ?? '', req.body, req.header('X-221-Signature') ?? '');
if (!ok) return res.sendStatus(401);Python
uv add sdk-221 # or: pip install sdk-221, in a virtual environmentimport os
from sdk221 import client
api = client(os.environ["API_KEY"], os.environ["API_URL"])
response = api.get_httpx_client().post(
"/v1/payments",
headers={"Idempotency-Key": "order-1042"},
json={
"amount": "5000",
"currency": "XOF",
"rail": "sn_wave",
"merchant_reference": "order-1042",
"return_url": "https://shop.example/payment/return",
"customer_phone": "77 123 45 67",
},
)
response.raise_for_status()
payment = response.json()from sdk221.webhook import verify_webhook
# FastAPI: raw_body = await request.body()
if not verify_webhook(WEBHOOK_SECRET, request.headers.get("X-221-Timestamp", ""), raw_body, request.headers.get("X-221-Signature", "")):
raise HTTPException(status_code=401)Dart and Flutter
dart pub add sdk221 # or: flutter pub add sdk221import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' show MultipartFile;
import 'package:sdk221/sdk221.dart';
Future<void> main() async {
final env = Platform.environment;
final client = createApiClient(apiKey: env['API_KEY'], baseUrl: env['API_URL'] ?? defaultBaseUrl);
final payment = await PaymentsApi(client).paymentsCreate(
MultipartFile.fromString('body', jsonEncode({
'amount': '5000',
'currency': 'XOF',
'rail': 'sn_wave',
'merchant_reference': 'order-1042',
'return_url': 'https://shop.example/payment/return',
'customer_phone': '77 123 45 67',
})),
idempotencyKey: 'order-1042',
);
print(payment);
}import 'package:sdk221/sdk221.dart';
final ok = verifyWebhook(webhookSecret, request.headers['x-221-timestamp'] ?? '', rawBody, request.headers['x-221-signature'] ?? '');
if (!ok) return Response(401);Limitation: the generated client imports dart:io. It works on the Dart VM,
Android, iOS and desktop, not on Flutter web, where the key must not live
anyway.
Offline
The embedded public holidays cover the years present in the data at the time
of the release. holidays(2099) throws an error instead of returning an
empty list: for another year, call GET /v1/jours-feries?year=.