Developers & APIs

Crypto payment API SDK: which crypto payment API has official SDKs for Node.js, PHP and Python?

Short answer: Payzum publishes official, open-source crypto payment API SDKs for Node.js/TypeScript (npm payzum), PHP (payzum/payzum-php), Python (payzum on PyPI) and Rust (payzum on crates.io). Each creates an invoice in a few lines and verifies signed webhooks against one shared test corpus. Payments settle non-custodially to your own wallet.

Key takeaways

  • Four official SDKs, all MIT, all public. Version 0.1.0 of each went live between 30 and 31 August 2026 on npm, Packagist, PyPI and crates.io. The source is on GitHub under payzum-dev, next to an OpenAPI 3.1 contract.
  • Same three moves in every language. Create an invoice, redirect the buyer to the hosted checkout, and verify the signed webhook before you ship anything.
  • The webhook check is the main reason to use an SDK. Payzum sends three signed webhook families with different algorithms and headers. The SDKs fix the header names, check signatures over the raw bytes, reject events older than 10 minutes and give you the event ID to deduplicate on.
  • Money never passes through a float. Amounts go in as decimal strings and come back exact. A create call is retried only when you pass an idempotency key, so a network hiccup can't create a second invoice.
  • Honest limits. These are 0.x releases, the Rust client is blocking, and there is no official Go, Java, .NET or Ruby SDK yet. For those stacks, use the OpenAPI contract.

Payzum is a non-custodial, stablecoin-first crypto payment processor: payments settle directly to wallets the merchant controls. If you are choosing a crypto payment API SDK, this guide shows the official SDKs for Node.js, PHP, Python and Rust. For each one you get the install line, an invoice and a verified webhook. Every snippet below was run against the published 0.1.0 packages on 6 October 2026. The guide is for developers adding crypto or stablecoin checkout to their own app. If you run a shopping cart, start with our open-source plugins instead, because one probably already exists for your platform.

Why developers look for a crypto payment API SDK, not just a REST reference

Developers pick payment providers by language, and the languages are lopsided. In the 2025 Stack Overflow Developer Survey, 66% of respondents had done extensive work in JavaScript, 57.9% in Python, 43.6% in TypeScript, 18.9% in PHP and 14.8% in Rust. Rust was again the most admired language, at 72%. On the web itself the order flips: W3Techs reported on 6 October 2026 that PHP runs 69.8% of websites whose server-side language it can detect.

Those four ecosystems cover most of the teams that would ever wire up a checkout. Any one of them can call a REST API. The trouble is that a payment integration isn't just HTTP calls. Three parts are hard, and every team that integrates from the reference alone writes them again:

  • Webhook verification. You need the right header and the right HMAC algorithm. You need the raw bytes, a constant-time comparison and a replay window.
  • Exact money. Amounts must never pass through a binary float, in either direction.
  • Safe retries. A timeout on "create invoice" must not turn into two invoices.

A REST reference documents these things. An SDK does them for you, the same way in every language.

What a hand-rolled crypto payment integration costs when payments are final

A wrong header fails silently. Payzum signs payment notifications with HMAC-SHA-512 in the x-nowpayments-sig header. Mass-payout webhooks use HMAC-SHA-256 in X-Payzum-Signature. If a handler checks a payment against the payout header, the signature never verifies and every delivery gets a 401. The gateway retries five times and then dead-letters the event. The customer has paid on-chain, and the order sits unpaid with no error anyone notices. We know because early versions of our own cart plugins made exactly this mix-up. The fix is why every SDK now verifies against one shared test corpus.

Floats quietly change amounts. In JavaScript, 19.99 * 100 evaluates to 1998.9999999999998. Round that the wrong way once and an invoice or a reconciliation is off by a cent. That cent is enough to fail an "amount paid equals order total" check.

Re-serialised JSON breaks the signature. The signature covers the exact bytes Payzum sent. If your framework parses the body and you sign it again, key order and number formatting can change. A value like 2.50 may come back as 2.5, and a genuine webhook then fails.

A blind retry can charge twice. Invoice creation accepts an idempotency key, but the deduplication is eventually consistent over about 60 seconds, and order_id isn't enforced as unique. A generic HTTP client that retries every 5xx or 429 can create a second real invoice. With cards you could refund or wait for a dispute. On-chain payments are final, so the undo is a manual transfer from your wallet.

Why card-SDK habits and generic HTTP clients don't carry over

Card-processor SDKs were built for a world where the processor holds the money and every payment can be reversed. The processor captures funds into its own balance, pays you out days later, and keeps a dispute window open for months. Mistakes are cheap there because almost everything can be undone. With crypto, the buyer pushes a final payment straight to an address. The signed webhook is your source of truth, and the browser's return URL is not.

Generic HTTP clients get the edge cases wrong too. Payzum has 16 error codes, and only three are worth retrying: RATE_LIMIT_EXCEEDED, INTERNAL_ERROR and RATE_PROVIDER_DOWN. QUOTA_EXCEEDED also arrives as a 429, but it means too many invoices are already open, so retrying it only makes things worse. Community wrappers can be good, but they depend on one maintainer and usually cover one webhook family. Nobody checks them against the provider's own test vectors. Custodial gateways' SDKs bring their own issue: they talk to a balance the gateway holds, not to your wallet.

Payzum's official crypto payment API SDKs at a glance

The four SDKs share one design and one test corpus. Here is what each one needs, as published on the registries and checked on 6 October 2026:

LanguagePackage and installRuntimeDependencies0.1.0 released
Node.js / TypeScriptnpm install payzumNode 18.17+, ESM, typedNone30 Aug 2026 (npm)
PHPcomposer require payzum/payzum-phpPHP 8.1+ with ext-curlNone30 Aug 2026 (Packagist)
Pythonpip install payzumPython 3.9+None31 Aug 2026 (PyPI)
Rustpayzum = "0.1" in Cargo.tomlBlocking client; declares Rust 1.75hmac, sha2, serde_json, ureq31 Aug 2026 (crates.io)

All four are MIT-licensed. The source lives at payzum-node, payzum-php, payzum-python and payzum-rust on GitHub. Two more pieces sit behind them:

  • The contract. payzum-openapi holds the OpenAPI 3.1 description of the /v1 API, 32 paths extracted from the gateway's handler code.
  • The test vectors. The same repo holds a webhook-signature corpus that every SDK must pass. It covers valid and tampered signatures, uppercase hex, truncated signatures, stale events and cross-scheme confusion. That last case is the header mix-up above, reproduced on purpose.

What every SDK does for you

  • Fixed webhook headers. Header names are built into the verifier and aren't settings, because a setting can be filled in wrong. If a payment notification arrives with the payout header, the Python SDK says so in its error message.
  • Raw-byte verification with a replay window. The verifier rejects events whose timestamp is more than 600 seconds (10 minutes) from now. That holds even when the signature itself is valid.
  • Exact decimals. Node and PHP return amounts as exact decimal strings, Python as Decimal, and Rust as arbitrary-precision JSON numbers.
  • Narrow, safe retries. Only the three retryable codes are retried, and the SDK honours the server's back-off hints. A create is retried only when you pass an idempotency key, and only after the 60-second consistency window.
  • Typed errors. You branch on error_code values such as AMOUNT_BELOW_MINIMUM, never on message text. Signature, transport and validation failures each get their own type.
  • A status vocabulary that fails closed. A payment is waiting, partially_paid, finished, expired or failed. An unexpected value throws an error instead of being read as "paid".

The three webhook families, and the method for each

FamilyAlgorithm and headerReplay defenceSDK method
Payment IPN (NOWPayments-compatible)HMAC-SHA-512, x-nowpayments-sig, key-sorted JSONevent_at within 10 min; dedupe on the x-payzum-event-id headerverifyPaymentIpn / verify_payment_ipn
CoinPayments-compatible IPNHMAC-SHA-512, HMAC header, form-encoded bodyNo timestamp exists, so dedupe on ipn_idverifyCoinPaymentsIpn / verify_coinpayments_ipn
Mass payoutHMAC-SHA-256, X-Payzum-Signature, JSON as senteventAt within the window; dedupe on eventIdverifyMassPayout / verify_mass_payout

HMAC itself is the standard keyed-hash construction from RFC 2104. The SDKs add no new cryptography. They just make it hard to apply the right primitive to the wrong bytes. If you want the background on why each step matters, read our guide to signed webhooks for crypto payments.

How to use the crypto payment API SDK, step by step

  1. Get two secrets from the dashboard. In the Payzum dashboard, copy your API key and your webhook secret. They are separate, and the webhook secret is shown once, when it is created or rotated. Choose which coins and networks buyers may use there too. That allowlist lives in the dashboard, not in code.
  2. Install the SDK for your language (table above). Use Payzum.sandbox(...) or Payzum::sandbox(...) while you build. It points at the staging host, which has isolated data and separate API keys.
  3. Create an invoice and redirect. Pass the price as a decimal string, "all" as the pay currency so the buyer picks a coin, your order ID, your webhook URL and an idempotency key. Then send the buyer to the returned invoice_url, the hosted checkout.
  4. Verify the webhook, then fulfil once. Hand the verifier the raw request body and the headers. Fulfil only when the status counts as paid, and use the event ID so a repeat delivery does nothing.

All three SDKs that we ran with a stub transport sent the same request: POST /v1/payment with an Idempotency-Key header and the amount written as the exact decimal you passed. Here is the full flow in each language.

Node.js / TypeScript (Express)

// npm install payzum   ·  Node 18.17+, ESM only, zero runtime dependencies
import express from "express"
import { Payzum, SignatureError, isPaidStatus, paymentStatusFromMerchant } from "payzum"

const payzum = new Payzum(config.payzumApiKey)            // Payzum.sandbox(key) while building
const verifier = payzum.webhooks(config.payzumWebhookSecret)
const app = express()

// 1) Create the invoice and send the buyer to the hosted checkout
app.post("/checkout/:orderId", async (req, res) => {
  const order = await orders.find(req.params.orderId)
  const invoice = await payzum.payments.create({
    priceAmount: order.total,             // a decimal string such as "49.99", never a number
    priceCurrency: "usd",
    payCurrency: "all",                   // the buyer picks the coin from your dashboard allowlist
    orderId: order.id,
    ipnCallbackUrl: "https://shop.example/payzum/ipn",
    idempotencyKey: order.id,             // makes a retry safe
  })
  res.redirect(303, String(invoice["invoice_url"]))
})

// 2) Verify the signed IPN on the RAW body (no express.json() on this route)
app.post("/payzum/ipn", express.raw({ type: "*/*" }), async (req, res) => {
  try {
    const data = verifier.verifyPaymentIpn(req.body, req.headers)
    const status = paymentStatusFromMerchant(String(data["payment_status"]))
    if (isPaidStatus(status)) {
      await orders.markPaidOnce(String(data["order_id"]), verifier.eventId(req.headers))
    }
    res.sendStatus(200)
  } catch (e) {
    if (e instanceof SignatureError) { res.status(401).send("bad signature"); return }
    throw e
  }
})

PHP (any framework, or none)

// composer require payzum/payzum-php   ·  PHP 8.1+ with ext-curl, zero dependencies
use Payzum\Payzum;
use Payzum\PaymentStatus;
use Payzum\Errors\SignatureException;

$payzum = new Payzum($config['payzum_api_key']);      // Payzum::sandbox($key) while building

// checkout.php: create the invoice and redirect to the hosted checkout
$invoice = $payzum->payments->create(
    priceAmount: $order->total,           // a string such as '49.99', never a float
    priceCurrency: 'usd',
    payCurrency: 'all',
    orderId: $order->id,
    ipnCallbackUrl: 'https://shop.example/payzum/ipn.php',
    idempotencyKey: $order->id,
);
header('Location: ' . $invoice['invoice_url'], true, 303);

// ipn.php: verify on the RAW body
$verifier = $payzum->webhooks($config['payzum_webhook_secret']);
$headers  = getallheaders();
try {
    $data = $verifier->verifyPaymentIpn(file_get_contents('php://input'), $headers);
} catch (SignatureException $e) {
    http_response_code(401);
    exit('bad signature');
}
if (PaymentStatus::fromMerchant($data['payment_status'])->isPaid()) {
    markPaidOnce($data['order_id'], $verifier->eventId($headers));
}
http_response_code(200);

Python (Flask; the same calls work in Django or FastAPI)

# pip install payzum   ·  Python 3.9+, zero runtime dependencies
from flask import Flask, redirect, request
from payzum import Payzum, PaymentStatus, SignatureError

payzum = Payzum(settings.PAYZUM_API_KEY)                 # Payzum.sandbox(key) while building
verifier = Payzum.webhooks(settings.PAYZUM_WEBHOOK_SECRET)
app = Flask(__name__)

@app.post("/checkout/<order_id>")
def checkout(order_id):
    order = orders.get(order_id)
    invoice = payzum.payments.create(
        price_amount=order.total,         # str or Decimal, never a float
        price_currency="usd",
        pay_currency="all",
        order_id=order.id,
        ipn_callback_url="https://shop.example/payzum/ipn",
        idempotency_key=order.id,
    )
    return redirect(invoice["invoice_url"], 303)

@app.post("/payzum/ipn")
def ipn():
    try:
        data = verifier.verify_payment_ipn(request.get_data(), request.headers)  # RAW bytes
    except SignatureError:
        return "bad signature", 401
    if PaymentStatus.from_merchant(data["payment_status"]).is_paid():
        orders.mark_paid_once(data["order_id"], verifier.event_id(request.headers))
    return "", 200

Rust (framework-agnostic)

// Cargo.toml: payzum = "0.1"   ·  blocking client (ureq): see the limits below
use payzum::{CreatePaymentParams, PaymentStatus, Payzum, PayzumError};

fn checkout(payzum: &Payzum, order: &Order) -> Result<String, PayzumError> {
    let invoice = payzum.payments().create(
        &order.total,                                  // "49.99": a decimal string, never f64
        "usd",
        "all",                                         // the buyer picks the coin
        CreatePaymentParams {
            order_id: Some(order.id.as_str()),
            ipn_callback_url: Some("https://shop.example/payzum/ipn"),
            idempotency_key: Some(order.id.as_str()),  // makes a retry safe
            ..Default::default()
        },
    )?;
    Ok(invoice["invoice_url"].as_str().unwrap_or_default().to_string())
}

fn on_ipn(raw_body: &[u8], headers: &[(String, String)], secret: &str) -> Result<(), PayzumError> {
    let verifier = Payzum::webhooks(secret)?;
    let data = verifier.verify_payment_ipn(raw_body, headers, None)?;   // Err(Signature) → answer 401
    let status = PaymentStatus::from_merchant(data["payment_status"].as_str().unwrap_or_default())?;
    if status.is_paid() {
        mark_paid_once(data["order_id"].as_str().unwrap_or_default(), verifier.event_id(headers));
    }
    Ok(())
}

What to do at each payment status

StatusMeaningWhat your code should do
waitingInvoice created, nothing paid yetNothing. Show "awaiting payment" if the buyer comes back.
partially_paidLess than the invoice amount arrivedDon't fulfil yet. Hold the order and review it if it never reaches finished.
finishedPaid in full, including overpaymentFulfil once. The paid check returns true.
expiredThe invoice window closed unpaidRelease any reserved stock and offer a new checkout.
failedThe payment did not completeDon't fulfil. Let the buyer retry.

Beyond the happy path, each SDK can read one invoice by Payzum ID or by your own order ID (payments.get), so you don't need a mapping table for reconciliation. It can also list invoices page by page, read the supported asset-and-chain catalogue, and fetch conversion estimates. One of those calls returns the network minimum for a currency pair. Check it before you create an invoice, because an amount below the minimum is rejected with AMOUNT_BELOW_MINIMUM. For the bigger picture of what the API covers, see our REST API crypto payment gateway guide.

Where the money goes

The SDK only talks to Payzum's API. The buyer's payment settles directly to a wallet you control, and Payzum never holds a balance for you, so there is nothing to freeze and no payout schedule to wait on. Typical confirmation takes about 0.4 seconds on Solana and about 2 seconds on Base or Polygon. If you'd rather not hold volatile coins, turn on auto-convert to USDC or USDT. It is optional and available on most supported networks.

What the SDKs don't do (yet)

  • They're 0.x. Every SDK is at 0.1.0. Under semantic versioning, anything may change before 1.0, so pin the exact version and read the changelog before you upgrade.
  • The Rust client is blocking. It is built on ureq. In a Tokio or other async service, call it from spawn_blocking or plug in your own client through the Transport trait. One more note: the crate declares Rust 1.75. In our 6 October 2026 test, a fresh build on Cargo 1.75 failed because a dependency now requires the 2024 edition. It built and passed on current stable, so use a recent toolchain.
  • The Node package is ESM-only. A CommonJS project needs a dynamic import().
  • No Go, Java, .NET or Ruby SDK as of 6 October 2026. Generate a client from the OpenAPI 3.1 contract and use the shared vectors to test your webhook check. .NET stores on nopCommerce have an official plugin instead.
  • The SDK doesn't store state. It hands you the event ID, but deduplication, order updates and refunds are your code. Refunds are sent from your wallet, because the payment was final.

Use cases: what teams build with a crypto payment API SDK

The SDKs suit teams whose checkout is their own code rather than a cart plugin. Five patterns we see:

  • A TypeScript SaaS selling annual plans abroad. The billing service creates one invoice per plan purchase, using the subscription ID as both the order ID and the idempotency key. The webhook route activates the plan when the status counts as paid. Support looks up an invoice with payments.get(subscriptionId), with no extra table. Buyers in countries where cards decline cross-border pay in USDC on Base, and the funds land in the company's own wallet.
  • A PHP agency building custom shops. Not every client runs WooCommerce or PrestaShop. For a Laravel, Symfony or plain-PHP storefront, the agency adds the same Composer package to each project. The same ipn.php pattern goes everywhere, so a new hire can't reinvent the webhook check.
  • A Python data API selling credit packs. A FastAPI backend sells prepaid credit bundles. Amounts come back as Decimal and map straight onto the database's numeric column, so no float rounding creeps into the credit balance. Django teams that already use the django-payments library can install the official django-payments-payzum backend instead.
  • A Rust game backend selling season passes. The studio's server creates an invoice when a player buys a pass. A worker thread verifies the webhook and unlocks the pass in the player's account, using the event ID to make sure it happens once. Season passes are digital, so final, chargeback-free payment removes the "bought, played, disputed" loop.
  • An affiliate network paying partners in stablecoins. The network sends monthly commissions with Payzum's EVM stablecoin mass payouts. Its backend uses verifyMassPayout to accept only genuine payout events, with HMAC-SHA-256 on the payout header, and marks each partner paid. It runs on the same SDK it uses to collect payments.

Migrating teams make up a sixth group. If your code already handles NOWPayments-style or CoinPayments-style IPNs, Payzum sends the same formats. verifyPaymentIpn and verifyCoinPaymentsIpn verify them without changes to your handler logic. If you run a headless store on Medusa, Saleor, Vendure or Sylius, there are ready-made modules; see stablecoin payments for headless commerce.

Official SDK vs hand-rolled REST vs card-processor SDK

Compared on neutral criteria. The card column describes the common pattern, not any single provider.

CriterionCard-processor SDK (typical)Hand-rolled client on a crypto APIPayzum official SDKs
Where funds landProcessor balance, then a bank payout in 1–3 daysDepends on the gateway; custodial gateways hold a balance firstYour own wallet, non-custodial
ReversalsChargebacks possible for about 120 daysOn-chain payments are finalFinal, no chargebacks
Webhook verificationProvided for the processor's own eventsYou write the header lookup, HMAC, constant-time compare and replay windowBuilt in for all three families, with fixed headers and a shared test corpus
Duplicate deliveriesEvent IDs to dedupe onYours to designEvent ID exposed by the verifier; you store it
Money precisionUsually integer minor units (cents)Easy to leak through a floatDecimal strings in, exact decimals out
Retrying "create"Idempotency keys are commonA blind retry can create a second invoiceRetried only with an idempotency key, after a 60-second window
LanguagesVaries by processorAny language, all the work is yoursNode.js/TypeScript, PHP, Python, Rust, plus an OpenAPI 3.1 contract

Common developer objections, answered

"We'd rather call the REST API directly."

You can. The OpenAPI 3.1 contract is public and the SDKs hide nothing. Most of the code in them goes into the three hard parts above. If you go direct, take the shared test vectors from the payzum-openapi repo and make your webhook check pass all of them, including the cross-scheme cases.

"Version 0.1 sounds early for payments."

That's a fair concern, and it's why the version number is honest. The surface is small: payments, invoices, currencies, rates, webhooks and a health check. Each SDK has its own test suite and must pass the shared signature corpus. Pin the exact version, build against the sandbox, and send a few real low-value payments before launch.

"We already integrated NOWPayments or CoinPayments."

Then most of your webhook handler carries over. Payzum's payment IPN uses the NOWPayments-compatible format and header, and a CoinPayments-compatible IPN exists too. The SDK verifiers handle both, so you mostly swap the client and the secrets.

"We don't want crypto volatility on our books."

Price in USD and let buyers pay in USDC or USDT, or turn on optional auto-convert to a stablecoin on the networks that support it. Either way the money settles in your own wallet. This is not financial or accounting advice, so confirm the treatment with your accountant.

Frequently asked questions

Which crypto payment API has official SDKs for Node.js, PHP and Python?

Payzum does. It publishes official, MIT-licensed SDKs for Node.js/TypeScript (npm payzum), PHP (Composer payzum/payzum-php), Python (PyPI payzum) and also Rust (crates.io payzum). Version 0.1.0 of each was released on 30–31 August 2026. Each SDK creates invoices and verifies all three signed webhook families against a shared test corpus, and payments settle non-custodially to the merchant's own wallet.

Is there a Rust SDK for crypto payments?

Yes. Add payzum = "0.1" to Cargo.toml. The Payzum Rust SDK creates invoices and verifies payment, CoinPayments-compatible and mass-payout webhooks. It is a blocking client built on ureq, so call it from spawn_blocking in async services or supply your own Transport. Use a recent stable toolchain: the crate declares Rust 1.75, but its current dependencies need the 2024 edition.

How do I verify a Payzum webhook signature?

Pass the raw request body, before any JSON parsing, and the request headers to the SDK verifier's payment IPN method. It reads the fixed x-nowpayments-sig header and checks the HMAC-SHA-512 signature in constant time. It also rejects events more than 10 minutes old. Answer 401 on a signature error, fulfil only when the status counts as paid, and deduplicate on the event ID.

Can I test the SDK without real payments?

Yes. Each SDK has a sandbox constructor that points at Payzum's staging host, which has isolated data and separate API keys. The SDKs also accept a custom transport, so your unit tests can script API responses without any network calls. Before launch, send a few small real payments end to end.

Do the SDKs ever hold or move my funds?

No. The SDKs only call Payzum's API and verify webhooks. Payzum is non-custodial: the buyer's payment settles to a wallet you control, and Payzum never holds a balance on your behalf. Refunds, when needed, are sent from your own wallet.

My stack isn't Node.js, PHP, Python or Rust. What can I use?

Generate a client from Payzum's public OpenAPI 3.1 contract and test your webhook check against the shared signature vectors in the same repository. Framework users also have official integrations: Omnipay for PHP, django-payments for Python, and a nopCommerce plugin for .NET stores, among 29 open-source plugins and apps.

Book 20 minutes and we'll map your SDK integration

Every backend handles payments differently: one invoice per order or per plan, a queue or an inline handler, a payout run at month end. Book 20 minutes with our engineers and we'll design how your app gets paid in crypto, non-custodial, for your specific case. We'll cover which SDK, which webhook family, what to do at each status and how to dedupe.

If the calendar does not load, book directly here · [email protected]

This article is general technical information, not legal, tax or financial advice. The SDKs are provided under the MIT licence in their repositories; test on the sandbox before using them in production. Package versions, dates and requirements were checked against npm, Packagist, PyPI, crates.io and GitHub on 6 October 2026. Survey figures are from the 2025 Stack Overflow Developer Survey and W3Techs as of 6 October 2026.