Appearance
Quickstart
From nothing to a signed 200 in about ten minutes. You need Node ≥ 22 (or Python ≥ 3.9) and openssl.
1. Get access
Neo Wallet gives your organisation a client portal account. You create and manage your API credentials there yourself — your private key never leaves your side.
| Environment | Use it for | Portal and API base URL |
|---|---|---|
| Test | Building and testing | Provided by Neo Wallet |
| Production | Live traffic, new keys | Provided at go-live |
2. Create credentials
Generate an Ed25519 key pair. The private key never leaves your side.
bash
openssl genpkey -algorithm ed25519 -out key.pem
openssl pkey -in key.pem -pubout -out pub.pemIn the client portal, open API credentials (you need the owner role) and:
- registers
pub.pem→ you get a key id (kid_…); - rotates the API key → you get an API key (
nwak_…), shown once.
bash
export NEO_BASE_URL="https://<test-api-host>"
export NEO_API_KEY="nwak_…"
export NEO_KEY_ID="kid_…"
export NEO_SIGNING_KEY="$(cat key.pem)"3. Send a signed request
Download the kit and put neo-wallet-signing.ts next to this file:
ts
// balance.ts — run with: node --experimental-strip-types balance.ts <walletUid>
import { buildRequest, loadSigningKey } from "./neo-wallet-signing.ts";
const credentials = {
apiKey: process.env.NEO_API_KEY!,
keyId: process.env.NEO_KEY_ID!,
privateKey: loadSigningKey(process.env.NEO_SIGNING_KEY!),
};
const path = `/api/v1/tenant/wallets/${process.argv[2]}/balance`;
const { method, headers } = buildRequest(credentials, { method: "GET", pathWithQuery: path });
const response = await fetch(process.env.NEO_BASE_URL + path, { method, headers });
console.log(response.status, await response.json());No wallet yet? Onboard a customer first — the response carries its main wallet's uid. Prefer clicking to coding? The kit's Bruno collection signs every request for you.
4. Make a write
Writes add an Idempotency-Key. Persist it before sending so a retry can reuse it.
ts
import { buildRequest, ulid } from "./neo-wallet-signing.ts";
const idempotencyKey = ulid();
const path = "/api/v1/tenant/wallet-credits";
const request = buildRequest(credentials, {
method: "POST",
pathWithQuery: path,
json: { walletUid, amountMinor: 250000, currency: "ETB", clientReference: "PAYROLL-2026-09-A" },
idempotencyKey,
});
const response = await fetch(process.env.NEO_BASE_URL + path, request);
// Timeout or 5xx? Call buildRequest again with the SAME idempotencyKey and resend.Money is integer minor units
250000 is ETB 2,500.00. There are no decimals anywhere in the API.
Got a 401?
Every authentication failure returns the same body on purpose. Check, in this order:
- Clock —
X-Timestampis epoch seconds, within ±300 s of real time. - Path — you signed exactly what you sent, including
/api/v1and the query string. - Body — you signed the exact bytes you sent, not a re-serialisation.
- Key id —
X-Key-Idnames the key you signed with, and it is still active.
Next
- Core flows — which calls make up a credit, a debit, a payout, a top-up.
- Webhooks — get told when an operation settles.
- Going live — the checklist.