NHBCoin
NHBCoin

Guide: Recurring Billing with NHBCoin Subscriptions

Where Stripe makes you create a Product and a Price before you can charge anything, an NHBCoin subscription Plan is one object: a name, a price, an asset, and a billing interval. Create it and it's live. From there, the chain itself charges your subscriber automatically every cycle -- there's no server-side card vault, no webhook race with a payment processor, and no separate "off-session charge" API to reason about. One signature from your subscriber is their entire standing mandate.

Read the on-chain module reference on GitHub

Step 1: Get a live API key with the subscriptions:write scope

From a business workspace, go to Developer -> API Keys and generate a key with the subscriptions:write scope. This is a separate scope from checkout:write -- a key issued only for checkout invoices cannot create or edit subscription plans, and vice versa.

  • Live keys only for now: sandbox-environment keys are rejected outright on every write endpoint (sandbox_not_supported) -- there is no simulated/test-mode flow yet.
  • Your key never touches your customers' money. Creating and editing Plans is something your API key can do on its own -- it's your own catalog. Enrolling a customer into a subscription is different: that action always requires the customer's own signature (Step 3), never your key.

Step 2: Create a Plan from your backend

This call must happen server-side -- it uses your secret API key. Price is a decimal wei string; the plan's price, asset, billing interval, and trial period are permanently fixed once created -- if you need different terms later, create a new Plan rather than editing this one (the same reason Stripe Prices are immutable: an existing subscriber's terms should never change under them).

server.mjs
// server.mjs -- this call must happen server-side, it uses your secret key
import express from 'express';

const app = express();
app.use(express.json());

const NHB_API_URL = 'https://portal.nhbcoin.com/api/v1/subscriptions/plans';
const NHB_API_KEY = process.env.NHB_API_KEY; // sk_live_... with the subscriptions:write scope

app.post('/plans/pro-monthly', async (req, res) => {
  const response = await fetch(NHB_API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': NHB_API_KEY,
      // Unique per plan you intend to create -- a retry with the same
      // key reuses the same plan instead of creating a duplicate.
      'Idempotency-Key': 'create-pro-monthly-v1'
    },
    body: JSON.stringify({
      name: 'Pro Monthly',
      priceWei: '10000000000000000000', // 10 NHB, an 18-decimal wei string
      asset: 'NHB',
      intervalSeconds: 2_592_000, // 30 days
      trialPeriodSeconds: 0
    })
  });

  if (!response.ok) {
    const errorBody = await response.json();
    return res.status(response.status).json(errorBody);
  }

  const plan = await response.json();
  // Share this URL with anyone you want to subscribe -- that hosted
  // page handles the customer's own wallet and signature. Nothing
  // else to build for enrollment.
  const subscribeUrl = `https://portal.nhbcoin.com/subscribe/${plan.planId}`;
  res.json({ plan, subscribeUrl });
});

app.listen(3001, () => console.log('Merchant server running on port 3001'));

Step 3: Send your customer to subscribe

You never collect or handle a customer's signature yourself. Send them to the hosted subscribe page at https://portal.nhbcoin.com/subscribe/{planId} -- it shows the plan, prompts them to log in if needed, and their own "Subscribe" click is the one signature that authorizes every future charge. Nothing else to build for this step.

  • The hosted page works for any NHBCoin portal user -- no dev integration required on your customer's side.
  • Once subscribed, the customer can see and cancel their own subscription any time from their personal /subscriptions page -- you don't need to build subscriber-facing cancellation yourself, though your API also supports merchant-initiated cancellation (Step 6).
  • There is no server-to-server way to enroll a customer -- this is deliberate. A subscription is a standing authorization to move a specific person's money; only that person's own signature can create it.

Step 4: The Plan request and response shapes

Every field the API actually accepts and returns for plan creation.

plan-shape.json
// Request body (POST /api/v1/subscriptions/plans)
{
  "name": "Pro Monthly",
  "priceWei": "10000000000000000000",  // decimal wei string, 18 decimals
  "asset": "NHB",                      // "NHB" or "ZNHB"
  "intervalSeconds": 2592000,          // billing cycle length
  "trialPeriodSeconds": 0              // optional, default 0
}

// Response (201) -- priceWei/asset/intervalSeconds/trialPeriodSeconds
// are permanently fixed from here on; only name/active can change later.
{
  "planId": "42",
  "name": "Pro Monthly",
  "priceWei": "10000000000000000000",
  "asset": "NHB",
  "intervalSeconds": 2592000,
  "trialPeriodSeconds": 0,
  "active": true,
  "createdAt": "2026-08-13T00:00:00.000Z"
}

Step 5: Configure your webhook (once per workspace)

Reuses the exact same webhook configured in Developer -> Webhooks for checkout events -- one URL and secret per workspace, verified the same way. Subscriptions add three new event types: invoice.paid, invoice.payment_failed, and subscription.suspended (fired after too many consecutive failed charges). Unlike checkout's fire-and-forget delivery, these billing-critical events are retried with backoff if your endpoint is briefly down -- verify each delivery the same way as any other webhook: recompute hex(hmac_sha256(secret, "POST|" + webhookPath + "|" + rawRequestBody + "|" + xTimestampHeader)) (where webhookPath is the path of your own webhook URL, rawRequestBody is the exact raw bytes received, and xTimestampHeader is the x-timestamp header's value) and compare it to the x-signature header with a constant-time comparison.

webhook-handler.js
import { createHmac, timingSafeEqual } from 'node:crypto';

app.post('/webhooks/nhbcoin', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.header('x-timestamp');
  const signature = req.header('x-signature');
  const eventType = req.header('x-nhb-event'); // e.g. invoice.paid
  const rawBody = req.body; // Buffer -- must be the exact raw bytes

  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!timestamp || ageSeconds > 300) {
    return res.status(400).send('stale or missing timestamp');
  }

  const canonical = ['POST', req.path, rawBody.toString('utf8'), timestamp].join('|');
  const expected = createHmac(
    'sha256',
    process.env.NHB_WEBHOOK_SECRET // whsec_..., same one used for checkout
  ).update(canonical).digest('hex');

  const expectedBuf = Buffer.from(expected, 'hex');
  const gotBuf = Buffer.from(signature ?? '', 'hex');
  if (expectedBuf.length !== gotBuf.length || !timingSafeEqual(expectedBuf, gotBuf)) {
    return res.status(401).send('invalid signature');
  }

  const event = JSON.parse(rawBody);
  if (eventType === 'invoice.paid') {
    // event.subscriptionId, event.amountWei, event.feeWei, event.chargedAt
  } else if (eventType === 'invoice.payment_failed') {
    // event.failureReason -- the subscriber already got a dunning email,
    // this is your hook to react too (e.g. flag the account internally).
  } else if (eventType === 'subscription.suspended') {
    // Too many consecutive failed charges -- the subscription will not
    // charge again unless the customer subscribes fresh.
  }

  res.sendStatus(200);
});

Step 6: Managing plans and subscriptions from your backend

The full API mirrors everything the dashboard can do: list/get plans and subscriptions, read charge history, update a plan's name or active flag, and cancel any subscription against your own plans (a merchant can always stop a subscription it owns -- it can never create one on a customer's behalf). Plan writes are capped at 30 requests/minute per API key; a 429 means back off, don't retry immediately.

manage.http
// List your plans
GET /api/v1/subscriptions/plans
Headers: { "X-Api-Key": NHB_API_KEY }

// Update a plan's name or active flag (pricing terms are immutable)
PATCH /api/v1/subscriptions/plans/42
Headers: { "X-Api-Key": NHB_API_KEY, "Idempotency-Key": "..." }
Body: { "name": "Pro Monthly (legacy)", "active": false }

// List subscriptions against your plans
GET /api/v1/subscriptions?status=active

// One subscription's full charge history
GET /api/v1/subscriptions/{subscriptionId}/charges

// Cancel a subscription against one of your own plans --
// stops future charges immediately, nothing already charged is refunded
DELETE /api/v1/subscriptions/{subscriptionId}
Headers: { "X-Api-Key": NHB_API_KEY, "Idempotency-Key": "..." }

// Your live fee rate
GET /api/v1/subscriptions/config

Step 7: What your customer sees

After subscribing, nothing further happens in any browser -- charges are deducted automatically by the chain itself on schedule, with no notification required from you. NHBCoin sends the subscriber a reminder email a few days before each charge and a payment-failure notice if a charge doesn't go through, so you don't need to build billing communications yourself.

Step 8: The fee -- a fraction of what you'd pay elsewhere

NHBCoin takes a small management fee on each successful charge (currently 1%, capped at 5% -- call GET /api/v1/subscriptions/config for the live rate), deducted automatically before your proceeds are credited. It's charged in addition to nothing else -- there's no separate per-transaction card-network fee stacked on top, and no monthly platform fee just to keep Subscriptions turned on.