Build a Crypto Payment Reconciliation Tool for Merchants
Build a Crypto Payment Reconciliation Tool for Merchants
Most developers think about crypto payments as a checkout problem. Create an invoice. Show a payment link. Wait for a callback. Mark the order as paid. That is enough for a demo. It is not enough for a real merchant.
Once a business starts receiving meaningful crypto payment volume, the hard question is no longer only: Did the customer pay? The harder questions are: Which order does this payment belong to? Was the amount exact, underpaid, late, duplicated, manually accepted, refunded, or still waiting? Did the order system, support team, finance team, and payment provider all agree on the same state? Can the merchant prove what happened when a customer opens a support ticket?
That is the opportunity. A developer can build a crypto payment reconciliation tool for merchants. Not another crypto checkout. Not another wallet dashboard. A tool that connects payment events, invoices, orders, customers, transactions, support records, reports, and exceptions into one operational system.
In this article, I will use OxaPay as the example payment infrastructure because its documentation exposes the primitives a developer needs for this kind of product: invoice generation, payment webhooks, payment information lookup, payment history search, static addresses, SDKs, and automation integrations. The goal is not to build a toy script. The goal is to understand how a developer can turn payment reconciliation into a real merchant-facing product or service.
The business opportunity
Merchants do not usually wake up thinking they need a reconciliation tool. They discover the need when payment volume creates operational friction. At low volume, a merchant can manually check payments. At higher volume, manual checking starts to break down.
- A customer says they paid, but the order is still pending.
- An invoice expires, but the blockchain transaction arrives late.
- A customer pays less than expected.
- A support agent sees an order ID, but the payment provider stores a track ID.
- The finance team exports payment history, but the ecommerce platform has a different number of paid orders.
The developer who solves this problem is not selling "API integration." They are selling operational confidence. A reconciliation product helps merchants answer:
- Which orders are fully paid?
- Which paid orders were not fulfilled?
- Which payments are unresolved?
- Which invoices are expired but have transaction activity?
- Which payments are underpaid or manually accepted?
- Which customers need support follow-up?
- Which payment records should be exported for finance?
- Which webhook events were received, ignored, duplicated, or failed?
That is much more valuable than simply adding a payment button.
Who would pay for this?
A crypto payment reconciliation tool is not for every merchant. A small seller with five payments per month probably does not need it. The best customers are merchants where payment confusion has a real cost.
Good targets include:
- Digital product stores
- SaaS platforms
- Hosting companies
- VPN and reseller panels
- Online course platforms
- Telegram or Discord paid communities
- Software license sellers
- Cross-border service businesses
- Agencies that manage multiple merchant clients
- Marketplaces or creator platforms that receive many payments
The common pattern is simple: they receive payments, deliver something after payment, and need proof that payment state and business state match. This product becomes more valuable when the merchant has:
- Many orders
- Multiple payment methods
- Manual support tickets
- Paid but undelivered orders
- Expired invoice disputes
- Multiple currencies or networks
- Finance reporting needs
- Operational staff who are not developers
The first version does not need to be a huge SaaS platform. It can start as a private dashboard, a managed reconciliation service, or a plugin-like internal tool for one niche.
What you are building
A crypto payment reconciliation tool sits between the merchant's business system and the payment provider. It receives payment events, stores normalized payment records, compares those records with merchant orders, detects mismatches, and gives humans a clear queue of exceptions.
The architecture looks like this:
Merchant store / SaaS / bot / CRM
โ
Order created
โ
OxaPay invoice / white-label / static address
โ
Customer payment
โ
Webhook callback
โ
Payment event store
โ
Reconciliation engine
โ
Matched / unresolved / risky / needs review
โ
Dashboard + alerts + finance export + support timeline
The product has four main responsibilities:
- Ingest payment events from webhooks.
- Backfill and verify payment records using payment information and payment history APIs.
- Match payments to merchant orders using identifiers such as
order_id,track_id, amount, currency, email, address, and time window. - Surface exceptions that require action.
The most important idea is this: Reconciliation is not just storing payment status. Reconciliation is proving that payment state, order state, fulfillment state, and finance state agree.
Why OxaPay has the right primitives for this
A reconciliation tool needs more than a checkout URL. It needs persistent identifiers, payment status, callbacks, transaction metadata, history endpoints, and the ability to query records again when the webhook stream is incomplete. OxaPay provides several useful primitives for this.
Generate Invoice
The Generate Invoice endpoint creates a payment invoice and returns a payment URL. The request can include fields such as amount, currency, lifetime, callback_url, return_url, email, and order_id. That order_id is important. It lets the developer connect the payment session to the merchant's internal order record from the beginning.
Reference: OxaPay Generate Invoice
Webhook
OxaPay webhooks send JSON notifications to a merchant-defined callback_url when payment status changes. The docs explain that the merchant should wait for the final paid state rather than treating the earlier paying status as final. The webhook guide also documents HMAC SHA-512 callback validation using the merchant API key as the shared secret. It also explains the retry behavior: OxaPay expects an HTTP 200 response with ok, and retries webhook delivery up to five times with increasing delays.
Reference: OxaPay Webhook
Payment Information
The Payment Information endpoint lets a developer retrieve details for a specific payment using its track_id. This is useful for support investigation, webhook recovery, and periodic verification.
Reference: OxaPay Payment Information
Payment History
The Payment History endpoint returns account payment records and supports filtering by criteria such as track_id, type, status, currency, network, address, date window, amount range, sorting, page, and size. This is essential for reconciliation because a robust tool should not depend only on webhooks. It should also run scheduled jobs that compare the merchant's internal records with provider-side history.
Reference: OxaPay Payment History
Payment Status Table
OxaPay documents payment statuses such as new, waiting, paying, paid, manual_accept, underpaid, refunding, refunded, and expired. A reconciliation tool should map these provider statuses to internal operational states.
Reference: OxaPay Payment Status Table
Static Address
For some merchants, invoice-based payments are not the only model. OxaPay also supports static addresses linked to a unique track_id, with callback support for payments made to that address. This can matter for recurring deposits, account top-ups, wallets, or customer-specific payment addresses.
Reference: OxaPay Generate Static Address
The core product: from payment records to reconciliation cases
The tool should not show merchants a raw table of transactions and call it a day. The product should turn payment data into cases. A case is a business question that needs resolution. Examples:
- Order paid but not fulfilled
- Invoice expired but payment activity exists
- Payment underpaid
- Payment confirmed but order missing
- Webhook received but signature invalid
- Duplicate callback received
- Provider status differs from local status
- Payment refunded but order still active
- Static address payment received without a matching customer action
This is where the product becomes valuable. A raw payment dashboard is easy to ignore. A prioritized exception queue is useful.
MVP scope
Do not start by building a full finance platform. Start with a focused MVP. A strong first version could include:
- Merchant account connection
- Invoice creation wrapper
- Webhook receiver with HMAC validation
- Payment event log
- Order-to-payment matching
- Reconciliation status engine
- Exception queue
- Manual resolution notes
- CSV export
- Daily summary email or Telegram alert
The MVP should answer five questions well:
- Which orders are paid?
- Which paid orders are not fulfilled?
- Which payments need review?
- Which webhook events were missed or duplicated?
- What should support or finance do next?
That is enough to sell to an early merchant.
Data model
A reconciliation tool needs its own normalized database. Do not rely only on the ecommerce platform database. Do not rely only on the payment provider dashboard. You need a middle layer that can compare both sides. Here is a practical schema.
CREATE TABLE merchants (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
oxapay_merchant_api_key_encrypted TEXT NOT NULL,
webhook_secret_hint TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE merchant_orders (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
external_order_id TEXT NOT NULL,
customer_email TEXT,
expected_amount NUMERIC(18,8) NOT NULL,
expected_currency TEXT NOT NULL,
order_status TEXT NOT NULL,
fulfillment_status TEXT NOT NULL DEFAULT 'not_fulfilled',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (merchant_id, external_order_id)
);
CREATE TABLE payment_sessions (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
order_id UUID REFERENCES merchant_orders(id),
provider TEXT NOT NULL DEFAULT 'oxapay',
provider_track_id TEXT NOT NULL,
provider_type TEXT NOT NULL,
requested_amount NUMERIC(18,8),
requested_currency TEXT,
provider_status TEXT NOT NULL,
internal_status TEXT NOT NULL,
invoice_url TEXT,
expires_at TIMESTAMP,
raw_provider_payload JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (merchant_id, provider, provider_track_id)
);
CREATE TABLE payment_transactions (
id UUID PRIMARY KEY,
payment_session_id UUID NOT NULL REFERENCES payment_sessions(id),
tx_hash TEXT,
amount NUMERIC(18,8),
currency TEXT,
network TEXT,
address TEXT,
tx_status TEXT,
confirmations INTEGER,
raw_tx_payload JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (payment_session_id, tx_hash)
);
CREATE TABLE webhook_events (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
provider TEXT NOT NULL DEFAULT 'oxapay',
provider_track_id TEXT,
event_status TEXT,
hmac_valid BOOLEAN NOT NULL,
payload_hash TEXT NOT NULL,
raw_payload JSONB NOT NULL,
received_at TIMESTAMP NOT NULL DEFAULT NOW(),
processed_at TIMESTAMP,
processing_status TEXT NOT NULL DEFAULT 'pending',
UNIQUE (merchant_id, payload_hash)
);
CREATE TABLE reconciliation_cases (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
order_id UUID REFERENCES merchant_orders(id),
payment_session_id UUID REFERENCES payment_sessions(id),
case_type TEXT NOT NULL,
severity TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
summary TEXT NOT NULL,
recommended_action TEXT,
assigned_to TEXT,
resolved_by TEXT,
resolution_note TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMP
);
This schema is intentionally operational. It is not only for storing payments. It is for investigating mismatches.
Internal payment state machine
Provider statuses are not always the same as business statuses. A merchant does not only care whether a payment is paid. They care whether the order is safe to deliver, whether support should intervene, and whether finance should include it in reporting.
You can map OxaPay statuses into internal states.
| OxaPay status | Internal state | Business meaning |
|---|---|---|
new |
created |
Invoice exists, no payer action yet |
waiting |
awaiting_payment |
Payer selected currency, waiting for transfer |
paying |
confirming |
Payment attempt seen, not final yet |
paid |
paid_confirmed |
Payment can trigger fulfillment |
manual_accept |
manually_accepted |
Merchant accepted manually; needs audit note |
underpaid |
needs_review_underpaid |
Partial payment; support/finance review |
expired |
expired_no_payment_or_late |
Invoice expired; check for late activity |
refunding |
refund_in_progress |
Refund started |
refunded |
refunded |
Refund completed |
Do not trigger fulfillment on paying. In OxaPay's webhook documentation, paying is an earlier status and paid is the status to wait for before treating the payment as confirmed. The reconciliation tool should make that distinction very visible.
Webhook receiver with HMAC validation
Webhook ingestion is the first technical building block. This example uses Node.js and Express. The important part is that HMAC validation must use the raw request body, not a parsed and re-serialized JSON object.
import express from "express";
import crypto from "crypto";
const app = express();
// Keep raw body for HMAC verification.
app.use("/webhooks/oxapay", express.raw({ type: "application/json" }));
function verifyOxaPayHmac(rawBody, receivedHmac, merchantApiKey) {
const calculated = crypto
.createHmac("sha512", merchantApiKey)
.update(rawBody)
.digest("hex");
const a = Buffer.from(calculated, "hex");
const b = Buffer.from(receivedHmac || "", "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
Comments
No comments yet. Start the discussion.