Skip to content

Changelog ​

Additive changes land under /api/v1; a breaking change ships as /api/v2 alongside it, with at least 90 days' notice. See Versioning.

Choose the phone a top-up prompts ​

POST /topup-intents takes an optional payerIdentifierUid: the payment prompt goes to that phone number, for this top-up only. It must be the uid of one of the wallet's customer's verified phone numbers — identifiers[].uid on GET /customers/{uid}, or the uid returned when you add the number — otherwise the request is refused with 422. Leave it out and nothing changes: the prompt goes to the customer's primary verified phone number, or, if the primary contact is not a verified phone number, to the verified phone number added first. The customer's primary number is not changed. A payment provider that does not prompt the customer's phone refuses the field with 422.

IP allowlists are enforced (fix) ​

If your account has an IP allowlist, a request from an address outside it is now answered 401. The authentication guide has always said so, but the allowlist was not being checked: a correctly signed request was accepted from any address. Signing is unchanged. If you set an allowlist in the client portal, make sure it lists every address your servers call from. An account with no allowlist is not restricted by address.

Top-ups prompt the customer's phone (breaking) ​

A top-up intent now sends the customer's phone a payment prompt for the amount; the customer approves it there instead of paying in and quoting intentReference. The request and response are unchanged. The prompt goes to the customer's verified phone number (the primary one when there are several): a customer without one is now refused with 422. A declined or unanswered prompt is not retried — the intent ends with topup.expired, as before. payerInstructions tells the customer what to do.

ledger_adjustment.completed is not readable by uid (documentation fix) ​

The webhook catalogue said you could read a ledger_adjustment.completed operation by its uid; you cannot. The adjustment is Neo Wallet's own correction, which may touch the wallets of more than one institution, so GET /operations/{uid} and GET /operations/{uid}/receipt answer 404 for it and the payload's links do not resolve. Nothing on the wire changed: keep the payload — the operation's uid, type, amount and times — as your record of the movement.

customer.status_changed payload (fix) ​

customer.status_changed now carries data.customer: { uid, status }, as the customer family has always been documented. It carried customerUid, from, to, reason and occurredAt at the top of data instead, so a receiver reading data.customer.status found nothing. status is the customer's new status; the envelope's occurredAt is when it changed. Neo Wallet's reason for a restriction, suspension or closure is no longer sent: read the customer (GET /customers/{uid}) for its current status.

Receipts of failed payouts (fix) ​

totalDebited on the receipt of a failed or reversed operation now says what the customer paid in the end. It read amount + fee + tax whatever happened. A payout the payment provider refuses returns the amount and every refundable charge to the wallet, so the receipt now shows only the charges that were not refunded — 0.00 when all of them were. Receipts of completed operations are unchanged, receiptHash included; the receipt of a failed operation carries a new receiptHash.

Starter kit: a Postman collection ​

The starter kit's Bruno collection is replaced by a Postman collection, and the API no longer serves its own description. The TypeScript and Python helpers are the same.

  • Removed route — the GET route /openapi.json. Use the API reference on the developer portal, and the Postman collection to call the API from an API client. This lands under /api/v1 as a clean-up, since no integration is live in production yet.
  • Every route, in guided steps — check the connection, onboard a customer and keep its identifiers, agreements and KYC current, credit the wallet and retry under the same Idempotency-Key, top up, pay out, register a mandate and debit on it, transfer between wallets. Each request saves what the next one needs.
  • Customer authentication included — on a payout, a mandate, a debit and a transfer the collection fills in the assertion and computes its transaction binding from the body it sends.
  • Signed for you — the collection signs every request and adds X-Request-Id and, on writes, Idempotency-Key. It runs in Postman and in Newman.
  • Documented in Postman — the set-up, the variables and a 401 checklist are in the collection's overview, and every request describes itself.

A smaller surface: writes, webhooks and one read per resource (clean-up) ​

Six read routes, two scopes and one receipt field are gone; three webhook events and three response fields are new. Everything removed returned data you already hold: you created every customer, identifier, agreement, mandate and operation you could list, got each one back on the write, and receive a webhook on every change. History, search and exports live in the client portal. This lands under /api/v1 as a clean-up — no /api/v2 and no deprecation window, since no integration is live in production yet.

  • Removed routes — the GET routes /wallets/{uid}/transactions, /operations, /mandates, /customers/{uid}/identifiers, /customers/{uid}/agreements and /topup-intents/{uid}. Read an operation, a mandate or a customer by uid instead; the customer summary carries the identifiers and standing agreements.
  • Removed scopes — customers:pii and customer-sessions:write. Every identifier on the API is masked; identifiers[].value leaves the customer summary. A grant that carried either scope has had it removed.
  • Removed field — references.journalUids on the receipt. The operation uid is your traceable reference; no internal accounting identifier is returned anywhere.
  • New fields — identifiers[].payoutEligible on the customer summary (a verified contact identifier that is valid now), and intentReference / expiresAt on GET /operations/{uid} for a topup_c2b (null on every other type).
  • New webhooks — ledger_adjustment.completed, wallet_closure.completed and closed_balance_refund.completed: the three back-office movements on a customer wallet that no call of yours starts. With them, your writes plus the webhook feed show every balance movement.
  • The starter kit follows: the Bruno collection drops the removed requests and reads a mandate by uid.

Amounts as decimal strings (breaking) ​

Every money field is now a decimal string with two decimal places, and its name no longer ends in Minor. What used to be the integer 250000 is now the string "2500.00". This affects every request body, every response and every webhook payload, so update your integration before your next call. It lands under /api/v1: there is no /api/v2.

  • Responses carry exactly two decimal places, including the trailing .00 and a leading - on a negative figure: "1250.00", "0.00", "-0.01". Do no rounding of your own.
  • Requests may send zero, one or two places — "1250", "1250.5" and "1250.50" are the same amount. A JSON number is refused with 400 BadRequest, and so are an exponent, a +, a leading zero, surrounding whitespace, a thousands separator and a bare .5 or 12..
  • A third decimal place is refused with the new AmountPrecisionExceeded code and a 400, as is an amount above 18 integer digits. An amount you send is never rounded for you.
  • Field names drop the suffix: amountMinor is amount, feeMinor is fee, taxMinor is tax, totalMinor is total, totalDebitedMinor is totalDebited, fixedOrMaxAmountMinor is fixedOrMaxAmount, maxBalanceMinor is maxBalance, maxSingleTxnMinor is maxSingleTxn, aggregateDailyLimitMinor is aggregateDailyLimit, monthlyOutLimitMinor is monthlyOutLimit, and a wallet balance reads current, held and available.
  • The transaction binding hashes the amount as two decimal places under the new template customerUid|walletUid|amount|currency|destinationIdentifierUid|clientReference. Echo that template as transactionBinding.canonical and hash 7500.00 where you hashed 750000; the old template is refused with AssertionNotBoundToTransaction.
  • The starter kit follows: transactionBinding() takes amount as a string, and the test vectors carry the new bodies, signatures and binding hash.

v1 ​

Initial release.

  • Access — every request is signed with your API key and Ed25519 key; scopes decide what each route allows; every write takes an Idempotency-Key and an X-Request-Id; every error has the same shape.
  • Customers — onboard a customer with its main wallet; read the summary; manage identifiers, agreement acceptances and KYC level changes.
  • Wallets — balance and statement.
  • Money movement — wallet credits, wallet debits (customer present, or on a standing mandate), payouts, top-up intents and fee quotes.
  • Mandates — create, list, read and revoke.
  • Operations — list, status with timeline, and receipt.
  • Webhooks — 16 signed event types, retried for about 38 hours.

Added since the initial release:

  • Wallet profiles — POST /customers takes an optional rulesProfileKey, the wallet profile to open the customer's wallet on; omit it for your account's default. The customer responses return the rulesProfileKey the wallet opened on. A profile, KYC level (kyc.tierCode) or payment provider (railCode) your account is not approved for is refused with 422.
  • Wallet transfers — POST /wallet-transfers moves money between two wallets of your own customers: to another customer (transferCategory: "p2p") or into your business collection wallet ("merchant"). It needs the new wallet-transfers:write scope, which no existing account holds until it is granted, and the sending customer's assertion, bound with destinationWalletUid as the binding target. The new transfer.completed webhook carries the operation with sourceWalletUid and destinationWalletUid; those two fields appear on wallet transfers only. The transfer shows on both wallets' statements, with the other party as a masked name, and GET /operations/{uid}/receipt takes an optional walletUid: set it to the wallet that was paid for the recipient's receipt, which has no fee lines.
  • Customer conflicts — a 409 on POST /customers is returned only when your account already has a customer with that verified identifier.
  • Required agreements — POST /customers is refused with 422 RequiredAgreementsMissing when agreements does not carry the customer's acceptance of wallet_opening, privacy and fee_schedule; nothing is created. Earlier such a request created the customer and wallet in pending, a state no request could leave.
  • Leaner onboarding — POST /customers now needs only what Neo Wallet cannot work out: customerType defaults to person; an identifier needs just type and value (class is derived for phone, email and identity-document types, the first identifier is primary, a contact identifier opens verified and a document pending); kyc.verificationSource and kyc.sourceReference are optional; an agreement's documentType defaults to its type and acceptedChannel to client_channel. A value you send always wins. kyc.reviewType is removed — sending it is refused with 400 like any other undeclared field.
  • Customer reference — clientReference on POST /customers is now unique among your customers: a second customer with the same value is refused with 409. It is returned on the customer responses as before and now also on the customer.created webhook, and the client portal's customer list finds a customer by it.