API reference

Build payments into your own stack

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.

Quickstart

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.

1. Raise an invoice

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
  }'

2. Send the customer to checkout

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.

3. Fulfil on the webhook

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
});

Authentication

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_xxx

A 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.

Sandbox

Free and unlimited. Test rails, simulated settlement, and the two simulate endpoints so you can drive paid and underpaid states without a faucet.

Live

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.

Conventions

Base URL

https://api.velt.example

Idempotency

Send 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-446655440000

Pagination

Every 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.

Rate limits

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.

Amounts

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.

Errors

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"
  }
}
CodeHTTPWhat it means
invalid_request400The body or query failed validation. The message names the field.
unauthenticated401Missing, malformed or revoked API key.
forbidden403The key is valid but not allowed to do this, or is scoped to another environment.
not_found404No such object in this organization and environment.
conflict409The object is not in a state that allows this, or an idempotency key was replayed with a different body.
insufficient_funds409Available balance does not cover the payout, refund or conversion.
rule_violation422Well formed, but a policy rejected it — a withdrawal ceiling, a frozen account, a screening hit.
rate_limited429Too many requests in the window. Back off and retry.
pricing_unavailable503No rate could be sourced, so no quote could be locked.
internal_error500Our fault. The requestId is logged on our side — quote it to support.

API reference

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.

Invoices

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/invoicesRaise an invoice and send it.
GET/v1/invoicesList invoices, newest first.
GET/v1/invoices/{id}Fetch one invoice with its order.
GET/v1/invoices/{id}/remindersSee what chasing has been sent.
POST/v1/invoices/{id}/voidVoid an invoice that is not paid.

Orders and checkout

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/ordersOpen an order against an invoice or link.
POST/v1/chargesOne call: create and open a charge.
GET/v1/ordersList orders.
GET/v1/orders/{id}Poll status, confirmations and address.
POST/v1/orders/{id}/checkoutPick asset and network; locks the rate.
POST/v1/orders/{id}/cancelAbandon an unpaid order.
POST/v1/orders/{id}/simulate-paySandbox only: settle without a chain payment.
POST/v1/orders/{id}/simulate-underpaySandbox only: pay short of the amount.

Subscriptions

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/subscriptionsStart a subscription on a cadence.
GET/v1/subscriptionsList subscriptions.
GET/v1/subscriptions/{id}Fetch one with its raised invoices.
POST/v1/subscriptions/{id}/pauseStop raising invoices.
POST/v1/subscriptions/{id}/resumeResume from the next period.
POST/v1/subscriptions/{id}/cancelEnd it for good.

Payment channels

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-channelsOpen a channel for a customer.
GET/v1/payment-channelsList channels with volume.
POST/v1/payment-channels/{id}/disableStop crediting a channel.

Customers and products

The billing contacts and catalogue you invoice against.

POST/v1/customersCreate a customer.
GET/v1/customersList customers.
POST/v1/productsCreate a priced product.
GET/v1/productsList products.

Balances, payouts and refunds

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/walletsBalances per asset and network.
POST/v1/destinationsAdd a withdrawal address to the book.
GET/v1/destinationsList withdrawal addresses.
POST/v1/payoutsRequest a withdrawal.
GET/v1/payoutsList payouts and their state.
POST/v1/refundsRefund a paid invoice, full or partial.
GET/v1/refundsList refunds.

Convert

Quote and lock FX between assets. The quote holds for thirty seconds and credits land on the ledger.

POST/v1/conversions/quotePrice a conversion.
POST/v1/conversionsExecute a quoted conversion.
GET/v1/conversionsList conversions.

Ledger and reporting

Double-entry accounts behind every movement, a reconciliation view, and CSV exports for finance. Exports accept from and to as dates.

GET/v1/ledger/accountsAccount balances.
GET/v1/ledger/transactionsEntries, paged.
GET/v1/ledger/reconciliationChain against ledger.
GET/v1/exports/{kind}.csvledger, payouts, invoices, orders, refunds, subscriptions.
GET/v1/reportsVolume and settlement summary.

Webhook management

Register endpoints, read your signing secret, inspect deliveries and flush the queue.

POST/v1/webhooks/endpointsRegister a URL and its events.
GET/v1/webhooks/endpointsList registered endpoints.
GET/v1/webhooks/secretRead the signing secret.
POST/v1/webhooks/secret/rotateRotate the signing secret.
GET/v1/webhooks/deliveriesDelivery log with attempts and status.
POST/v1/webhooks/flushDrain the retry queue now.

Webhooks

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.

Verifying a delivery

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.

Delivery and retries

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.

Events

EventFires when
invoice.sentAn invoice was emailed to the customer.
invoice.overdueAn invoice passed its due date unpaid.
order.confirmingA payment was seen on-chain and is waiting for confirmations.
order.paidConfirmed and credited. Fulfil on this event and no other.
order.underpaidLess arrived than the locked amount.
order.overpaidMore arrived than the locked amount.
order.expiredThe rate lock elapsed with no payment.
order.flaggedAddress screening raised the payment for review.
order.reorgedA confirmed payment was reversed by a chain reorganisation.
payout.requestedA withdrawal was requested and awaits approval.
payout.sentA withdrawal was signed and broadcast.
refund.requestedA refund was requested and awaits approval.
refund.sentA refund was signed and broadcast.
subscription.createdA subscription started.
subscription.invoicedA period raised an invoice.
subscription.pausedBilling was paused.
subscription.resumedBilling resumed.
subscription.canceledThe subscription was cancelled.
subscription.endedThe 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.

Assets and networks

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.

AssetNetworkConfirmations
BTCBitcoin1
ETHEthereum2
ETHBase2
USDCEthereum2
USDCBase2
USDTTron3

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.

SDKs and integrations

Node

Typed client with automatic cursor paging, retries with backoff and jitter, and one-call webhook verification.

npm install @velt/sdk

Python

The same surface for Python services, including the paging iterator and signature verification.

pip install velt

OpenAPI

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.

WooCommerce

Redirects the shopper to hosted checkout and listens for confirmation on a callback route.

Going live

Before you switch a key from sandbox to live, the short list that matters:

  1. Verify your business. Submit your details in Settings. An operator reviews the filing manually; live keys are issued after approval.
  2. Register a production webhook endpoint and verify signatures on it. Confirm your handler is idempotent by replaying a delivery from the log.
  3. Fulfil on order.paid only, and handle order.underpaid and order.reorged deliberately rather than ignoring them.
  4. Send Idempotency-Key on every write, especially payouts and refunds.
  5. Add your withdrawal addresses to the address book and set a withdrawal policy. Payouts are approved by an operator before they are signed.
  6. Keep keys server-side and rotate the webhook secret on a schedule.