DEV Community

NodeX
NodeX

Posted on

How to Accept USDT Payments on Your Website: A Developer's Guide

Say you're running a SaaS product and someone wants to pay $19 for a monthly plan. They already have USDT in their wallet, and they're happy to use it. You could give them a wallet address, wait for the transfer, and activate their account manually.

That sounds fine until you start getting more than a handful of payments. Now you need to figure out which transfer belongs to which customer, whether the amount is correct, and whether the same order has already been processed.

The blockchain transaction isn't usually the hard part. It's everything your application needs to do around that transaction.

Let's look at the options for accepting USDT on a website, what a basic integration looks like, and a few payment-handling details that are easy to get wrong.

Three ways to accept USDT

The simplest approach is direct wallet payments. Put a USDT address on your checkout page, ask customers to transfer funds, and verify the transactions yourself. There's very little setup involved, which makes it reasonable for occasional payments or small private transactions.

The trouble starts when several customers send similar amounts, someone chooses the wrong network, or a payment arrives after the order expires. You need a reliable way to identify the payer, monitor confirmations, handle exceptions, and update orders. A static wallet address doesn't do that for you.

The second option is building your own payment infrastructure. You can generate payment addresses, monitor supported blockchains, match transactions against orders, and maintain the necessary reconciliation logic. This gives you considerable control, but it also means maintaining RPC connections, blockchain monitoring, security controls, and recovery procedures.

The third option is using a payment gateway. Your website creates a payment order, the gateway provides the payment details, and your backend receives the payment result. You still control your products, customers, and fulfillment logic, but you don't have to implement every part of blockchain payment processing yourself.

For most small SaaS teams, I'd start by evaluating the third option before committing engineering time to a custom payment system.

Start with your own order, not the wallet address

Here's a small detail that matters more than it first appears: your application should create its own order before asking the customer to pay.

Imagine someone purchases a $19 subscription. Your database should know who placed the order, what they're buying, how much is due, and whether the order has been paid. The payment provider's transaction is associated with that order, not treated as the order itself.

A minimal database table might look like this:

CREATE TABLE payment_orders (
order_id TEXT PRIMARY KEY,
provider_invoice_id TEXT UNIQUE,
amount_usd NUMERIC(18, 2) NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT NOW()
);

It's deliberately boring. You can add customer IDs, payment asset details, expiration times, transaction identifiers, and reconciliation fields later. The important thing is that your application has a stable order reference and doesn't depend on the customer's wallet address to identify a purchase.

Also, calculate the price on your server. Don't trust an amount sent directly from the browser. If someone edits the request from $19 to $0.19, your backend should still know the actual price of the subscription.

Adding USDT checkout with JangoPay

For this example, I'll use JangoPay, a stablecoin payment platform built for online businesses. It provides Payment Links for simpler collections and APIs for applications that need to connect payments with their existing order systems.

The basic workflow is familiar if you've integrated a traditional payment gateway before: create a local order, create a payment invoice, collect payment, and update your application after confirmation.

JangoPay's developer documentation includes a Node.js SDK. Its documented methods allow a backend to create an invoice and request payment details for a supported asset and network.

Here's a simplified TypeScript example using those SDK methods:

import { JangoPayClient } from "jangopay-node-sdk";

const client = new JangoPayClient({
apiKey: process.env.JANGOPAY_API_KEY!
});

const { invoice } = await client.createInvoice({
orderId: "ORDER-1042",
amount: "19.00",
currencyCode: "USD",
allowedCurrency: ["USDT"]
});

const payment = await client.confirmPaymentCoin(
invoice.invoiceId,
"USDT",
"tron"
);

This illustrates the documented SDK calls, not a complete production checkout. Before creating the invoice, your application should load the actual order from its database and query the currently available payment assets. The selected currency and network must be supported for your merchant account.

The payment result contains the receiving information needed for that payment session, including the payment address and payable amount. Your frontend can use those details in a checkout interface, while your API credentials remain on the server.

If you don't want to build a checkout UI, JangoPay also provides hosted Payment Links. That's often a better starting point for a small business that wants to validate customer demand before writing a full integration.

Official documentation: https://doc.jangopay.org/integration-guide

Don't use the success page to activate the product

This is probably the most important part of the integration.

A customer completes payment and returns to your website. The browser displays a success message, so your application activates their subscription.

That implementation looks convenient, but it's not reliable. The customer might close the browser before returning, a payment might still be confirming, or someone might access the success URL without actually paying.

The frontend redirect is part of the user experience. It is not proof of payment.

Your backend needs a verified payment result before it changes the customer's account. Payment providers commonly handle this through Webhooks, and JangoPay supports server-to-server payment completion notifications.

One implementation detail is worth checking carefully: JangoPay's current documentation describes encrypted Webhook payloads using RSA-OAEP-SHA256 and AES-256-GCM. That is different from copying a generic HMAC signature verification example from a Stripe tutorial.

Your receiving service must decrypt the notification according to the documented envelope format. Encryption and successful decryption alone should not be treated as proof that the request came from the payment provider; the application should also establish the event's authenticity, including checking the authoritative invoice status through the authenticated API when necessary.

After verification, your application should match the invoice to an existing local order, check the expected amount and status, and only then perform fulfillment.

There is another problem to handle: duplicate notifications.

Webhooks can be delivered more than once. If your code adds $19 in credits every time it receives the same completion event, a single payment can turn into multiple balance updates.

The fix belongs in your database, not just in an application-level if statement. A successful order transition and the corresponding credit or fulfillment record should happen atomically, with a unique reference that prevents the same purchase from being processed twice.

JangoPay's current Webhook documentation does not specify a separate event ID in its completion payload. It recommends deduplicating using the event type together with the relevant invoice or transaction identifiers.

That sounds like a small implementation detail, but it's the sort of thing you want to discover before customers start paying real money.

What I'd test before going live

A successful test payment is only the beginning. I'd also check what happens when a customer underpays, selects an unsupported network, pays just before expiration, or sends funds after the payment window has closed.

Then I'd deliberately send the same completion notification multiple times and confirm that the application activates the subscription only once. I'd test what happens when the Webhook endpoint is temporarily unavailable, and whether the backend can reconcile an order by querying its status later.

Finally, I'd verify that no API keys, private keys, or sensitive payment information have ended up in frontend code, public repositories, or application logs.

These tests aren't particularly exciting, but payment bugs have a habit of becoming support tickets, accounting problems, and unhappy customers.

Do you actually need a payment gateway?

Not necessarily.

If you receive two USDT payments a week and are happy verifying them manually, adding a payment gateway might not save you much time. If you're building a product with recurring customer purchases, account top-ups, automated delivery, or international customers, the calculation changes.

That's where services like JangoPay become useful. Rather than spending your time maintaining blockchain payment infrastructure, you can focus on the application logic your customers actually care about.

USDT and USDC don't have to replace cards or other payment methods, either. They can simply provide another checkout option for customers who already prefer using stablecoins.

The main thing I'd keep in mind is this: a transaction arriving in a wallet is not the same as a payment being completed in your application. Once you design around that distinction, the rest of the integration becomes much easier to reason about.

If you're building something similar, I'm curious: are you handling crypto payments with your own wallet monitoring, or using a gateway?

Developer resources

JangoPay website: https://jangopay.org

Integration guide: https://doc.jangopay.org/integration-guide

API reference: https://doc.jangopay.org/api-reference

Note: The SDK excerpt illustrates methods from the public documentation and has not been independently tested against a live merchant account. Verify the installed SDK version and current API contracts before production use. JangoPay availability is subject to merchant eligibility and applicable regional restrictions.

Top comments (0)