Skip to content

Node.js SDK

The SDK is a thin client for the Asqary API. It handles authentication, identifier hashing, retries, timeouts, request IDs and typed errors. It needs Node.js 20.3 or later, ships TypeScript types, supports ESM and CommonJS, and has no runtime dependencies.

Terminal window
npm install @twalle/asqary

You need two secrets, both kept on your server:

Setting Environment variable What it is
apiKey ASQARY_API_KEY Your API key. asq_test_… keys use test data and asq_live_… keys use live data.
identifierKey ASQARY_IDENTIFIER_KEY Your key for hashing identifiers. You generate it and keep it.
import { Asqary } from "@twalle/asqary";
const asqary = new Asqary({
apiKey: process.env.ASQARY_API_KEY,
identifierKey: process.env.ASQARY_IDENTIFIER_KEY,
baseUrl: "https://api-staging.asqary.com", // the default until production is live
timeout: 30_000, // ms per attempt
maxRetries: 2,
});

Create one client and share it. It is safe for concurrent use.

const decision = await asqary.score({
transactionId: "txn_123",
type: "loan_disbursement",
amount: 150_000, // minor units: ₦1,500.00
currency: "NGN",
occurredAt: new Date(),
customerId: "customer_456",
deviceId: "device_789",
beneficiaryAccount: "0123456789",
beneficiaryBankCode: "058",
});
switch (decision.recommendedAction) {
case "approve":
return approve();
case "step_up":
return requestAdditionalVerification();
case "manual_review":
return sendToReviewQueue(decision.decisionId);
case "block":
return decline();
}

Give each identifier either raw (customerId, hashed for you) or already hashed (customerIdHash), never both.

Each reason has a code, a contribution in points, a message and details with the evidence:

for (const reason of decision.reasons) {
console.log(`${reason.code} +${reason.contribution}: ${reason.message}`);
}

Every error is an AsqaryError with code, message, statusCode, requestId (quote it to support), details and retryable.

import { AsqaryError, RateLimitError, ValidationError } from "@twalle/asqary";
try {
await asqary.score(transaction);
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.fieldErrors);
} else if (error instanceof RateLimitError) {
console.error(`retry in ${error.retryAfter}s`);
} else if (error instanceof AsqaryError) {
console.error(error.code, error.statusCode, error.requestId);
}
throw error;
}

Errors never include your API key, identifier key or transaction data. See Errors for the full list.

  • Retries: up to maxRetries (default 2), with exponential backoff and jitter. 429, 502 and 503 are always retried. Network errors, timeouts, 500 and 504 are retried when the call is safe to repeat. 400, 401, 403, 404, 409, 413 and 422 are never retried. Retries reuse the same request ID and idempotency key.
  • Timeouts: 30 seconds per attempt by default. Override per client or per call: asqary.score(tx, { timeout: 2_000 }).
  • Idempotency: scoring is idempotent on transactionId. Creating policies, backtests and feedback sends an Idempotency-Key, which the SDK generates per call. Pass your own to make retries safe across restarts: asqary.policies.create(input, { idempotencyKey: "policy-123" }).
  • Cancellation: pass an AbortSignal: asqary.score(tx, { signal }).
  • Request IDs: every call has one, on decision.requestId and error.requestId. Pass your own with { requestId } to correlate with your logs.
await asqary.rules.list();
await asqary.policies.create({ /* … */ });
await asqary.policies.activate(policy.policyId);
await asqary.decisions.get(decision.decisionId);
await asqary.backtests.create({ /* … */ });
await asqary.webhooks.create({ /* … */ });
await asqary.feedback.create({ /* … */ });

These are covered in Policies, Backtesting, Webhooks and Feedback.

API keys, identifier keys and payloads are never logged or included in errors. Raw identifiers are hashed in your process and only hashes are sent. HTTPS is required (plain HTTP is allowed only for localhost). The SDK sends no telemetry and stores nothing on disk.