@andeslabs/mint-sdk
v4.0.0
Published
TypeScript SDK for the Andes Mint API (https://andeslabs.io/mint/v1).
Maintainers
Readme
@andeslabs/mint-sdk
TypeScript SDK for the Andes Mint API. Wraps all public outbound endpoints, signs ES256 JWTs per request, unwraps response envelopes, and ships a webhook signature verifier.
- Node 18+ (uses built-in
fetch,FormData,node:crypto). - Zero runtime dependencies.
- Server-side only — the SDK holds a signing key.
Install
npm install @andeslabs/mint-sdkAuthentication
Andes uses ES256 (ECDSA P-256) signed JWTs plus a project API key. Generate a key pair once and share the public key during onboarding:
openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
openssl ec -in ec_private.pem -pubout -out ec_public.pemThen create a client:
import { readFileSync } from "node:fs";
import { createClient } from "@andeslabs/mint-sdk";
const andes = createClient({
apiKey: process.env.ANDES_API_KEY!,
privateKey: readFileSync("ec_private.pem", "utf8"),
});Every request signs a fresh 2-minute JWT. Pass issuer if you want an iss claim embedded (the backend does not verify it, so it's optional).
Using a custom signer (KMS / HSM)
Pass a signer function instead of a PEM to keep the private key outside SDK memory:
const andes = createClient({
apiKey: process.env.ANDES_API_KEY!,
signer: async () => kms.signAndesJwt(),
});Usage
const { userId } = await andes.accounts.create({ name: "Juan Pérez" });
const { address } = await andes.wallets.create("base", { user_id: userId, asset: "arsa" });
const wallets = await andes.wallets.forUser(userId);
const stats = await andes.project.stats();The response envelope ({ status, message, data }) is unwrapped — methods resolve with data directly.
Errors
Non-2xx responses throw AndesApiError:
import { AndesApiError } from "@andeslabs/mint-sdk";
try {
await andes.wallets.create("base", { user_id, asset: "arsa" });
} catch (err) {
if (err instanceof AndesApiError) {
console.error(err.httpStatus, err.status, err.message, err.body);
}
}err.retryAfter carries the Retry-After header (seconds) when the API sends one. The first outbound transfer or fiat withdrawal from a freshly provisioned Stellar wallet returns 425 Too Early while the account is being activated — nothing is created on that response; retry after the delay and it succeeds.
Resource reference
| Namespace | Methods |
|---|---|
| accounts | create, list |
| wallets | create(chain, body), list, forUser |
| wallets.transfers | create, list, forUser, forUserSingle |
| fiat | create(body, files?), createBusiness(body), updateAlias(body), list, forUser, arca, cvuLookup, withdraw, movements, movementsForUser, movementForUser |
| international.accounts.fiat | list, createBob, createPen, createPyg, delete(id, { userId }) |
| international.quotes | arsBob, arsPen, arsPyg |
| international.offramp | createArsBob, createArsPen, createArsPyg, list |
| international.banks | bob, pen, pyg |
| international.cotization | () |
| project | stats, statsTimeseries |
| webhooks | create, list, get, update, delete, deliveries, signingKey |
Multipart: creating a fiat account
import { readFileSync } from "node:fs";
await andes.fiat.create(
{
user_id,
chain: "base",
email: "[email protected]",
cuit: "20345678901",
name: "Juan",
last_name: "Pérez",
phone: "+5491145678901",
birthdate: "1990-05-15",
},
{
face: { buffer: readFileSync("face.jpg"), filename: "face.jpg", contentType: "image/jpeg" },
id_front: { buffer: readFileSync("id-front.jpg"), filename: "id-front.jpg", contentType: "image/jpeg" },
id_back: { buffer: readFileSync("id-back.jpg"), filename: "id-back.jpg", contentType: "image/jpeg" },
}
);The personal fields and KYC images are required only for first-time onboarding of a CUIT. Once a person has a completed fiat account, attach a CVU on another chain with a minimal request — no personal fields, no files:
await andes.fiat.create({ user_id, chain: "stellar", cuit: "20345678901" });Webhooks
Andes signs outbound webhooks with ECDSA-SHA256. The signed string is `${timestamp}.${rawBody}`; the signature is hex-encoded. Fetch the public key once from GET /webhooks/signing-key and cache it.
Your handler must verify against the raw request body — do not re-serialize JSON.
import { createServer } from "node:http";
import { verifyWebhookSignature, type WebhookEventPayload } from "@andeslabs/mint-sdk";
const PUBLIC_KEY = process.env.ANDES_WEBHOOK_PUBLIC_KEY!; // PEM string
createServer(async (req, res) => {
const chunks: Buffer[] = [];
for await (const c of req) chunks.push(c as Buffer);
const rawBody = Buffer.concat(chunks);
const ok = verifyWebhookSignature({
rawBody,
timestamp: String(req.headers["x-webhook-timestamp"] ?? ""),
signature: String(req.headers["x-webhook-signature"] ?? ""),
publicKey: PUBLIC_KEY,
maxAgeMs: 5 * 60 * 1000,
});
if (!ok) return res.writeHead(401).end();
const event = JSON.parse(rawBody.toString("utf8")) as WebhookEventPayload;
// handle event …
res.writeHead(200).end();
}).listen(3000);Event types
fiat.account.created, fiat.deposit.success, fiat.deposit.failed, fiat.withdrawal.success, fiat.withdrawal.failed, crypto.transfer.success, crypto.transfer.failed, wallet.active, international.offramp.success, international.offramp.failed.
WebhookEventPayload is a discriminated union on event — narrow it in a switch, or use the dispatchWebhookEvent helper below.
Typed event dispatcher
Skip the switch with a typed handler map. Each callback receives only the matching variant, unknown event names fail to compile, and any events you don't list are silently ignored.
import {
dispatchWebhookEvent,
type WebhookEventHandlers,
type WebhookEventPayload,
} from "@andeslabs/mint-sdk";
const handlers: WebhookEventHandlers = {
"fiat.deposit.success": async (e) => credit(e.user_id, e.amount, e.mint_receipt_hash),
"fiat.withdrawal.success": async (e) => settle(e.id, e.destination_cbu),
"crypto.transfer.failed": async (e) => flag(e.id, e.to_address),
};
// inside your request handler, after verifying the signature:
const event = JSON.parse(rawBody.toString("utf8")) as WebhookEventPayload;
await dispatchWebhookEvent(event, handlers);Returning a value from handlers
WebhookEventHandlers<R> is generic — every handler returns R, and dispatchWebhookEvent resolves to R | undefined (undefined when no handler matched the event). Useful when the HTTP layer needs to react to what a handler decided:
const handlers: WebhookEventHandlers<{ retry: boolean }> = {
"fiat.deposit.success": async (e) => ({ retry: !(await credit(e.user_id, e.amount)) }),
};
const result = await dispatchWebhookEvent(event, handlers);
if (result?.retry) return res.sendStatus(503); // Andes will retry
res.sendStatus(200);If a handler throws, return a non-2xx to trigger the Andes retry schedule. See examples/webhook-dispatcher.ts.
Headers sent with each delivery
| Header | Description |
|---|---|
| X-Webhook-Signature | hex ECDSA-SHA256 signature of ${timestamp}.${rawBody} |
| X-Webhook-Event | Event type |
| X-Webhook-Delivery-Id | UUID |
| X-Webhook-Timestamp | Unix ms |
Retries: up to 5 attempts with backoff 10s → 60s → 5m → 30m → 2h.
Releasing
See RELEASE.md for the publish workflow, version bumps, and deprecation steps.
License
MIT
