./docs

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.

./core-concepts

Core concepts

./advanced

Advanced

  • Mass payouts (dual-approval)Soon
  • Recurring billingSoon
  • Risk screeningSoon
  • Telegram operator botSoon
./install

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
./auth

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/balances
./invoices

Your 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"
}
./statuses

Payment statuses

Every invoice reports one of these. The next action column is what the merchant dashboard shows an operator for that state.

StatusLabelNext action
newNewShare checkout link
pendingPendingWaiting for payment
confirmingConfirmingWaiting for confirmations
paidPaidSettlement ready
partially_paidPartialAsk customer to complete payment
overpaidOverpaidReview credited extra amount
expiredExpiredCreate a new invoice
failedFailedOpen details
cancelledCancelledNo action required
refundedRefundedNo action required
./idempotency

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

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.

Docs · VeilNebula