Skip to content
Migration guide

Move from Cryptomus or Heleket without rewriting your order flow.

VeilNebula exposes a compatibility-oriented invoice input, but its canonical API is explicit: POST /v1/payments, x-api-key authentication, signed mutating requests, hosted checkoutUrl, and signed webhooks.

API mapping

VeilNebula accepts the Cryptomus-style aliases below (camelCase, and snake_case where listed) on POST /v1/payments and folds them into the canonical fields. If both are sent, the canonical field wins.

Cryptomus / HeleketVeilNebulaWhat to change
POST https://api.cryptomus.com/v1/paymentPOST https://api.veilnebu.la/v1/paymentsCreates the invoice. A 201 response returns the invoice object.
merchant + sign headersx-api-key, x-ncp-timestamp, x-ncp-signaturex-api-key is the project API key. POST requests are signed: hex HMAC-SHA256, keyed with the API key, over timestamp.METHOD.path.rawBody, where timestamp is ISO 8601 and within the server window.
amount + currencyamountMinor + currencyCodeamountMinor is a string of digits in minor units of currencyCode, not a decimal: 15.00 USD is "1500".
order_idexternalIdYour order id. Fetch the invoice later with GET /v1/payments/by-order/{orderId}.
to_currencysettlementAssetCode (alias toCurrency / to_currency)USDT or USDC. VeilNebula does not offer BTC or fiat settlement.
lifetimelifetimeSeconds (alias lifetime)Integer seconds, 60 to 2,592,000.
currencies / except_currenciesallowedAssets / excludedAssets (aliases currencies, exceptCurrencies, except_currencies)Arrays of asset codes that narrow the hosted checkout.
accuracy_payment_percentunderpaidToleranceBps (alias accuracyPaymentPercent / accuracy_payment_percent)The alias takes an integer percent 0 to 100 (no decimals); it is multiplied by 100 and capped at 5000 bps.
subtractsubtractBoolean, not a 0 to 100 percentage. true deducts the fee from amountMinor; false (default) adds it on top.
is_refreshisRefresh (alias is_refresh)Re-quotes and extends an existing invoice only when the same idempotency key and request body are sent again.
url_success / url_returnsuccessUrl / cancelUrlAbsolute http(s) URLs for the customer redirect.
url_callbackProject webhook endpointThere is no per-invoice callback URL. Register the endpoint once for the project in the dashboard.
uuid / url (response)id / checkoutUrlStore id; redirect the customer to checkoutUrl.
POST /v1/payment/infoGET /v1/payments/{id}Read-only status lookup. GET requests need x-api-key only, no request signature.
Retriesidempotency-key header (or idempotencyKey body field)Use one stable key per order. The same key with the same body returns the existing invoice; a different body returns 409.

Webhook mapping

VeilNebula sends a flat JSON body with event, eventId and the invoice fields. Verify the raw body against x-nebula-signature (t=...,v1=...) before parsing it, and reject timestamps more than 300 seconds old.

Cryptomus statusVeilNebula eventHandling
sign (body field)x-nebula-signature (header)Cryptomus signs md5(base64(json) + API key) inside the body. VeilNebula sends t=<unix seconds>,v1=<hex HMAC-SHA256 of t + "." + raw body> keyed with the endpoint secret.
confirm_checkpayment.confirmingDeposit seen on chain, waiting for confirmations.
paidpayment.paidTreat as the successful payment transition.
paid_overpayment.overpaidPaid in full with an overpayment; fulfil and reconcile the difference.
wrong_amountpayment.partially_paidKeep the order open; compare receivedInvoiceAmountMinor with invoiceAmountMinor.
cancelpayment.expired / payment.cancelledStop waiting for this invoice.
fail / system_failpayment.failedRoute to your existing payment-failure path.
refund_process / refund_paid / refund_failNoneRefunds are not live yet.

Migration steps

  1. Create an account and project. In the dashboard at https://app.veilnebu.la, create a sandbox project first, then a live project when the integration is verified.
  2. Issue a project API key. Store it as VEILNEBULA_API_KEY; never expose it in browser code.
  3. Swap the invoice endpoint. Replace the provider create-invoice call with POST https://api.veilnebu.la/v1/payments.
  4. Update headers. Replace merchant and sign with x-api-key, x-ncp-timestamp, x-ncp-signature, and a stable idempotency-key.
  5. Register the webhook endpoint. Add your URL as a project webhook endpoint instead of sending url_callback per invoice, and store its signing secret as VEILNEBULA_WEBHOOK_SECRET.
  6. Test the checkout and webhook. Create a sandbox invoice, redirect to checkoutUrl, and use the dashboard webhook test before processing live orders.
  7. Go live. Change credentials and endpoint configuration only after replay protection and signature verification pass.

Node.js

The example signs the exact JSON bytes sent on the wire and keeps the raw webhook body for verification.

import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();
const apiKey = process.env.VEILNEBULA_API_KEY;
const webhookSecret = process.env.VEILNEBULA_WEBHOOK_SECRET;

function signedHeaders(method, path, body, idempotencyKey) {
  const timestamp = new Date().toISOString();
  const signature = createHmac('sha256', apiKey)
    .update([timestamp, method, path, body].join('.'))
    .digest('hex');
  return {
    'content-type': 'application/json',
    'x-api-key': apiKey,
    'x-ncp-timestamp': timestamp,
    'x-ncp-signature': signature,
    'idempotency-key': idempotencyKey,
  };
}

function verifyWebhook(rawBody, header) {
  const parts = Object.fromEntries(
    header.split(',').map((part) => part.trim().split('=', 2)),
  );
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = Buffer.from(
    createHmac('sha256', webhookSecret).update(parts.t + '.' + rawBody).digest('hex'),
  );
  const received = Buffer.from(parts.v1);
  return expected.length === received.length && timingSafeEqual(expected, received);
}

app.post('/pay', express.json(), async (req, res) => {
  const orderId = String(req.body.orderId);
  const path = '/v1/payments';
  const body = JSON.stringify({
    amountMinor: '250000', // 2500.00 USD in cents
    currencyCode: 'USD',
    settlementAssetCode: 'USDT',
    externalId: orderId,
    description: 'Order ' + orderId,
    successUrl: 'https://merchant.example/paid',
    cancelUrl: 'https://merchant.example/cancelled',
  });
  const response = await fetch('https://api.veilnebu.la' + path, {
    method: 'POST',
    headers: signedHeaders('POST', path, body, 'order-' + orderId),
    body,
  });
  if (!response.ok) return res.sendStatus(502);
  const invoice = await response.json();
  return res.redirect(303, invoice.checkoutUrl);
});

app.post('/webhooks/veilnebula', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body.toString('utf8');
  if (!verifyWebhook(rawBody, req.get('x-nebula-signature') ?? '')) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(rawBody);
  if (event.event === 'payment.paid') {
    // Deduplicate on event.eventId, then fulfil the order in event.externalId.
  }
  return res.sendStatus(204);
});

PHP

The PHP example uses cURL and hash_hmac with the same request-signature formula.

<?php
function createInvoice(string $apiKey, string $orderId, string $amountMinor): array
{
    $path = '/v1/payments';
    $body = json_encode([
        'amountMinor' => $amountMinor, // minor units: '250000' = 2500.00 USD
        'currencyCode' => 'USD',
        'settlementAssetCode' => 'USDT',
        'externalId' => $orderId,
        'description' => 'Order ' . $orderId,
        'successUrl' => 'https://merchant.example/paid',
        'cancelUrl' => 'https://merchant.example/cancelled',
    ], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
    $timestamp = gmdate('Y-m-d\TH:i:s\Z');
    $signature = hash_hmac('sha256', implode('.', [$timestamp, 'POST', $path, $body]), $apiKey);

    $ch = curl_init('https://api.veilnebu.la' . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_HTTPHEADER => [
            'content-type: application/json',
            'x-api-key: ' . $apiKey,
            'x-ncp-timestamp: ' . $timestamp,
            'x-ncp-signature: ' . $signature,
            'idempotency-key: order-' . $orderId,
        ],
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $response = curl_exec($ch);
    if ($response === false) throw new RuntimeException(curl_error($ch));
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status < 200 || $status >= 300) throw new RuntimeException('VeilNebula API returned ' . $status);
    return json_decode($response, true, flags: JSON_THROW_ON_ERROR);
}

function verifyWebhook(string $raw, string $signatureHeader, string $secret): bool
{
    if (!preg_match('/t=(\d+),v1=([0-9a-f]+)/', $signatureHeader, $matches)) return false;
    if (abs(time() - (int) $matches[1]) > 300) return false;
    $expected = hash_hmac('sha256', $matches[1] . '.' . $raw, $secret);
    return hash_equals($expected, $matches[2]);
}

function handleWebhook(string $raw, string $signatureHeader, string $secret): void
{
    if (!verifyWebhook($raw, $signatureHeader, $secret)) {
        http_response_code(400);
        return;
    }
    $event = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
    if ($event['event'] === 'payment.paid') {
        // Deduplicate on $event['eventId'], then fulfil $event['externalId'].
    }
    http_response_code(204);
}

$orderId = $_POST['orderId'] ?? 'order-1001';
$amountMinor = $_POST['amountMinor'] ?? '250000';
$invoice = createInvoice(getenv('VEILNEBULA_API_KEY') ?: '', $orderId, $amountMinor);
header('Location: ' . $invoice['checkoutUrl'], response_code: 303);

// In the webhook route, call:
// handleWebhook(file_get_contents('php://input'), $_SERVER['HTTP_X_NEBULA_SIGNATURE'] ?? '', getenv('VEILNEBULA_WEBHOOK_SECRET') ?: '');

Scope to plan for

VeilNebula currently accepts USDT and USDC only. Payouts and refunds are not live yet; fiat and BTC are not supported. Keep those paths disabled in the migrated integration.

Migrate from Cryptomus or Heleket · VeilNebula