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. You sign requests with the private key; Neo Wallet only ever holds the public key.

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. register pub.pem → you get a key id (kid_…);
  2. rotate 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. To explore the API from an API client first, the kit's Postman 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, amount: "2500.00", 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 a decimal string

"2500.00" is ETB 2,500.00. Every amount is a string with at most two decimal places — never a JSON number, never a float in your own code. See Amounts.

Got a 401? ​

A 401 has the same body whichever check failed. Check, in this order:

  1. Clock — X-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 id — X-Key-Id names the key you signed with, and it is still active.

Next ​

  • Amounts — how money is written on the wire.
  • 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.