Sandbox
Free and unlimited. Test rails, simulated settlement, and the two simulate endpoints so you can drive paid and underpaid states without a faucet.
API reference
A REST API over HTTPS, JSON in and JSON out, with signed webhooks for anything that happens after the request returns. If you have integrated a card gateway before, this will feel familiar — the difference is that money arrives on a chain, so an order is confirmed rather than authorised.
Three calls take you from nothing to a paid order. Everything below runs in the sandbox, where you can settle a payment yourself without touching a chain.
Amounts are strings so nothing is lost to floating point, and they are denominated in your pricing currency — not in crypto. Velt converts at the moment the shopper picks a rail.
curl -X POST https://api.velt.example/v1/invoices \
-H "authorization: Bearer velt_sandbox_xxx" \
-H "content-type: application/json" \
-H "idempotency-key: 7f3c1d90-invoice-001" \
-d '{
"customer": "Helio Hosting",
"email": "ap@helio.host",
"amount": "2400.00",
"currency": "USD",
"dueInDays": 7
}'The response carries a paymentUrl. Redirect to it, drop it in an email, or embed it in an iframe with ?embed=1 and listen for postMessage status updates. The shopper picks an asset, the rate locks for twenty minutes, and a one-time deposit address is minted.
Do not fulfil on a redirect back to your site — a shopper can close the tab, and a chain payment can arrive minutes later. Fulfil on order.paid, which fires once the payment has the confirmations that rail requires.
// Express, using the official Node SDK
import { Velt } from "@velt/sdk";
const velt = new Velt({ apiKey: process.env.VELT_API_KEY });
app.post("/webhooks/velt", express.raw({ type: "*/*" }), (req, res) => {
const event = velt.webhooks.verify(
req.body, // the raw body, not the parsed object
req.header("x-velt-signature"),
process.env.VELT_WEBHOOK_SECRET,
);
if (event.type === "order.paid") {
fulfil(event.data.sourceId); // your invoice or link id
}
res.sendStatus(200); // 2xx stops the retries
});Send your key as a bearer token on every request. Keys are server-side only: anything that reaches a browser is compromised, and a leaked key can move money.
authorization: Bearer velt_live_xxxA key is bound to one environment when it is minted and cannot act outside it. That boundary is enforced on our side, so a sandbox key pointed at production data fails closed rather than doing something surprising.
Free and unlimited. Test rails, simulated settlement, and the two simulate endpoints so you can drive paid and underpaid states without a faucet.
Real chains and real money. Available once your business is verified, which an operator reviews manually.
Create and revoke keys in the dashboard under Developers. Revocation is immediate. Rotate rather than share: a key identifies an integration, and revoking one should never take down another.
https://api.velt.exampleSend an Idempotency-Key header on any write. Replaying the key with the same body returns the original response instead of acting twice; replaying it with a different body is a conflict. Networks fail halfway, and a retried payout that sends twice is not recoverable — so treat this as required on anything that moves money, not optional.
idempotency-key: 550e8400-e29b-41d4-a716-446655440000Every list returns the same envelope. Walk it with the cursor rather than an offset: the cursor pins a position in the ordering, so a page stays correct even when invoices are voided or orders expire while you are reading.
GET /v1/invoices?limit=50&cursor=aW52XzlhcDEgMjAy...
{
"object": "list",
"data": [ ... ],
"hasMore": true,
"nextCursor": "aW52XzhhbDIgMjAy..."
}limit defaults to 50 and caps at 200. Lists run newest first.
300 requests per minute per key by default. Over the limit you get 429 with rate_limited; back off and retry. The SDKs already retry with exponential backoff and jitter.
Fiat amounts are decimal strings in the pricing currency, such as "2400.00". Crypto amounts are returned as strings at the asset's own precision — eight decimal places for BTC, six for USDC and USDT. Never parse either into a binary float.
Every failure returns the same envelope. Branch on code, which is stable, rather than on the message, which is written for people and may change. Quote requestId to support and we can find the exact request in our logs.
{
"error": {
"code": "insufficient_funds",
"message": "Available USDC balance is 120.00, payout requested 500.00",
"requestId": "req_8f21ac09"
}
}| Code | HTTP | What it means |
|---|---|---|
invalid_request | 400 | The body or query failed validation. The message names the field. |
unauthenticated | 401 | Missing, malformed or revoked API key. |
forbidden | 403 | The key is valid but not allowed to do this, or is scoped to another environment. |
not_found | 404 | No such object in this organization and environment. |
conflict | 409 | The object is not in a state that allows this, or an idempotency key was replayed with a different body. |
insufficient_funds | 409 | Available balance does not cover the payout, refund or conversion. |
rule_violation | 422 | Well formed, but a policy rejected it — a withdrawal ceiling, a frozen account, a screening hit. |
rate_limited | 429 | Too many requests in the window. Back off and retry. |
pricing_unavailable | 503 | No rate could be sourced, so no quote could be locked. |
internal_error | 500 | Our fault. The requestId is logged on our side — quote it to support. |
Grouped by what you are trying to do. Every path below is relative to the base URL, and every list endpoint takes limit and cursor.
Bill a named customer for an amount in fiat. Velt emails a pay link, chases the invoice on a reminder ladder, and marks it paid when the chain confirms.
| POST | /v1/invoices | Raise an invoice and send it. |
| GET | /v1/invoices | List invoices, newest first. |
| GET | /v1/invoices/{id} | Fetch one invoice with its order. |
| GET | /v1/invoices/{id}/reminders | See what chasing has been sent. |
| POST | /v1/invoices/{id}/void | Void an invoice that is not paid. |
A shareable URL that opens hosted checkout. Fixed or open amount, reusable or single use. No integration needed on your side.
| POST | /v1/payment-links | Create a link. |
| GET | /v1/payment-links | List links with volume and payment counts. |
| GET | /v1/payment-links/{id} | Fetch one link. |
| POST | /v1/payment-links/{id}/disable | Stop accepting on a link. |
An order is one attempt to pay. Picking a rail locks a rate for twenty minutes and mints a deposit address. Fulfil on order.paid and nothing else.
| POST | /v1/orders | Open an order against an invoice or link. |
| POST | /v1/charges | One call: create and open a charge. |
| GET | /v1/orders | List orders. |
| GET | /v1/orders/{id} | Poll status, confirmations and address. |
| POST | /v1/orders/{id}/checkout | Pick asset and network; locks the rate. |
| POST | /v1/orders/{id}/cancel | Abandon an unpaid order. |
| POST | /v1/orders/{id}/simulate-pay | Sandbox only: settle without a chain payment. |
| POST | /v1/orders/{id}/simulate-underpay | Sandbox only: pay short of the amount. |
Recurring billing without a stored card. Each period raises an invoice and emails it; the customer pays when they choose. Unpaid periods are chased on a dunning ladder rather than retried against a card.
| POST | /v1/subscriptions | Start a subscription on a cadence. |
| GET | /v1/subscriptions | List subscriptions. |
| GET | /v1/subscriptions/{id} | Fetch one with its raised invoices. |
| POST | /v1/subscriptions/{id}/pause | Stop raising invoices. |
| POST | /v1/subscriptions/{id}/resume | Resume from the next period. |
| POST | /v1/subscriptions/{id}/cancel | End it for good. |
A standing deposit address tied to one customer, reusable until you disable it. Built for retainers, marketplace sellers and recurring top-ups.
| POST | /v1/payment-channels | Open a channel for a customer. |
| GET | /v1/payment-channels | List channels with volume. |
| POST | /v1/payment-channels/{id}/disable | Stop crediting a channel. |
The billing contacts and catalogue you invoice against.
| POST | /v1/customers | Create a customer. |
| GET | /v1/customers | List customers. |
| POST | /v1/products | Create a priced product. |
| GET | /v1/products | List products. |
Settled funds sit in your balance as available or pending. A payout is requested, approved by an operator, then signed from treasury. A refund debits available and returns to the paying address.
| GET | /v1/wallets | Balances per asset and network. |
| POST | /v1/destinations | Add a withdrawal address to the book. |
| GET | /v1/destinations | List withdrawal addresses. |
| POST | /v1/payouts | Request a withdrawal. |
| GET | /v1/payouts | List payouts and their state. |
| POST | /v1/refunds | Refund a paid invoice, full or partial. |
| GET | /v1/refunds | List refunds. |
Quote and lock FX between assets. The quote holds for thirty seconds and credits land on the ledger.
| POST | /v1/conversions/quote | Price a conversion. |
| POST | /v1/conversions | Execute a quoted conversion. |
| GET | /v1/conversions | List conversions. |
Double-entry accounts behind every movement, a reconciliation view, and CSV exports for finance. Exports accept from and to as dates.
| GET | /v1/ledger/accounts | Account balances. |
| GET | /v1/ledger/transactions | Entries, paged. |
| GET | /v1/ledger/reconciliation | Chain against ledger. |
| GET | /v1/exports/{kind}.csv | ledger, payouts, invoices, orders, refunds, subscriptions. |
| GET | /v1/reports | Volume and settlement summary. |
Register endpoints, read your signing secret, inspect deliveries and flush the queue.
| POST | /v1/webhooks/endpoints | Register a URL and its events. |
| GET | /v1/webhooks/endpoints | List registered endpoints. |
| GET | /v1/webhooks/secret | Read the signing secret. |
| POST | /v1/webhooks/secret/rotate | Rotate the signing secret. |
| GET | /v1/webhooks/deliveries | Delivery log with attempts and status. |
| POST | /v1/webhooks/flush | Drain the retry queue now. |
Anything that happens after your request returns — a shopper paying an hour later, a payout clearing, a reorg reversing a confirmed order — reaches you as a signed webhook. Register endpoints in the dashboard or through the API, and subscribe each one to the events it cares about.
Each delivery carries x-velt-signature: t=<unix seconds>,v1=<hex hmac>. The HMAC is SHA-256 over "<timestamp>.<raw body>" using your organization's signing secret. Verify before you trust the payload, compare in constant time, and reject a timestamp outside a five minute window so a captured delivery cannot be replayed at you later.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!Number.isFinite(age) || age > 300) throw new Error("stale signature");
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error("bad signature");
}
return JSON.parse(rawBody);
}Both SDKs ship this as one call. Note that it takes the raw body: if your framework has already parsed and re-serialised the JSON, the bytes no longer match and every signature fails.
Reply 2xx to acknowledge. Anything else, or a timeout, is retried with backoff, and the delivery log in the dashboard shows every attempt and its response. Handlers must be idempotent: a retry after your endpoint timed out but succeeded will deliver the same event twice.
| Event | Fires when |
|---|---|
invoice.sent | An invoice was emailed to the customer. |
invoice.overdue | An invoice passed its due date unpaid. |
order.confirming | A payment was seen on-chain and is waiting for confirmations. |
order.paid | Confirmed and credited. Fulfil on this event and no other. |
order.underpaid | Less arrived than the locked amount. |
order.overpaid | More arrived than the locked amount. |
order.expired | The rate lock elapsed with no payment. |
order.flagged | Address screening raised the payment for review. |
order.reorged | A confirmed payment was reversed by a chain reorganisation. |
payout.requested | A withdrawal was requested and awaits approval. |
payout.sent | A withdrawal was signed and broadcast. |
refund.requested | A refund was requested and awaits approval. |
refund.sent | A refund was signed and broadcast. |
subscription.created | A subscription started. |
subscription.invoiced | A period raised an invoice. |
subscription.paused | Billing was paused. |
subscription.resumed | Billing resumed. |
subscription.canceled | The subscription was cancelled. |
subscription.ended | The subscription reached its end date. |
Fulfil on order.paid, and only on order.paid. order.confirming means the money has been seen, not that it is final — and order.reorged exists because a confirmed payment can still be undone by the chain.
A shopper pays in any of these; you hold and settle in whichever asset you choose. Confirmation counts are per rail, because the chains do not agree on what finality costs.
| Asset | Network | Confirmations |
|---|---|---|
| BTC | Bitcoin | 1 |
| ETH | Ethereum | 2 |
| ETH | Base | 2 |
| USDC | Ethereum | 2 |
| USDC | Base | 2 |
| USDT | Tron | 3 |
Call GET /v1/currencies rather than hard-coding this table — it returns the rails your environment currently accepts, with their confirmation counts, and sandbox runs on testnets.
Typed client with automatic cursor paging, retries with backoff and jitter, and one-call webhook verification.
npm install @velt/sdk
The same surface for Python services, including the paging iterator and signature verification.
pip install velt
The full machine-readable description, generated from the same schemas the server validates against — so the published contract is the enforced one. Generate a client in any language.
Redirects the shopper to hosted checkout and listens for confirmation on a callback route.
Before you switch a key from sandbox to live, the short list that matters:
order.underpaid and order.reorged deliberately rather than ignoring them.