Skip to content

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.

EnvironmentUse it forPortal and API base URL
TestBuilding and testingProvided by Neo Wallet
ProductionLive traffic, new keysProvided 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.pem

In the client portal, open API credentials (you need the owner role) and:

  1. registers pub.pem → you get a key id (kid_…);
  2. 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:

  1. ClockX-Timestamp is epoch seconds, within ±300 s of real time.
  2. Path — you signed exactly what you sent, including /api/v1 and the query string.
  3. Body — you signed the exact bytes you sent, not a re-serialisation.
  4. Key idX-Key-Id names 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.