Developers

Quickstart for accepting payments with the LevinGate API.

TESTNET environment

Quickstart in your language

Node.js

18+ · built-in fetch

docs/developers/nodejs.md

Python

requests

docs/developers/python.md

.NET (C#)

.NET 8 · HttpClient

docs/developers/dotnet.md

Go

1.22+ · net/http

docs/developers/go.md

Each guide covers create → retrieve → list → webhook verification → errors, end to end.

1. Create an invoice

Amounts are decimal strings — never floating point. Tokens are identified by contract address on the backend; you send asset + network. Send an Idempotency-Key so retries never double-create.

curl -X POST https://api.levingate.example/api/v1/invoices \
  -H "Authorization: Bearer sk_test_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_12345" \
  -d '{
    "amount": "25.00",
    "currency": "USD",
    "asset": "BNB",
    "network": "BSC",
    "external_order_id": "order_12345",
    "customer_email": "customer@example.com"
  }'

Interactive reference: Swagger UI at /api/docs

2. Endpoint map

POST/invoicescreate (Idempotency-Key respected)
GET/invoices/:idstatus + checkout_url
GET/invoiceslist · cursor pagination
GET/payments/:idon-chain payment detail
GET/transactionsplatform tx list · env-scoped
GET/networksactive networks (current env)
GET/assetsactive assets (current env)
GET/statstotals + daily series (?days=1..90)
POST/webhooksdashboard sessions only — 403 with API keys (by design)

3. Listing & errors

# Cursor pagination — follow next_cursor while has_more is true
curl "https://api.levingate.example/api/v1/invoices?limit=50" \
  -H "Authorization: Bearer sk_test_your_api_key"

# Errors always look like:
# { "error": { "code": "INVALID_REQUEST", "message": "…", "request_id": "req_…" } }

4. Receive webhooks

Configure your endpoint under Webhooks in the sidebar (dashboard-only — API keys cannot manage webhook endpoints by design). Every delivery is signed with HMAC-SHA256 over timestamp + '.' + raw_body. Always verify against the raw request body, before JSON parsing.

import { createHmac, timingSafeEqual } from "node:crypto";

/**
 * Verify a LevinGate webhook (§28).
 * Header: X-LevinPay-Signature: t=<timestamp>,v1=<hmac_sha256(secret, ts + "." + raw_body)>
 */
export function verifyWebhook(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.trim().split("=")),
  );
  const { t, v1 } = parts;
  if (!t || !v1) throw new Error("Malformed signature header");

  // Reject replays older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) {
    throw new Error("Timestamp outside tolerance window");
  }

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

  if (
    expected.length !== v1.length ||
    !timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
  ) {
    throw new Error("Signature mismatch");
  }
  return JSON.parse(rawBody);
}

A ready-made verifier is also available in @levinpay/sdk.

Good to know

Invoices are marked PAID only after the configured number of confirmations.

Payments arriving after expiry are recorded with is_late and routed to reconciliation — the invoice stays EXPIRED.

LevinGate is non-custodial: funds go straight to your own wallet. We never hold keys or sign transactions.