Migration guide
Replace Coinbase Commerce after its March 31, 2026 closure.
The old charge model maps cleanly to a VeilNebula invoice and hosted checkout. Keep your order IDs and fulfillment rules; replace the provider API calls and webhook verifier.
Concept mapping
| Coinbase Commerce | VeilNebula | Migration note |
|---|---|---|
POST /charges | POST /v1/payments | Create one VeilNebula invoice per order. Store the returned id alongside your order. |
X-CC-Api-Key | x-api-key + x-ncp-timestamp + x-ncp-signature | Project API key. POST requests also carry a hex HMAC-SHA256 signature, keyed with the API key, over timestamp.METHOD.path.rawBody. |
local_price { amount, currency } | amountMinor + currencyCode | amountMinor is a string of digits in minor units: 25.00 USD is "2500". |
metadata (order reference) | externalId | Keep your order id here; look it up with GET /v1/payments/by-order/{orderId}. |
redirect_url / cancel_url | successUrl / cancelUrl | Absolute http(s) URLs for the customer redirect. |
hosted_url | checkoutUrl | Redirect the customer to the URL returned by POST /v1/payments. |
pricing | amountMinor, totalFeeMinor, netAmountMinor | Coinbase lists per-currency prices; VeilNebula returns the invoice amount plus explicit fee and net fields. |
X-CC-Webhook-Signature | x-nebula-signature | HMAC-SHA256 over t + "." + raw body (not the raw body alone), keyed with the endpoint secret; header format t=...,v1=.... |
charge:pending | payment.confirming | Deposit seen on chain, waiting for confirmations. |
charge:confirmed | payment.paid | Fulfil after signature verification; deduplicate on eventId. |
charge:failed | payment.expired / payment.partially_paid | Expired without full payment, or underpaid and still open. |
Migration steps
- Set up a VeilNebula account and project. In the dashboard at
https://app.veilnebu.la, start in sandbox and create a live project after the full flow is verified. - Map charges to invoices. Keep your existing order identifier in
externalId; save the returned invoiceidandcheckoutUrl. - Update code. Replace charge creation with signed
POST /v1/payments. Sendx-api-key, request-signature headers and a stableidempotency-key. - Update webhooks. Register your URL as a project webhook endpoint and store its signing secret as
VEILNEBULA_WEBHOOK_SECRET. Receive the raw body, verifyx-nebula-signature, deduplicate oneventId, and mappayment.paidto fulfillment.
Node.js example
Use express.raw for the webhook route: the HMAC covers the exact bytes received, so parse JSON only after 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 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('/checkout', express.json(), async (req, res) => {
const orderId = String(req.body.orderId);
const path = '/v1/payments';
const body = JSON.stringify({
// Coinbase local_price { amount: '25.00', currency: 'USD' } becomes minor units.
amountMinor: String(Math.round(Number(req.body.totalUsd) * 100)),
currencyCode: 'USD',
settlementAssetCode: 'USDT',
externalId: orderId,
description: req.body.description,
successUrl: 'https://merchant.example/paid',
cancelUrl: 'https://merchant.example/cancelled',
});
const timestamp = new Date().toISOString();
const signature = createHmac('sha256', apiKey)
.update([timestamp, 'POST', path, body].join('.'))
.digest('hex');
const response = await fetch('https://api.veilnebu.la' + path, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': apiKey,
'x-ncp-timestamp': timestamp,
'x-ncp-signature': signature,
'idempotency-key': 'coinbase-migration:' + 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') {
// Replaces charge:confirmed. Deduplicate on event.eventId.
}
return res.sendStatus(204);
});Current product boundaries
VeilNebula accepts USDT and USDC. Payouts and refunds are not live yet, and fiat and BTC are not available. Preserve any legacy flows that depend on those capabilities until an equivalent is live.