Developer reference
Read the source, run the sandbox, ship to your merchants. Everything here matches the OpenAPI spec — if anything diverges, the API is the source of truth.
Getting started
Core concepts
- ▸ Fees + tiers
- ▸ Payment statuses
- ▸ Idempotency keys
- ▸ Networks + assetsSoon
Advanced
- ▸ Mass payouts (dual-approval)Soon
- ▸ Recurring billingSoon
- ▸ Risk screeningSoon
- ▸ Telegram operator botSoon
Install in 30 seconds
$ git clone https://github.com/veilnebula/nebula $ cd nebula $ cp .env.example .env $ docker compose up -d postgres redis $ npm install $ npm run db:migrate -w apps/api $ npm run dev:api # in one terminal $ npm run dev:web # in another $ open http://localhost:3000
Authentication
Server-to-server calls authenticate with x-api-key. Project keys are scoped to a single merchant project. Operator endpoints require a separate ADMIN_API_KEY.
curl -H "x-api-key: ncp_test_..." \
https://api.veilnebu.la/v1/balancesYour first invoice
POST /v1/payments
Idempotency-Key: order-1234
x-api-key: ncp_test_...
{
"amount": "100.00",
"currencyCode": "USD",
"settlementAssetCode": "USDT",
"successUrl": "https://you.example/success",
"cancelUrl": "https://you.example/cancel"
}Payment statuses
Every invoice reports one of these. The next action column is what the merchant dashboard shows an operator for that state.
| Status | Label | Next action |
|---|---|---|
new | New | Share checkout link |
pending | Pending | Waiting for payment |
confirming | Confirming | Waiting for confirmations |
paid | Paid | Settlement ready |
partially_paid | Partial | Ask customer to complete payment |
overpaid | Overpaid | Review credited extra amount |
expired | Expired | Create a new invoice |
failed | Failed | Open details |
cancelled | Cancelled | No action required |
refunded | Refunded | No action required |
Idempotency keys
Send an Idempotency-Key header on every write. A repeat of the same key returns the original resource instead of creating a second one, so a retried request after a timeout cannot double-charge a customer or issue a duplicate payout. Use a value derived from your own order id, not a random one, or a retry will generate a fresh key and defeat the guard.
POST /v1/payments Idempotency-Key: order-1234 # ← your order id, stable across retries x-api-key: ncp_test_...
Webhooks (v1 HMAC + v2 Ed25519)
Every event carries x-novacrypta-signature: t=<unix>,v1=<hex>,v2=<b64>?. Verify within 300 seconds of t.
import { verifyWebhook } from '@nebula/sdk';
if (!verifyWebhook(req.headers['x-novacrypta-signature'], rawBody, {
secret: process.env.NEBULA_WEBHOOK_SECRET,
})) return res.status(401).end();Verify in Node.js (no SDK)
import crypto from 'node:crypto';
function verify(header, rawBody, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
if (!parts.t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Verify in Python
import hmac, hashlib, time
def verify(header: str, raw_body: bytes, secret: str) -> bool:
parts = dict(p.split('=') for p in header.split(','))
if abs(time.time() - int(parts['t'])) > 300:
return False
expected = hmac.new(
secret.encode(),
(parts['t'] + '.' + raw_body.decode()).encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts['v1'])Verify in PHP
function verify_webhook(string $header, string $body, string $secret): bool {
$parts = [];
foreach (explode(',', $header) as $p) { [$k, $v] = explode('=', $p); $parts[$k] = $v; }
if (abs(time() - intval($parts['t'])) > 300) return false;
$expected = hash_hmac('sha256', $parts['t'].'.'.$body, $secret);
return hash_equals($expected, $parts['v1']);
}Event types
payment.pending— invoice created, awaiting deposit.payment.confirming— tx seen, awaiting confirmations.payment.paid— confirmed; merchant credited.payment.partially_paid— underpaid; partial credit.payment.overpaid— overpaid; full credit.payment.expired— invoice expired before payment.payment.risk_flagged— sanctions or risk hit.payout.completed— outbound transfer broadcast.refund.completed— refund settled to buyer.static_wallet.topup.completed— standing address received deposit.subscription.invoice_created— new cycle invoice minted.internal_transfer.sent/.received— off-chain merchant ↔ merchant move.
Retry policy
We retry non-2xx responses up to 5 times over 24 hours with exponential backoff (1s, 30s, 5m, 30m, 4h). Failed deliveries move to the dead-letter queue (visible in the dashboard) and can be replayed manually.