TypeScript and Node.js SDK
@anis-ly/partners — for Node.js 22 and later, written in TypeScript, with ES module and CommonJS builds and full
types. It uses Node’s own fetch and WebCrypto; its one dependency is the OpenTelemetry API.
- Package: npmjs.com/package/@anis-ly/partners
- Source code: github.com/anis-ly/anis.partners-sdk-node
An optional helper, provided as is
Every Anis SDK is provided free of charge under the MIT License, “as is”, without warranty of any kind. Anis (Aniscom for Technical Services) accepts no responsibility or liability for its use or for any loss arising from it. You remain responsible for your own integration — recording orders before you send them, recovery, key custody and testing. The source code is public: read it to understand exactly what it does before you rely on it. You do not need an SDK — you can call the Anis Partner API directly using these guides and the API reference.
SDK 2.2.0. SDK 1.x is deprecated; see the changelog.
Install
npm install @anis-ly/partners@2.2.0
Every release is published from GitHub with a signed provenance statement, which npm shows on the package page.
Configure
import { AnisPartnersClient, PemP256Signer } from '@anis-ly/partners';
const signer = (await PemP256Signer.fromPemFile('/secure/partner-key.pem')).forKey(keyId);
const client = AnisPartnersClient.create({
options: {
authority: 'https://<the address Anis gave you>',
signatureLifetimeSeconds: 60,
acceptLanguage: 'ar',
signingKeyCacheSeconds: 300,
timeoutMs: 30_000,
},
signer,
});
The settings are checked when you create the client: an address that is not HTTPS (plain HTTP is accepted only on your own machine), a signature lifetime outside 1 to 60 seconds, or a key cache longer than 5 minutes stops it there.
The signer is a separate step on purpose. PemP256Signer signs with a key file. For a vault or hardware module,
implement RequestSigner — your keyId and a sign(bytes) method that returns the 64-byte signature — and the key
never enters your process.
Serverless and multi-instance hosts: pass a keyCache (get and set) so instances share Anis’s public
signing-key document instead of each fetching it. Use a cache only your application can write.
The SDK keeps Anis’s key list for at most 5 minutes, counted from when it was fetched, even when it comes from the shared cache.
Every call accepts { signal }, an AbortSignal that cancels everything the call does.
Use it
const profile = await client.profile.get();
for await (const wallet of client.wallets.list()) { // every page, followed for you
console.log(wallet.name ?? wallet.id, wallet.balance.toString());
}
for await (const card of client.catalogue.listCards(walletId, subcategoryId)) {
console.log(card.id, card.unitPrice?.toString());
}
const credential = await client.ownedCards.reveal(walletId, soldCardId);
Amounts are Money values with exactly three decimal places — never JavaScript numbers. Money.of('10.500', 'LYD')
and price.multiply(2) do exact arithmetic.
What it verifies
SDK 2.0.1 and later verify every answer of the calls Anis signs — place an order, read an order, reveal a
card, reveal an invoice’s cards, the four enrolment calls, application suspension, key revocation and the signature self-check — success and every refusal.
It requires the signature there: an answer on one of these calls without a valid signature is thrown away
(UnverifiableResponseError) and, on an order, comes back as unknown. It does not verify the information calls — your
profile, wallets, the catalogue and your owned cards (list and read): their answers carry no signature, and TLS protects
them. It still signs every request. See Verifying answers for which answers are signed.
Orders
const price = card.unitPrice;
if (!price) throw new Error('This card has no price for this wallet.');
const operationId = crypto.randomUUID(); // yours
const request = { cardId: card.id, quantity: 2, expectedUnitPrice: price, expectedTotal: price.multiply(2) };
await saveOrderIntent(operationId, walletId, request); // store it BEFORE sending
const result = await client.orders.create(walletId, operationId, request);
if (result.kind === 'completed') await storeCredentials(result.credentials); // store FIRST
switch (result.kind) {
case 'completed':
if (result.codesWithheld) await reportCodesWithheld(operationId); // paid, codes withheld: do not buy again; write to support@anis.ly
break;
case 'replayed':
await ensureStored(result.order); // sent earlier: reveal by invoice if you never stored them
break;
case 'processing':
case 'unknown':
await resumeLater(operationId, result.suggestedDelayMs); // may still complete: resume with the SAME id
break;
case 'notPlaced':
await closeNotPlaced(operationId, result.refusal); // nothing bought: fix the cause, new order, new id
break;
}
client.orders.resume(walletId, operationId, request) sends the same order again under the same id — the way to
recover — and returns the same five outcomes. Recovery is always this signed resend, never a loop of
client.orders.get(), which only reads state. An order whose status is recoveryExhausted is not failed: keep
resuming the same id, slowly (minutes, not seconds), and tell support@anis.ly the operation
id. See Orders and recovery for the full rules.
No retry wrapper around fetch
A retrying fetch resends the same signed request with the same one-time value, and Anis refuses the
copy. Recover orders with orders.resume(), which signs a fresh request.
Refusals
An order does not throw a refusal: it comes back as notPlaced or unknown. Other calls reject with an
AnisApiError (or a subclass such as PriceChangedError or InsufficientBalanceError) carrying code, requestId,
retryAfter and isReplayed. Each error page names the code.
| Error | Means | On an order |
|---|---|---|
UnverifiableResponseError |
An answer on a signed call could not be shown to come from Anis; it was thrown away | Not thrown — unknown: resume with the same id |
LimitExceededError (spending_limit_exceeded, 429) |
The order would pass the application’s or wallet’s spending cap for the current window; nothing was bought or charged | Not thrown — notPlaced. After retryAfter seconds, place a new order with a new id; never resume this one |
RequestSigningError |
Your signer failed; nothing was sent | Thrown — send it again once the signer works |
TypeError, RangeError |
Your input was wrong (an id that is not a UUID, a total that is not unit price × quantity); nothing was sent | Thrown before anything is sent |
Every SDK error extends AnisPartnersError, so one instanceof check covers them all.
Observability
Pass any logger with debug, info, warn and error (pino, winston and console all fit) as logger; traces and
metrics go through OpenTelemetry under @anis-ly/partners. No key, signature, one-time value or card code is ever
recorded. See Observability for what to alert on.
Methods and card answers
Owned-card calls use /v1/wallets/{walletId}/owned-cards, its /{soldCardId} read and
/{soldCardId}/reveal, and /v1/wallets/{walletId}/invoices/{invoiceId}/owned-cards/reveal.
Catalogue card reads use GET /v1/wallets/{walletId}/catalog/cards/{cardId}.
CardSummary is shared by owned cards, reveals and order soldCards: card id and localized name,
optional face value and logo, subcategory and category. Owned cards use soldCardId and operationId.
Revealed credentials inside orders include invoice id, card summary and purchase time too. Empty strings
and empty localized text are omitted. Money is a string with three decimals and an uppercase three-letter
currency — negative only for a wallet balance while the wallet uses allowed debt; read a wallet’s currency from balance.currency.
The SDK requires the v2 signing tag, both request signature headers in the covered list, and the key-document lifecycle checks. See Verifying answers. Stops are immediate; only Anis staff can resume an application, and revoking your current key requires new enrolment. See Security.
const reason = "Suspected compromise";
const catalogueCard = await client.catalogue.getCard(walletId, cardId);
const suspended = await client.access.suspendApplication(reason);
const revoked = await client.access.revokeKey(keyId, reason);
Product resources in SDK 2.2.0
SDK 2.2.0 adds meterCharges, directRechargeProducts (products, locations, quote), directRecharges (create, a list of your own, read and recipient updates), webhookEndpoint, webhookEvents, a webhook signature verifier and updated error types. Upgrade from 2.1.0 to use them. Follow the meter, Direct recharge and webhook guides for HTTP behavior. Keep the operation id and exact body before a money-moving call; a lost answer is recovered with the same id. Each SDK’s README and its docs/webhooks.md give the method names and the verifier’s arguments.