Documentation

Build with HelaMesh

Non-custodial crypto payments via API, hosted checkout, or embeddable SDK. Five minutes from signup to your first paid invoice — no KYC, no custody, no lockup.

#Quickstart

HelaMesh is a non-custodial crypto payment gateway. Funds flow directly to your wallet — HelaMesh never holds them. You create invoices via the API or dashboard, share a checkout link with your customer, and receive an HMAC-signed webhook the moment the payment confirms on chain.

Five minutes from signup to first payment:

  1. Create an account and a test client. Save the API key (shown once).
  2. Create an invoice via the API.
  3. Share the returned checkout URL with your customer.
  4. Receive a webhook at your endpoint when the payment is confirmed.

Create your first invoice

curl
curl -X POST https://api.helamesh.com/v1/merchant/invoices \
  -H "x-api-key: hm_test_YOUR_KEY" \
  -H "content-type: application/json" \
  -d '{
    "clientId": "YOUR_CLIENT_ID",
    "amount": 25.00,
    "metadata": { "orderId": "1234" }
  }'

Response:

200 OK
{
  "id": "65f8a1b2c3d4e5f6a7b8c9d0",
  "amount": "25.000000",
  "token": "USDT",
  "network": "TRON",
  "status": "PENDING",
  "paymentAddress": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "expiresAt": "2026-04-10T18:30:00.000Z",
  "txHash": null,
  "confirmations": 0,
  "metadata": { "orderId": "1234" }
}

The hosted checkout link is https://pay.helamesh.com/pay/{id}. Share that with your customer.

#Authentication

HelaMesh exposes two distinct authentication surfaces:

SurfaceUsed byHeader
API keyYour backend → HelaMesh APIx-api-key: hm_test_… / hm_live_…
Session cookieBrowser → HelaMesh dashboardCookie: hm_session=… (httpOnly)

You only need API keys to integrate. Generate one when you create a client in the dashboard. Keys are SHA-256 hashed at rest and shown exactly once at creation — store them in your secrets manager.

Treat API keys like passwords. Never commit them. Never ship them in client-side bundles. If a key leaks, rotate it immediately from Settings → Rotate API key.

#Test vs live mode

Each client belongs to one of two environments, indicated by the API key prefix:

  • hm_test_* — test mode. In dev with MOCK_BLOCKCHAIN=true, you can simulate payments from the dashboard with one click.
  • hm_live_* — production. Real on-chain payments only.

Test and live data are completely isolated. Test invoices never appear in live reports and vice-versa.

#Errors

HelaMesh uses standard HTTP status codes:

CodeMeaning
200Success
201Resource created
400Bad request — validation failed
401Missing or invalid API key
403Authenticated but not authorized for this resource
404Resource not found
409Conflict (e.g. duplicate open invoice)
429Rate limited
5xxHelaMesh error — safe to retry with backoff

Error response shape:

409 Conflict
{
  "statusCode": 409,
  "message": "An open invoice for the same address and amount already exists. Wait for it to complete/expire, or use a different amount.",
  "error": "Conflict"
}

#POST /merchant/invoices

Create a new invoice. Returns the full invoice object including the hosted checkout payment address.

Headers

text
x-api-key: hm_test_…
content-type: application/json

Body

FieldTypeRequiredNotes
clientIdstringyesThe client this invoice belongs to
amountnumberyesDecimal USDT, 6 decimal places max
metadataobjectnoFree-form JSON, returned in webhook

Example

node.js
const res = await fetch('https://api.helamesh.com/v1/merchant/invoices', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.HELAMESH_API_KEY,
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    clientId: process.env.HELAMESH_CLIENT_ID,
    amount: 25.00,
    metadata: { orderId: '1234', customer: 'alice@example.com' },
  }),
});

const invoice = await res.json();
console.log('Pay here:', `https://pay.helamesh.com/pay/${invoice.id}`);

#GET /invoices/:id

Public endpoint — no authentication required. Returns the safe invoice shape used by the hosted checkout page.

bash
curl https://api.helamesh.com/v1/invoices/65f8a1b2c3d4e5f6a7b8c9d0

#Invoice states

An invoice moves through this lifecycle:

text
PENDING → DETECTED → CONFIRMING → CONFIRMED → COMPLETED
   │
   └─→ EXPIRED  (terminal, only from PENDING)
StateMeaning
PENDINGCreated, awaiting payment
DETECTEDPayment seen on chain (0+ confirmations)
CONFIRMINGAccumulating confirmations
CONFIRMEDEnough confirmations (≥20 on TRON)
COMPLETEDWebhook delivered successfully (2xx response)
EXPIREDNot paid before expiry (default 30 minutes)

#Webhooks overview

When a payment hits CONFIRMED, HelaMesh sends a signed HTTP POST to the webhook URL on your client. Your backend should:

  1. Verify the signature
  2. Look up the invoice by ID in your own database
  3. Mark the order as paid
  4. Return 200 OK within 10 seconds

If you return any non-2xx status (or time out), HelaMesh retries with exponential backoff.

#Webhook payload

POST your-webhook-url
{
  "event": "payment.confirmed",
  "invoiceId": "65f8a1b2c3d4e5f6a7b8c9d0",
  "timestamp": "2026-04-10T18:32:14.000Z",
  "amount": "25.000000",
  "token": "USDT",
  "network": "TRON",
  "txHash": "abc123…",
  "confirmations": 20
}

Sent with these headers:

text
Content-Type: application/json
User-Agent: HelaMesh-Webhooks/1.0
X-HelaMesh-Signature: t=1700000000,v1=<hex>

#Verifying signatures

Every webhook is signed with HMAC-SHA256 using your client's webhook secret. The signed payload is `${timestamp}.${rawBody}` which prevents both tampering and replay attacks.

Node.js example

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

const SECRET = process.env.HELAMESH_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300; // 5 minutes

function verifySignature(header, rawBody) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=')),
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!t || !v1) return false;

  // Reject signatures outside the tolerance window
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_SECONDS) return false;

  const expected = createHmac('sha256', SECRET)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  return expected.length === v1.length &&
    timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));
}

// Express handler — note rawBody is required, not parsed JSON
app.post('/webhooks/helamesh', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.header('X-HelaMesh-Signature');
  if (!sig || !verifySignature(sig, req.body.toString())) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  // Mark the order paid in your database
  await markOrderPaid(event.invoiceId, event.txHash);
  res.status(200).send('ok');
});
You must verify against the raw request body, not the parsed JSON. JSON.stringify is not deterministic across languages — a re-serialised body will have a different hash and your signature will fail. Express users: use express.raw() middleware on the webhook route only.

#Webhook retries

If your endpoint returns non-2xx or times out, HelaMesh retries with exponential backoff:

text
Attempt 1 — immediately
Attempt 2 — 2 seconds later
Attempt 3 — 4 seconds later
Attempt 4 — 8 seconds later
…
Attempt 8 — 256 seconds later
After 8 attempts → marked FAILED

FAILED webhooks can be re-driven manually from the dashboard: Webhooks → click Redrive.

Your handler should be idempotent. The same invoiceId may arrive more than once if you returned 2xx but HelaMesh didn't see it (network blip). Always check your DB for an existing record before applying the payment.

#Hosted checkout

Every invoice has a hosted checkout page at https://pay.helamesh.com/pay/{invoiceId}. The page renders the amount, TRON address, scannable QR, copy button, and live status updates as the payment moves through DETECTED → CONFIRMING → CONFIRMED → COMPLETED.

You can:

  • Redirect customers to it directly
  • Embed it in an iframe (no special config needed)
  • Open it in a popup window
  • Share the link via WhatsApp, email, SMS

The checkout polls the public GET /v1/invoices/:id endpoint every 4 seconds, so the customer sees status changes within seconds of the chain confirming.

#Testing without spending crypto

When the API is running with MOCK_BLOCKCHAIN=true, the dashboard shows a ⚡ Simulate payment button on any test-mode PENDING invoice.

Clicking it injects a synthetic on-chain transfer matching that invoice. Within ~15 seconds the poller picks it up and runs the full state machine + webhook pipeline as if a real customer had paid. Use this to test your webhook handler end-to-end without spending real USDT.

Simulate is gated three ways: requires MOCK_BLOCKCHAIN=true, a test-environment client, and a PENDING invoice. Live invoices ALWAYS require a real on-chain payment — there is no production code path that bypasses the chain.

#HelaMesh JS SDK

Embed the HelaMesh checkout directly inside your own page. The SDK is a tiny script (~3kb gzipped) that mounts an iframe-based widget you control via JavaScript callbacks. No payment UI to build.

The model is the same as Stripe Elements: your server creates the invoice and gets back an ID, then your browser code uses that ID plus a publishable key to render the payment widget. The customer never leaves your site.

The SDK is loaded from https://pay.helamesh.com/sdk/v1.js. In dev pointed at localhost, use http://localhost:4000/sdk/v1.js.

#Publishable keys

Each client has two keys with different scopes:

KeyWhere it livesCan do
hm_test_… / hm_live_…Server only — never expose to browserCreate invoices, list, rotate, everything
pk_test_… / pk_live_…Browser — safe to ship in JS bundlesRead invoices belonging to its client. Nothing else.

The publishable key is scope-restricted on the server side. Even if someone scrapes it from your page source, they can only use it to read invoices that already exist for your client. They cannot create, list, or modify anything.

Find your publishable key in the dashboard under Clients. It's shown on every client card.

#HTML / vanilla JS

The simplest possible integration. Drop into any HTML page:

index.html
<!-- Load the SDK -->
<script src="https://pay.helamesh.com/sdk/v1.js"></script>

<!-- Where the checkout will render -->
<div id="helamesh-checkout"></div>

<script>
  HelaMesh.mount('#helamesh-checkout', {
    invoiceId: 'YOUR_INVOICE_ID',
    publishableKey: 'pk_test_YOUR_KEY',
    onPaid: (event) => {
      console.log('Payment received', event.invoice);
      window.location.href = '/order/success';
    },
    onExpired: () => {
      alert('Payment window expired. Please try again.');
    },
  });
</script>

You only need three things on the page: the script tag, a div with an id, and the mount call. The SDK handles the iframe, polling, auto-resize, and lifecycle events.

#React

CheckoutWidget.tsx
import { useEffect, useRef } from 'react';

declare global {
  interface Window {
    HelaMesh?: { mount: (target: any, opts: any) => { destroy: () => void } };
  }
}

export function CheckoutWidget({ invoiceId }: { invoiceId: string }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let instance: { destroy: () => void } | null = null;

    const tryMount = () => {
      if (!ref.current || !window.HelaMesh) return false;
      instance = window.HelaMesh.mount(ref.current, {
        invoiceId,
        publishableKey: process.env.NEXT_PUBLIC_HELAMESH_PK!,
        onPaid: (event) => {
          // Update your local state, redirect, fulfil order
          window.location.href = `/order/success?inv=${event.invoice.id}`;
        },
      });
      return true;
    };

    if (!tryMount()) {
      // SDK not loaded yet — wait for it
      const script = document.createElement('script');
      script.src = 'https://pay.helamesh.com/sdk/v1.js';
      script.async = true;
      script.onload = tryMount;
      document.body.appendChild(script);
    }

    return () => instance?.destroy();
  }, [invoiceId]);

  return <div ref={ref} />;
}

#Next.js (App Router)

app/checkout/[id]/page.tsx
'use client';

import Script from 'next/script';
import { useEffect, useRef } from 'react';

export default function CheckoutPage({ params }: { params: { id: string } }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const interval = setInterval(() => {
      // @ts-ignore — SDK attaches to window.HelaMesh
      if (!window.HelaMesh || !ref.current) return;
      clearInterval(interval);

      // @ts-ignore
      const instance = window.HelaMesh.mount(ref.current, {
        invoiceId: params.id,
        publishableKey: process.env.NEXT_PUBLIC_HELAMESH_PK!,
        onPaid: () => {
          window.location.href = '/order/success';
        },
      });

      return () => instance.destroy();
    }, 100);

    return () => clearInterval(interval);
  }, [params.id]);

  return (
    <>
      <Script
        src="https://pay.helamesh.com/sdk/v1.js"
        strategy="afterInteractive"
      />
      <div ref={ref} />
    </>
  );
}

Note: NEXT_PUBLIC_HELAMESH_PK must be set in your env and must start with NEXT_PUBLIC_ for Next.js to expose it to the browser bundle. The publishable key is safe to ship there. Never use the secret hm_* key in any NEXT_PUBLIC_* variable.

#Lifecycle events

Every state change in the embed fires a callback you can hook into. Use these to drive your own UI: show a spinner during confirmation, redirect on success, log analytics, etc.

javascript
HelaMesh.mount('#checkout', {
  invoiceId: 'abc123',
  publishableKey: 'pk_test_…',

  onReady: ({ invoice }) => {
    // Embed mounted, showing PENDING state
  },
  onStatusChange: ({ from, to, invoice }) => {
    // Fires for EVERY transition. Great for analytics.
    analytics.track('helamesh_status', { from, to });
  },
  onDetected: ({ invoice }) => {
    // Payment seen on chain (0+ confirmations)
  },
  onConfirming: ({ invoice }) => {
    // Accumulating confirmations
  },
  onConfirmed: ({ invoice }) => {
    // Enough confirmations, last step before COMPLETED
  },
  onPaid: ({ invoice }) => {
    // TERMINAL: invoice is COMPLETED. This is "done".
    window.location.href = '/order/success';
  },
  onExpired: ({ invoice }) => {
    // TERMINAL: payment window passed without payment
  },
  onError: ({ message }) => {
    console.error('HelaMesh embed error:', message);
  },
});

#Theming

The embed accepts three theme tokens that override its default dark palette. All three accept any CSS color value.

javascript
HelaMesh.mount('#checkout', {
  invoiceId: 'abc123',
  publishableKey: 'pk_test_…',
  theme: {
    accentColor: '#3b82f6',     // buttons, hover states, badge accents
    backgroundColor: '#0f172a', // card background
    textColor: '#f1f5f9',       // primary text
  },
});

The theme tokens are applied via CSS variables on the embed root, which are then consumed by the embed's stylesheet. Phase 2 will expose more granular tokens (border colors, success/danger, font family) — these three cover the 95% case.

You can also try the embed visually with live theme controls in the Embed demo page in your dashboard. It generates ready-to-paste integration code as you tweak the colors.

Got stuck? Email hello@helamesh.com — a real human reads it.