What is Moneva Platform?
Moneva Platform is the account layer for your product: onboard a customer with hosted KYC, give them named virtual fiat accounts, and move money in and out across 130+ countries on stablecoin rails. One REST API, one secret key, everything event-driven.
Features
How it works
Your backend talks to one REST API with one secret key. Underneath, Moneva orchestrates licensed banking, compliance and settlement partners. Their sessions, retries, reconciliation and corridor quirks are handled inside the platform and never surface in your integration.
Self-custody, always. Every customer balance settles to a wallet only they control: their own wallet or the embedded wallet of your app. Moneva never generates, stores, or has access to keys or funds.
KYC and regulatory compliance are owned by licensed partners. Your product embeds a hosted verification flow and reads back a clean status. You never store documents or make compliance decisions.
Status: the sandbox is live and self-serve today. Live mode is in development. Pricing: flat monthly platform fee plus a per-verification fee, with no basis points on volume from Moneva.
Next steps
Your first payout in 15 minutes
1. Create a customer
The customer starts in kyc_required. The wallet address is the customer's own; Moneva is self-custody only.
2. Register your webhook
Everything that happens next arrives as signed events. Register an endpoint once; the signing secret is returned only on creation.
No endpoint yet? GET /v1/events serves the same events as a queryable log.
3. Verify identity
Create a hosted KYC session and open the returned URL in your product. In test mode, force the outcome instead:
Your webhook receives customer.kyc.approved; the customer is active.
4. Give them an account
Provisioning is asynchronous: 202 now, then account.activated delivers the IBAN.
5. Simulate a deposit
6. Add a bank destination
Fetch the corridor's form schema, render it, submit the fields:
Removing a beneficiary deactivates it instead of erasing it: past transfers keep their reference for audit, new transfers with it return 422 beneficiary_deactivated, and beneficiary.deactivated fires.
7. Quote, then transfer
Watch transfer.created then transfer.processing arrive on your webhook, then force the terminal state with POST /v1/test/transfers/{id}. That is the whole integration.
Next steps
Integrate your way
The ladder
sdk-ts): zero-dependency typed client, webhook verification, and the one-call wallet-session prompt. Tested end to end against the gateway; npm publish pending.sdk-react): useCorridors, useBeneficiarySchema, useProfileSchema, and a <BeneficiaryForm> that renders itself from the corridor schema. Bring your own styling.verifyWebhook, an Express handler, and a Next.js route helper, with timing-safe signature checks and typed events.The SDK, as implemented
Zero dependencies, field names identical to the REST API, every call typed:
import Moneva, { verifyWebhook } from "@moneva/platform"; const moneva = new Moneva(process.env.MONEVA_KEY); const customer = await moneva.customers.create({ email: "maria@example.com", first_name: "Maria", last_name: "Silva", country: "BR", wallet: { address: userWallet }, }); await moneva.walletSessions.prompt(customer.id, window.ethereum); // one call const quote = await moneva.quotes.create({ customer_id: customer.id, source: { currency: "USDC", amount: "100.00" }, destination_currency: "BRL" }); await moneva.transfers.create({ ... }, { idempotencyKey: order.id }); const event = verifyWebhook(rawBody, req.headers["moneva-signature"], secret);
For AI agents
Point a coding agent at /llms.txt and it gets the full picture: every docs page as text, the spec, the event catalog, and the public corridor and jurisdiction endpoints it can call without a key. The playground's copy-page button serves the same content per page for humans pasting into a chat.
One source of truth. The SDKs, components, docs, and agent bundles are all generated from openapi.yaml and the corridor registry. When a corridor or field changes, every integration path updates together.
Next steps
Authentication
Authorization: Bearer mk_test_4f8a... # test mode Authorization: Bearer mk_live_9c2d... # live mode (in development)
| Key prefix | Mode | Behavior |
|---|---|---|
mk_test_ | Test | Deterministic simulator. Instant, free, resettable. Data is fully isolated from live. |
mk_live_ | Live | In development Returns live_mode_disabled today. |
Secret keys grant full account access. Keep them server-side, never in client code, and rotate immediately if exposed.
Getting and rotating keys
- Test keys are issued when your organization is created. The self-serve developer dashboard is in development; until it ships, keys are provisioned by Moneva.
- Rotation: keys can be revoked and reissued without downtime; old keys stop authenticating immediately on revocation.
- Scope: a key belongs to one organization and one mode. Test and live data are fully isolated.
Auth errors
| Code | Status | When |
|---|---|---|
invalid_api_key | 401 | Missing, malformed, unknown or revoked key |
live_mode_disabled | 403 | Live key used while live mode is in development |
test_mode_only | 403 | Live key calling a /v1/test/* simulation endpoint |
Next steps
Customers & KYC
Create a customer
| Field | Notes |
|---|---|
email | Required. Unique per organization and mode; a duplicate returns 409 customer_exists with the existing id |
first_name / last_name | Required. Latin character sets; punctuation is normalized before hitting banking rails |
country | Required. ISO 3166-1 alpha-2 residence; drives jurisdiction rules below |
wallet.address | Required. The customer's own wallet: external or the integrator's embedded wallet (passkey smart accounts work well). Self-custody only; Moneva never generates or stores keys |
metadata | Optional, up to 20 string keys, returned on every read |
Lifecycle
| Status | Meaning | What to do |
|---|---|---|
kyc_required | Verification not started | Create a KYC session and open url |
kyc_pending | Under review | Wait for customer.kyc.approved or .rejected |
active | Verified | All money movement available |
rejected | Failed permanently | Do not retry; inform the user |
suspended | Compliance hold | Contact support |
Profile data
Before documents, KYC needs the "tell us about yourself" data: date of birth, address, occupation, and per-country extras. Two ways to collect it; mix them freely.
- Hosted (default): do nothing. The hosted verification flow collects whatever is missing.
- Your own form: render it from the per-country schema, exactly like bank forms, and submit via
PATCH. Your branding, our rules.
curl https://platform.moneva.io/v1/jurisdictions/BR/profile_schema
{
"country": "BR",
"fields": [
{ "key": "date_of_birth", "type": "date", "required": true, "sensitive": false },
{ "key": "occupation", "type": "text", "required": true, "sensitive": false },
{ "key": "address.line1", "type": "text", "required": true, "sensitive": false }, …,
{ "key": "address.country", "type": "text", "required": true, "sensitive": false }
]
}- Per-country extras are schema-driven: US adds
phoneandssn, Nigeria addsbvn. Your form never hardcodes any of it. - Sensitive ids are stored masked. SSN and BVN are forwarded for verification and kept as
kindplus last four only. - Identity fields lock after verification: changing them on an
activecustomer returns409 profile_locked. - Updates emit
customer.updated.
Wallet sessions
Self-custody is the only model: funds settle to a wallet only the customer controls. When an operation needs the customer's authorization, the gateway prepares a message, their wallet signs it client-side, and we verify. Two calls, one wallet prompt.
- Sessions last six days.
customer.wallet_session.activeconfirms activation; when an operation needs a fresh signature you get409 wallet_session_required. - Any EIP-1193 wallet works: external wallets or embedded ones. The SDK wraps both calls and the wallet prompt in one helper.
- EOAs and smart accounts both verify. A 65-byte ECDSA signature verifies offline; smart-contract accounts (Safe, Biconomy Nexus) verify on-chain via ERC-1271, including ERC-6492 counterfactual accounts that are not deployed yet.
- Moneva never holds keys. The platform prepares and verifies messages; signing happens only on the customer's device.
Supported jurisdictions
Which virtual accounts and payouts a customer can use depends on their verified residency. The rules below are reference data from GET /v1/jurisdictions; per-customer eligibility is enforced automatically at KYC.
Create the customer with any of these residence countries; everything outside the list is rejected at onboarding with a clear error, before any KYC session is started.
Running verification
One call returns both integration modes; pick whichever fits your product. Either way the same verification runs, the same customer.kyc.* events report the outcome, and you never receive or store identity documents.
Option A: hosted URL (recommended)
Open url and you are done. On web, redirect or open a new tab. In a mobile app, open it in a webview; the flow uses the device camera for document and selfie capture, so a bare webview needs three switches flipped:
- Camera access. Android WebView: grant camera in
onPermissionRequestand implementonShowFileChooserfor uploads. iOS WKWebView:allowsInlineMediaPlaybackplus a camera usage description. React Native:react-native-webviewwithmediaPlaybackRequiresUserAction={false}and the camera permission granted before opening. - Keyboard. On Android set
windowSoftInputMode="adjustResize"for the hosting activity, or text fields hide behind the keyboard on small screens. - Simplest escape hatch. Opening the URL in the system browser (Custom Tabs /
SFSafariViewController) needs none of the above and is fully supported.
Option B: embedded SDK
The response also carries sdk_token. Mount the Sumsub SDK (mobile or web) with it for verification that never leaves your app; this is exactly how the Moneva consumer app runs KYC in production. Request a fresh session when the token expires; outcomes still arrive as the same webhook events, so nothing else in your integration changes.
Next steps
Virtual accounts
Create an account
- KYC first. The customer must be
active; otherwise422 kyc_not_passed. - One account per currency. A duplicate returns
409 account_existswith the existing id. - Closing:
DELETE /v1/customers/{id}/accounts/{acct_id}marks it closed: history stays queryable, new deposits are rejected,account.closedfires.
- Statuses:
provisioning→active(details ready) ·closed(provisioning failed or retired). - Eligibility follows the customer's residency; see Supported jurisdictions.
Provisioning is asynchronous
Account creation returns 202 with status provisioning. When the account is ready, typically within seconds, account.activated delivers the full deposit details. Never poll; consume the event.
{
"object": "account",
"currency": "EUR",
"status": "active",
"deposit_details": {
"iban": "DE89 3704 0044 0532 0130 00",
"bic": "…",
"bank_name": "…"
}
}| Currency | Deposit details |
|---|---|
| EUR | IBAN + BIC (SEPA) |
| GBP | Account number + sort code (Faster Payments) |
| USD | Account + routing number (ACH) |
| NGN | Local account number |
Deposits
Inbound fiat produces two events: account.deposit.received when the transfer is detected, and account.deposit.settled when converted proceeds land at the customer's settlement address.
Settlement is always USDC. Whatever currency arrives on a virtual account, the customer's balance is credited in USDC after conversion. Payouts then draw from that USDC balance. For NGN, funding flows mainly through stablecoins today; the local rail complements the crypto path.
Next steps
Corridors & bank forms
The beneficiary schema
curl https://platform.moneva.io/v1/corridors/BRL/beneficiary_schema
{
"currency": "BRL",
"transfer_methods": ["pix"],
"fields": [
{ "key": "account_holder_name", "type": "text", "required": true },
{ "key": "pix_key", "type": "text", "required": true },
{ "key": "tax_id", "type": "digits", "required": true }
]
}Render your bank form from fields. On submit, the same registry entry that produced the schema validates the values, so the form you rendered and the validation applied can never disagree. Rejections come back per field with stable codes.
The corridor object
curl https://platform.moneva.io/v1/corridors
{{
"data": [
{{ "currency": "BRL", "country": "BR", "transfer_methods": ["pix"],
"minimum_usd": "5.00", "estimated_arrival": "Within minutes",
"status": "active" }},
…
]
}}Public reference data. GET /v1/corridors, the beneficiary schemas, and GET /v1/jurisdictions need no API key and allow browser requests, so coverage pages and pre-signup flows can read them directly.
Currency availability
Live from GET /v1/jurisdictions and GET /v1/corridors: which currencies can come in as virtual-account deposits and which can go out as payouts.
Every row above is queryable, without an API key: GET /v1/corridors returns method, live minimum, arrival estimate and status per currency; GET /v1/corridors/{currency}/beneficiary_schema returns exactly which fields that destination requires, with labels and validation patterns; GET /v1/jurisdictions returns the residency rules and the full list of 185 onboarding countries. Try one:
Payout methods
Not every corridor is a bank account. The beneficiary schema tells you how each destination is addressed, so your form always collects the right thing:
The transfer_methods array on each corridor plus the served field set encode all of this; your integration never branches on method type by hand.
Normalization included
Common real-world failures are absorbed before they reach the banking rails: IBANs are uppercased and de-spaced, names are checked against the character sets the rails actually accept, and per-corridor formats are enforced up front.
Corridor availability and live minimums come from GET /v1/corridors. It reflects real capability, not a static table.
Next steps
Quotes
Create a quote
{
"id": "qt_8d3f6a",
"object": "quote",
"rate": "5.067400",
"fees": [{ "type": "transfer_fee", "amount": "1.40", "currency": "USD" }],
"net_receivable": { "amount": "535.40", "currency": "BRL" },
"minimum_usd": "5.00",
"minimum_destination": { "amount": "25.34", "currency": "BRL" },
"expires_at": "2026-07-23T00:41:02Z"
}Quotes price payouts from the customer's USDC balance. Inbound deposits need no quote; they convert automatically at settlement.
How the price composes
One formula, no hidden components: net_receivable = (source amount − fees) × rate. The rate is mid-market; Moneva's margin is the visible transfer_fee line. For the example above: (100.00 − 1.40) × 5.0674 = 535.40 BRL, delivered exactly.
- Net receivable is exact. What the quote says is what the beneficiary receives.
- Minimums are live, in both currencies. Amounts below the floor reject with
amount_below_minimum;detailscarriesminimum_usdandminimum_destination(converted at the quoted rate), so you can show users the minimum in their own currency. - Quotes expire. Execute within
expires_at(60 seconds) or create a new one. - Arrival estimates are served, not guessed.
GET /v1/corridorscarriesestimated_arrivalper corridor, so apps display payout timing from the API instead of hardcoding it.
Fields
| Field | Meaning |
|---|---|
rate | Mid-market FX rate, source to destination, six decimals |
fees[] | Itemized fee lines; today a single transfer_fee in USD |
net_receivable | Exact amount and currency the beneficiary receives |
minimum_usd | Live corridor floor at quote time |
minimum_destination | The floor converted to the payout currency at the quoted rate, display-ready; a send-amount floor, not a guaranteed net payout after fees |
expires_at | Execution deadline; the transfer must be created before it |
Errors
| Code | When |
|---|---|
amount_below_minimum | Amount under the live corridor floor; details carries both minimums |
unsupported_corridor | No active corridor for destination_currency |
unsupported_source_currency | Quotes price from USDC/USD today |
quote_expired | Raised at transfer creation when expires_at has passed |
quote_beneficiary_mismatch | Quote currency differs from the beneficiary's corridor |
Using a quote in a transfer
- Pass
quote_idonPOST /v1/transfers; the quote's rate, fees and net receivable are frozen onto the transfer at creation and never rewritten by settlement. - The quote's destination currency must match the beneficiary's corridor, or the transfer is rejected with
quote_beneficiary_mismatch. - Display the quote to your user, then execute inside the 60-second window; on expiry, re-quote and re-confirm.
Next steps
Transfers
Create a transfer
{
"id": "tr_4c7m9k",
"object": "transfer",
"type": "remittance",
"status": "created",
"destination": { "currency": "BRL", "amount": "535.40", "beneficiary_id": "ben_5j8w2e" },
"fees": [{ "type": "transfer_fee", "amount": "1.40", "currency": "USD" }],
"timeline": [{ "status": "created", "at": "…" }]
}List with GET /v1/transfers?customer_id=…&status=…; retrieve a single transfer for the full timeline.
Types
| Type | Meaning |
|---|---|
payout | Bank/method payout to a beneficiary in the account's currency zone |
remittance | Cross-currency payout against a quote |
deposit | Inbound funding recorded by the platform (never created by you) |
Idempotency, required
Every transfer creation requires an Idempotency-Key header. Retries with the same key return the original result; the same key with a different payload is rejected with 409 idempotency_key_reused. Network failures can never double-pay.
Frozen quote values
The quote's rate, fees and net receivable are copied onto the transfer at creation. What you display to your user is what the record says forever; settlement never rewrites history.
States
Every transition is its own event with the full object attached, and the timeline array records each step with timestamps. failed always carries a machine-readable failure_reason and funds return to source.
Errors
| Code | When |
|---|---|
idempotency_key_required | Missing Idempotency-Key header |
idempotency_key_reused | Same key, different payload |
idempotency_in_progress | Original request still processing; retry shortly |
kyc_not_passed | Customer is not active |
beneficiary_not_owned | Beneficiary belongs to a different customer |
quote_expired | Quote's expires_at has passed |
quote_beneficiary_mismatch | Quote currency differs from the beneficiary's corridor |
Next steps
Events & webhooks
Verify signatures
const [t, v1] = header.split(",").map(p => p.split("=")[1]); const expected = crypto .createHmac("sha256", endpointSecret) .update(`${t}.${rawBody}`) .digest("hex"); if (v1 !== expected || tooOld(t)) reject(); // 5-minute replay window
The signature arrives in the Moneva-Signature header as t=<unix>,v1=<hmac>. The endpoint secret is shown once at registration.
Delivery semantics
- At-least-once. Deduplicate on
event.id. - Every transition delivered. Intermediate states are never collapsed away by retries.
- Retries back off over 72 hours; persistently failing endpoints are disabled and you are notified.
- Recovery:
GET /v1/eventsis the durable log; reconcile any gap from it.
Manage endpoints
- Omit
event_typesto receive everything; list and delete endpoints withGET/DELETE /v1/webhook_endpoints/{id}. - Multiple endpoints per mode are supported; each has its own secret.
Reading the log directly
Cursor pagination via next_cursor; events are retained 90 days. The log is the recovery path after any webhook outage.
Event catalog
| Event | Fired when |
|---|---|
customer.created | Customer record created |
customer.kyc.pending | Verification started |
customer.kyc.approved | Customer is active |
customer.kyc.rejected | Verification failed permanently |
customer.wallet_session.active | Customer-signed wallet session active |
customer.updated | Profile or metadata updated |
account.activated | Deposit details ready |
account.deposit.received | Inbound fiat detected |
account.deposit.settled | Proceeds settled to the customer |
beneficiary.activated | Bank destination payable |
transfer.created / .processing | Transfer accepted / funds in flight |
transfer.completed | Fiat delivered |
transfer.failed / .returned | Terminal failure / returned by receiving bank |
Next steps
Email notifications
Your users expect to hear "your transfer arrived" from you, not from us. Two ways to make that happen, and both are first-class:
| Approach | How | When to choose it |
|---|---|---|
| Build your own | Consume the webhook events and notify through your own stack: email, push, SMS, in-app | You already have notification infrastructure, or want channels beyond email |
| Platform-sent emails | Enable branded emails below; the platform sends on your behalf, in your brand, from your domain | You want status emails live today with zero email infrastructure |
Everything on this page is optional and off by default. The email system is a convenience layered on the same events you receive anyway; nothing here is required, and skipping it costs you no functionality. Mix freely: many integrators send their own marketing and product mail but let the platform handle transactional status receipts.
Configure once
- Off by default. Nothing is sent until you flip
branded_emails. - Pick your events. Leave
event_typesempty to email on every notifiable event, or select a subset. - Your brand carries. Logo, accent color, sender name, reply-to, and footer are all yours.
Send from your own domain
By default mail leaves from the shared domain. Add your own domain and verify it via DNS, and every email is sent from your address: the end user never sees anything but your brand, down to the sender domain.
The response carries the DNS records to add at your DNS host:
| Type | Host | Value |
|---|---|---|
TXT | _moneva.acme.com | moneva-verify=tok_… |
CNAME | mnv1._domainkey.acme.com | mnv1.dkim.moneva.io |
CNAME | mnv2._domainkey.acme.com | mnv2.dkim.moneva.io |
CNAME | bounce.acme.com | bounce.moneva.io |
- Then set your address:
PATCH /v1/notification_settingswithfrom_email: "no-reply@acme.com". Setting it requires the domain to be added first. - Fail-safe: an unverified domain is never used; until verification passes, sends fall back to the shared domain.
notification.domain.verifiedfires when DNS checks out. - Test mode verifies immediately so you can build the full flow against the sandbox; live mode resolves your real DNS records.
What gets sent
| Event | |
|---|---|
customer.kyc.approved | You're verified |
customer.kyc.rejected | We couldn't verify your identity |
account.activated | Your account is ready |
account.deposit.settled | Your deposit has settled |
transfer.completed | Your transfer was delivered |
transfer.failed | Your transfer could not be completed |
Fully custom templates
The default layout carries your brand tokens. When that is not enough, replace it entirely: your own subject line and your own complete HTML per event. Your design system, your copy, your language.
- Variables:
{{sender_name}}everywhere; money events add receipt values like{{amount_sent}},{{delivered}},{{fee}},{{reference}},{{amount_received}},{{settled}}. Values are HTML-escaped on insert; unknown variables stay visible so mistakes surface in preview. - Per event: override only the emails you care about; the rest keep the default layout.
DELETEthe template to revert. - Preview works the same:
GET /previewrenders your custom template with sample values before anything is enabled.
Preview before enabling
Returns the rendered subject and html with your current branding applied, so you can review every template before a single mail goes out. Try it below: the same template, rendered live with your brand.
Every send is observable
Each qualifying send appears in your event stream as notification.email.sent with the triggering event_type, the recipient, and the subject. In test mode no real mail leaves; the events fire exactly as they will in production, so you can build and verify the full loop against the sandbox.
Errors
{
"code": "amount_below_minimum",
"message": "Minimum for BRL is $5.00.",
"details": { "minimum_usd": "5.00", "minimum_destination": { "amount": "25.34", "currency": "BRL" } }
}| Code | Status | Meaning |
|---|---|---|
invalid_api_key | 401 | Missing, malformed or revoked key |
live_mode_disabled | 403 | Live keys not yet enabled |
resource_not_found | 404 | Unknown id for this org and mode |
idempotency_key_required | 400 | Money POST without an Idempotency-Key |
idempotency_key_reused | 409 | Same key, different payload |
kyc_not_passed | 422 | Customer must be active first |
validation_failed | 422 | Field-level failures in fields[] |
amount_below_minimum | 422 | Below the live corridor floor |
quote_expired | 422 | Create a fresh quote |
unsupported_corridor | 422 | No active corridor for that currency |
customer_exists | 409 | Duplicate email within your org and mode |
account_exists | 409 | Customer already has an account in that currency |
beneficiary_not_owned | 403 | Beneficiary belongs to a different customer |
quote_beneficiary_mismatch | 422 | Quote and beneficiary currencies differ |
idempotency_in_progress | 409 | Original request still processing |
test_mode_only | 403 | Simulation endpoint called with a live key |
rate_limited | 429 | Request window exhausted; honor Retry-After |
upstream_validation | 422 | The banking provider rejected a field value |
upstream_error | 502 | Upstream provider fault; safe to retry |
route_not_found | 404 | Unknown path |
internal_error | 500 | Unexpected fault; report with your Request-Id |
Next steps
Rate limits & versioning
Limits exist to keep the sandbox fast for everyone. They are enforced per minute, per caller, and every response tells you where you stand.
| Caller | Limit | Keyed by |
|---|---|---|
| Authenticated requests | 300 / minute | API key |
| Public reference data (no key) | 120 / minute | IP address |
Every /v1 response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). Past the window you receive:
HTTP/1.1 429 Too Many Requests
Retry-After: 24
{
"code": "rate_limited",
"message": "Too many requests. Retry after the window resets.",
"details": { "retry_after_seconds": 24, "limit_per_minute": 300 }
}- Honor
Retry-After. Back off for the stated seconds; do not busy-retry. - Retries are always safe on money endpoints. Replay the same
Idempotency-Keyafter a 429 and you can never double-create a transfer. - Webhooks beat polling. If you are polling status endpoints hard enough to hit limits, subscribe to events instead.
- Need more? Production limits are set per organization in your agreement; contact us if your integration needs higher sandbox windows.
API versioning
The version lives in the path: /v1. What we promise on top of it:
- Additive changes are not breaking. New endpoints, new optional request fields, new response fields, new event types and new error codes can appear within
/v1. Parse tolerantly: ignore fields you do not recognize. - Breaking changes get a new version. Renaming or removing fields, changing types or semantics only happens under
/v2, never silently under/v1. - Deprecations give you 12 months. When a version is scheduled for sunset, every affected org is notified and the old version keeps working for at least 12 months from the notice.
- Docs are stamped. Every page here carries its last-updated date, and llms-full.txt always reflects the current surface.
Next steps
Test mode
Real sandboxes at banking providers are slow, gated and periodically wiped. The simulator exists so your CI and local development never depend on any of that, while behaving exactly like the live API: same objects, same events, same async provisioning.
| Endpoint | Simulates |
|---|---|
POST /v1/test/customers/{id}/kyc | KYC outcome: approved, rejected, resubmission_required |
POST /v1/test/accounts/{id}/deposits | Inbound fiat deposit with sender name |
POST /v1/test/transfers/{id} | Terminal outcome: completed, failed, returned |
Simulation endpoints only accept test keys. Calling them with a live key returns test_mode_only.
Examples
Every simulation emits the same events as the real transition, so your webhook handling is exercised end to end. Test data is isolated per organization and never touches live records.
Next steps
Endpoint reference
Customers
| POST | /customers | Create a customer |
| GET | /customers · /customers/{id} | List / retrieve |
| POST | /customers/{id}/kyc_session | Verification session: hosted URL + SDK token |
| POST | /customers/{id}/wallet_sessions · /verify | Client-signed wallet sessions |
| GET | /jurisdictions/{cc}/profile_schema | Per-country profile fields Public |
| PATCH | /customers/{id} | Update profile ("tell us about yourself") |
Accounts & beneficiaries
| POST | /customers/{id}/accounts | Provision a virtual account |
| GET | /customers/{id}/accounts | List accounts |
| DEL | /customers/{id}/accounts/{acct_id} | Close a virtual account |
| POST | /customers/{id}/beneficiaries | Create a bank destination |
| GET | /customers/{id}/beneficiaries | List beneficiaries |
| DEL | /customers/{id}/beneficiaries/{ben_id} | Remove a beneficiary (deactivates) |
Corridors, quotes, transfers
| GET | /corridors | Live corridor availability + minimums Public |
| GET | /jurisdictions | Residency rules + currency availability Public |
| GET | /corridors/{ccy}/beneficiary_schema | Bank form schema Public |
| POST | /quotes | Deterministic pricing |
| POST | /transfers | Execute (Idempotency-Key required) |
| GET | /transfers · /transfers/{id} | List / retrieve with timeline |
Events & webhooks
| GET | /events · /events/{id} | Durable 90-day event log |
| POST | /webhook_endpoints | Register an endpoint (secret shown once) |
| GET | /webhook_endpoints | List endpoints |
| DEL | /webhook_endpoints/{id} | Delete an endpoint |
Test mode
| POST | /test/customers/{id}/kyc | Force KYC outcome |
| POST | /test/accounts/{id}/deposits | Simulate deposit |
| POST | /test/transfers/{id} | Force transfer outcome |
Build your flow
Playground
API console
Every documented request is runnable. Pick an endpoint, adjust the body, and send it against the sandbox: real gateway, real status codes, real payloads. Use your mk_test_ key; keep live keys server-side.
https://platform.moneva.ioTerms of service
Effective 2026-07-23. These are the Sandbox Terms. Production access is governed by a signed services agreement that names the contracting entity, pricing and production limits; these terms apply until then.
The service
- What Moneva provides. A developer platform: API access, documentation, SDKs, a sandbox simulator, and orchestration of regulated payment services which are performed by licensed partner institutions. Moneva is the software layer.
- Self-custody by design. End-user funds settle to wallets your users control. Moneva does not take custody of end-user funds.
- Sandbox means sandbox. Test keys (
mk_test_) never move real money, never touch real banking rails, and sandbox data may be reset with notice. No uptime commitment applies to the sandbox; production SLAs are part of the signed agreement.
Your account and keys
- Using the sandbox constitutes acceptance of these terms and the Acceptable Use Policy.
- Keep secret keys server-side. You are responsible for activity under your keys; rotate a key immediately if you suspect exposure.
- Accurate information. Live keys are issued only after business verification (KYB) and an executed agreement.
Liability and changes
- Sandbox is provided as-is, without warranty. To the maximum extent permitted by law, Moneva is not liable for indirect or consequential damages arising from sandbox use.
- We may update these terms. Material changes are announced on this page (see its Last updated stamp) before they take effect.
- Termination. We may suspend keys that violate these terms or the Acceptable Use Policy. You may stop using the service at any time.
Going to production? The production services agreement, order form, data processing agreement and country-specific disclosures are provided during onboarding. Ask via ops@moneva.io.
Acceptable use
Payments infrastructure carries legal obligations that flow down to every integrator. Building any of the following on the platform is prohibited:
- Sanctions and illegal activity. Serving sanctioned persons or jurisdictions, money laundering, terrorist financing, or any activity illegal where you or your users operate.
- Circumventing compliance. Splitting transactions to evade limits or reporting, obscuring the true originator or beneficiary of a payment, or onboarding users under false identities. Every paying end user must complete KYC through the platform.
- Undisclosed aggregation. Operating as a payment intermediary that pools or forwards funds for undisclosed third parties. Your customers must be your own end users.
- High-risk categories without written approval. Gambling, adult content, weapons and ammunition, narcotics and drug paraphernalia, debt collection, binary options, and multi-level marketing.
- Fraud and deception. Ponzi or pyramid schemes, fake invoicing, phishing, or products that misrepresent pricing, fees or the nature of the service to end users.
- Anonymity infrastructure. Mixers, tumblers, or services whose purpose is defeating financial-crime controls.
Prohibited-use screening happens at business verification and continuously on live traffic. Violations lead to key suspension; where the law requires it, activity is reported to the relevant authorities. If you are unsure whether your use case qualifies, ask first: ops@moneva.io.
Privacy & data
What we process
| Data | Purpose | Retention |
|---|---|---|
| Organization and developer contact details | Account administration, security notices | Life of the account |
| End-customer identity data (name, email, residency, date of birth) | KYC and sanctions screening, performed by licensed partners | As required by AML law (typically 5 years after relationship ends) |
| Beneficiary bank details | Executing payouts you instruct | While the beneficiary is active, then per AML retention |
| Transfer and account records | Processing, support, regulatory reporting | Per AML retention |
| Events | Webhook delivery and replay | 90 days |
| API request logs (path, status, timing, Request-Id; never bodies or keys) | Debugging and abuse prevention | 30 days |
How it is handled
- Processor chain is disclosed. KYC, banking and payout execution are performed by licensed partner institutions under their own regulatory obligations; the current partner list is available on request.
- Keys are never stored in plaintext. API keys are stored and matched as SHA-256 hashes; rate limiting also uses hashed key material.
- No selling of data. Data is processed to provide the service, meet legal obligations, and nothing else.
- Encryption. TLS in transit everywhere; encryption at rest for stored records.
- Your users' rights. Access, correction and deletion requests for end-customer data flow through you as the controller; we support them as processor. A signable DPA (GDPR Art. 28) is part of production onboarding.
Questions or data requests: ops@moneva.io.