<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kevin.s</title>
    <description>The latest articles on DEV Community by kevin.s (@kevins1988).</description>
    <link>https://dev.to/kevins1988</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3423109%2Fa8e23593-00bd-4b90-9b9a-420408d9ce82.png</url>
      <title>DEV Community: kevin.s</title>
      <link>https://dev.to/kevins1988</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kevins1988"/>
    <language>en</language>
    <item>
      <title>Build a Crypto Payment Support Desk</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Thu, 23 Jul 2026 06:39:13 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-crypto-payment-support-desk-354</link>
      <guid>https://dev.to/kevins1988/build-a-crypto-payment-support-desk-354</guid>
      <description>&lt;p&gt;Most developers think about crypto payments as a checkout problem.&lt;/p&gt;

&lt;p&gt;Generate an invoice.&lt;br&gt;
Show a payment page.&lt;br&gt;
Wait for a webhook.&lt;br&gt;
Mark the order as paid.&lt;/p&gt;

&lt;p&gt;That is the clean version.&lt;/p&gt;

&lt;p&gt;Real merchants do not live in the clean version.&lt;/p&gt;

&lt;p&gt;They live in support tickets.&lt;/p&gt;

&lt;p&gt;A customer says they paid, but the order is still pending.&lt;br&gt;
A payment arrives after the invoice expires.&lt;br&gt;
Someone sends the right amount on the wrong network.&lt;br&gt;
A webhook fails.&lt;br&gt;
A customer underpays.&lt;br&gt;
A support agent cannot tell whether the issue is customer error, blockchain delay, invoice expiry, fulfillment failure, or an internal system bug.&lt;/p&gt;

&lt;p&gt;This is where developers can build a real product.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Crypto Payment Support Desk&lt;/strong&gt; is a support and operations layer for merchants that accept crypto payments. It helps support teams search payments, inspect payment timelines, classify issues, explain statuses to customers, escalate real problems, and reduce the amount of manual investigation required for every crypto payment ticket.&lt;/p&gt;

&lt;p&gt;In this article, I will use &lt;strong&gt;OxaPay&lt;/strong&gt; as the example payment infrastructure because its documentation exposes the primitives needed to build this kind of product: invoice generation, payment status callbacks, HMAC-signed webhooks, payment information lookup, payment history, static addresses, SDKs, plugins, and automation integrations.&lt;/p&gt;

&lt;p&gt;This is not a generic “add crypto payments to your app” article.&lt;/p&gt;

&lt;p&gt;It is a blueprint for developers who want to build a support-facing product that merchants may actually pay for.&lt;/p&gt;


&lt;h2&gt;
  
  
  The business idea
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a support desk that sits between a merchant's payment system, order system, and support team.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The merchant already accepts crypto payments.&lt;/p&gt;

&lt;p&gt;The problem is that their support team cannot quickly answer payment-related questions.&lt;/p&gt;

&lt;p&gt;Your product gives them one place to investigate cases like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“The customer says they paid, but the order is unpaid.”&lt;/li&gt;
&lt;li&gt;“The invoice expired, but a transaction later appeared.”&lt;/li&gt;
&lt;li&gt;“The payment is underpaid.”&lt;/li&gt;
&lt;li&gt;“The webhook was received, but fulfillment did not happen.”&lt;/li&gt;
&lt;li&gt;“The support team needs a safe customer-facing status explanation.”&lt;/li&gt;
&lt;li&gt;“Finance wants to know which payment issues are still unresolved.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product is not a payment gateway.&lt;/p&gt;

&lt;p&gt;It is not a wallet.&lt;/p&gt;

&lt;p&gt;It is not an exchange.&lt;/p&gt;

&lt;p&gt;It is a &lt;strong&gt;payment support operations tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters because merchants do not pay only for APIs. They pay for reduced confusion, fewer unresolved tickets, faster support handling, cleaner internal workflows, and better visibility into payment incidents.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why this can be a real developer business
&lt;/h2&gt;

&lt;p&gt;Support tooling becomes valuable when three things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A workflow repeats often.&lt;/li&gt;
&lt;li&gt;Manual investigation is slow.&lt;/li&gt;
&lt;li&gt;Mistakes cost money, time, reputation, or customer trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Crypto payments create exactly that situation for many merchants.&lt;/p&gt;

&lt;p&gt;A card payment support agent usually has a payment processor dashboard, a dispute dashboard, a refund panel, and clear payment states.&lt;/p&gt;

&lt;p&gt;A crypto payment support agent may need to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the merchant order system,&lt;/li&gt;
&lt;li&gt;the invoice status,&lt;/li&gt;
&lt;li&gt;the payment amount,&lt;/li&gt;
&lt;li&gt;the selected coin,&lt;/li&gt;
&lt;li&gt;the selected network,&lt;/li&gt;
&lt;li&gt;the invoice expiration time,&lt;/li&gt;
&lt;li&gt;the payment status,&lt;/li&gt;
&lt;li&gt;webhook delivery,&lt;/li&gt;
&lt;li&gt;fulfillment status,&lt;/li&gt;
&lt;li&gt;internal notes,&lt;/li&gt;
&lt;li&gt;and sometimes payout or refund state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without a support desk, this becomes a Slack thread, a spreadsheet, a manual API check, or a developer interruption.&lt;/p&gt;

&lt;p&gt;That is a product opportunity.&lt;/p&gt;

&lt;p&gt;You are not selling “crypto payment integration.”&lt;/p&gt;

&lt;p&gt;You are selling:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A way for merchants to handle crypto payment support without asking developers to debug every payment issue.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Who would use this service?
&lt;/h2&gt;

&lt;p&gt;This product is most useful for merchants that already have enough payment volume to feel operational pain.&lt;/p&gt;

&lt;p&gt;Good targets include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;digital product stores,&lt;/li&gt;
&lt;li&gt;SaaS apps,&lt;/li&gt;
&lt;li&gt;hosting providers,&lt;/li&gt;
&lt;li&gt;VPN and proxy resellers,&lt;/li&gt;
&lt;li&gt;online course platforms,&lt;/li&gt;
&lt;li&gt;software license sellers,&lt;/li&gt;
&lt;li&gt;Telegram or Discord paid communities,&lt;/li&gt;
&lt;li&gt;agencies managing merchant clients,&lt;/li&gt;
&lt;li&gt;marketplaces,&lt;/li&gt;
&lt;li&gt;donation platforms,&lt;/li&gt;
&lt;li&gt;affiliate platforms,&lt;/li&gt;
&lt;li&gt;and any business with international crypto-paying customers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first buyer is usually not the CEO.&lt;/p&gt;

&lt;p&gt;It may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a founder who still handles support manually,&lt;/li&gt;
&lt;li&gt;a developer tired of payment debugging,&lt;/li&gt;
&lt;li&gt;a support manager,&lt;/li&gt;
&lt;li&gt;an operations manager,&lt;/li&gt;
&lt;li&gt;or a finance/admin person who needs cleaner payment records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more support tickets a merchant receives, the easier this product is to justify.&lt;/p&gt;


&lt;h2&gt;
  
  
  The core merchant pain
&lt;/h2&gt;

&lt;p&gt;The pain is not simply “we need to know if a payment was paid.”&lt;/p&gt;

&lt;p&gt;The real pain is context.&lt;/p&gt;

&lt;p&gt;A support agent needs to answer a customer quickly and safely.&lt;/p&gt;

&lt;p&gt;That means the agent needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which order the payment belongs to,&lt;/li&gt;
&lt;li&gt;whether the invoice is still active,&lt;/li&gt;
&lt;li&gt;whether OxaPay has detected payment activity,&lt;/li&gt;
&lt;li&gt;whether the customer paid fully,&lt;/li&gt;
&lt;li&gt;whether the payment became &lt;code&gt;paid&lt;/code&gt;, &lt;code&gt;underpaid&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt;, or another status,&lt;/li&gt;
&lt;li&gt;whether the webhook was received,&lt;/li&gt;
&lt;li&gt;whether the merchant system processed the webhook,&lt;/li&gt;
&lt;li&gt;whether fulfillment happened,&lt;/li&gt;
&lt;li&gt;whether the case needs finance or developer review,&lt;/li&gt;
&lt;li&gt;and what message can be sent back to the customer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Crypto Payment Support Desk turns this into a structured workflow.&lt;/p&gt;


&lt;h2&gt;
  
  
  What you are building
&lt;/h2&gt;

&lt;p&gt;At MVP level, you are building a web app with four main areas.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Payment search
&lt;/h3&gt;

&lt;p&gt;Support agents can search by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;order ID,&lt;/li&gt;
&lt;li&gt;invoice ID,&lt;/li&gt;
&lt;li&gt;OxaPay &lt;code&gt;track_id&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;customer email,&lt;/li&gt;
&lt;li&gt;amount,&lt;/li&gt;
&lt;li&gt;coin,&lt;/li&gt;
&lt;li&gt;network,&lt;/li&gt;
&lt;li&gt;date range,&lt;/li&gt;
&lt;li&gt;status,&lt;/li&gt;
&lt;li&gt;or internal case ID.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. Payment timeline
&lt;/h3&gt;

&lt;p&gt;Each payment has a readable event timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invoice created
Customer selected currency
Payment activity detected
Webhook received
Payment status changed
Merchant order updated
Fulfillment attempted
Fulfillment completed or failed
Support case created
Support case resolved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Issue classification
&lt;/h3&gt;

&lt;p&gt;The tool automatically classifies common cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid but not fulfilled,&lt;/li&gt;
&lt;li&gt;expired invoice,&lt;/li&gt;
&lt;li&gt;underpaid invoice,&lt;/li&gt;
&lt;li&gt;webhook not received,&lt;/li&gt;
&lt;li&gt;webhook received but not processed,&lt;/li&gt;
&lt;li&gt;duplicate callback,&lt;/li&gt;
&lt;li&gt;unknown payment reference,&lt;/li&gt;
&lt;li&gt;static address deposit needs review,&lt;/li&gt;
&lt;li&gt;fulfillment failed,&lt;/li&gt;
&lt;li&gt;possible customer mistake,&lt;/li&gt;
&lt;li&gt;manual review required.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Support response assistant
&lt;/h3&gt;

&lt;p&gt;The tool provides safe support templates.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We found your payment attempt, but the invoice has not reached the final paid status yet. We are monitoring the payment status and will update your order once it is confirmed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your invoice expired before full payment was received. Our support team is reviewing the case and will contact you with the next step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This matters because support agents should not improvise payment explanations for every crypto issue.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why OxaPay is useful for this kind of product
&lt;/h2&gt;

&lt;p&gt;A support desk needs payment visibility. OxaPay exposes several useful primitives for that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate Invoice
&lt;/h3&gt;

&lt;p&gt;OxaPay's invoice endpoint lets a merchant create a new invoice and receive a payment URL. The request can include order-related metadata such as &lt;code&gt;order_id&lt;/code&gt;, plus a &lt;code&gt;callback_url&lt;/code&gt; for payment updates.&lt;/p&gt;

&lt;p&gt;That means your app can link a merchant order to an OxaPay payment session from the beginning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment Status Table
&lt;/h3&gt;

&lt;p&gt;OxaPay documents payment statuses such as &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;paying&lt;/code&gt;, &lt;code&gt;paid&lt;/code&gt;, &lt;code&gt;manual_accept&lt;/code&gt;, &lt;code&gt;underpaid&lt;/code&gt;, &lt;code&gt;refunding&lt;/code&gt;, &lt;code&gt;refunded&lt;/code&gt;, and &lt;code&gt;expired&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A support desk can map these technical statuses into agent-friendly explanations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook
&lt;/h3&gt;

&lt;p&gt;OxaPay webhooks send payment status updates to the merchant's configured callback URL. The Python SDK documentation also notes HMAC validation using SHA-512 over the raw request body.&lt;/p&gt;

&lt;p&gt;For a support desk, webhooks become the event stream.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment Information
&lt;/h3&gt;

&lt;p&gt;OxaPay's Payment Information endpoint retrieves details for a specific payment using its &lt;code&gt;track_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is useful when a support agent opens a case and needs the latest payment state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment History
&lt;/h3&gt;

&lt;p&gt;OxaPay's Payment History endpoint returns payment records associated with the merchant API key and supports filtering, time ranges, status filters, and pagination.&lt;/p&gt;

&lt;p&gt;This is useful for backfill jobs, reporting, and detecting missed webhook events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static Address
&lt;/h3&gt;

&lt;p&gt;OxaPay can generate a static address linked to a &lt;code&gt;track_id&lt;/code&gt;, and callbacks can be configured for payments made to that address.&lt;/p&gt;

&lt;p&gt;Static addresses are useful for deposits, top-ups, recurring deposit flows, and customer-specific wallet addresses, but they create additional support needs because payments may not map to a one-time invoice in the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  SDKs and automation integrations
&lt;/h3&gt;

&lt;p&gt;OxaPay provides SDKs for PHP, Python, and Laravel. Its Make integration exposes modules such as invoice generation, payment information, payment search, static address generation, payout generation, and webhook watchers.&lt;/p&gt;

&lt;p&gt;This gives developers multiple build paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full custom SaaS,&lt;/li&gt;
&lt;li&gt;agency tool,&lt;/li&gt;
&lt;li&gt;internal support dashboard,&lt;/li&gt;
&lt;li&gt;low-code support workflow,&lt;/li&gt;
&lt;li&gt;or hybrid productized service.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The main architecture
&lt;/h2&gt;

&lt;p&gt;A practical Crypto Payment Support Desk needs to combine webhook data, order data, payment history, and support case data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer checkout
      ↓
Merchant app creates OxaPay invoice
      ↓
OxaPay payment session
      ↓
Customer pays or abandons invoice
      ↓
OxaPay webhook callback
      ↓
Webhook ingestion service
      ↓
Payment event store
      ↓
Reconciliation and classification engine
      ↓
Support desk dashboard
      ↓
Agent response, escalation, or resolution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A production system should not depend only on webhooks.&lt;/p&gt;

&lt;p&gt;It should also run scheduled sync jobs using Payment History or Payment Information, because support tools need recovery paths when callbacks are missed, delayed, rejected, or not processed by the merchant system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The key design principle: support needs timelines, not just statuses
&lt;/h2&gt;

&lt;p&gt;A payment status alone is not enough.&lt;/p&gt;

&lt;p&gt;A support agent does not only need to know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status = paid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They need to know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invoice created at 10:01
Customer selected USDT/TRC20 at 10:03
Payment detected at 10:07
Webhook received at 10:08
Merchant fulfillment failed at 10:08
Support case opened at 10:14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This timeline tells the real story.&lt;/p&gt;

&lt;p&gt;The payment may be fine.&lt;/p&gt;

&lt;p&gt;The fulfillment may be broken.&lt;/p&gt;

&lt;p&gt;Or the customer may have paid late.&lt;/p&gt;

&lt;p&gt;Or the webhook may have been rejected.&lt;/p&gt;

&lt;p&gt;A support desk should show the whole operational history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data model
&lt;/h2&gt;

&lt;p&gt;Here is a practical starting schema.&lt;/p&gt;

&lt;p&gt;You can adapt it for PostgreSQL, MySQL, SQLite, or your SaaS framework of choice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_merchant_key_encrypted&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;webhook_secret_hint&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;external_customer_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;telegram_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchant_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;external_order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;fulfillment_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'not_started'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchant_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- invoice, white_label, static_address&lt;/span&gt;
  &lt;span class="n"&gt;payment_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expected_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;expected_currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;selected_coin&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;selected_network&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;internal_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payment_session_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payment_sessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload_hash&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;support_cases&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payment_session_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payment_sessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchant_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;case_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'normal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;assigned_to&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resolution&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;resolved_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;support_case_notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;case_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;support_cases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;author_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- system, agent, developer, finance&lt;/span&gt;
  &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_delivery_logs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;verified&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema is intentionally support-oriented.&lt;/p&gt;

&lt;p&gt;It does not only store payments.&lt;/p&gt;

&lt;p&gt;It stores the evidence support agents need to reason about payment cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Internal status mapping
&lt;/h2&gt;

&lt;p&gt;Do not expose raw gateway statuses directly to non-technical support agents.&lt;/p&gt;

&lt;p&gt;Create an internal status layer.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;InternalPaymentStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;awaiting_customer_action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_detected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fully_paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partially_paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired_without_payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refund_in_progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manual_review_required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mapOxaPayStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;InternalPaymentStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;awaiting_customer_action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paying&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_detected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manual_accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fully_paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;underpaid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partially_paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired_without_payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refunding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refund_in_progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why do this?&lt;/p&gt;

&lt;p&gt;Because support agents need operational meaning, not raw API vocabulary.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;paying&lt;/code&gt; may mean “payment activity has been detected, but do not fulfill yet.”&lt;/p&gt;

&lt;p&gt;&lt;code&gt;paid&lt;/code&gt; means the merchant system can move toward fulfillment.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;underpaid&lt;/code&gt; means support should review whether the customer needs to complete payment, receive instructions, or be escalated according to the merchant's policy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Issue taxonomy
&lt;/h2&gt;

&lt;p&gt;A good support desk should create issue categories automatically.&lt;/p&gt;

&lt;p&gt;Here is a practical taxonomy.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Paid but not fulfilled
&lt;/h3&gt;

&lt;p&gt;Payment reached final paid status, but the merchant order was not delivered or activated.&lt;/p&gt;

&lt;p&gt;Likely causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fulfillment service failed,&lt;/li&gt;
&lt;li&gt;webhook was processed but downstream job failed,&lt;/li&gt;
&lt;li&gt;order mapping failed,&lt;/li&gt;
&lt;li&gt;product inventory logic failed,&lt;/li&gt;
&lt;li&gt;manual review flag blocked delivery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;confirm payment,&lt;/li&gt;
&lt;li&gt;retry fulfillment,&lt;/li&gt;
&lt;li&gt;escalate to operations if retry fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Payment detected but not final
&lt;/h3&gt;

&lt;p&gt;Payment activity exists, but the status is not final.&lt;/p&gt;

&lt;p&gt;Likely causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;still awaiting confirmation,&lt;/li&gt;
&lt;li&gt;underpayment risk,&lt;/li&gt;
&lt;li&gt;customer payment in progress,&lt;/li&gt;
&lt;li&gt;network delay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;do not manually fulfill unless policy allows,&lt;/li&gt;
&lt;li&gt;send a waiting message,&lt;/li&gt;
&lt;li&gt;monitor status.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Underpaid invoice
&lt;/h3&gt;

&lt;p&gt;The customer paid less than required.&lt;/p&gt;

&lt;p&gt;Likely causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;network fee misunderstanding,&lt;/li&gt;
&lt;li&gt;wrong amount entered,&lt;/li&gt;
&lt;li&gt;exchange withdrawal fee deducted,&lt;/li&gt;
&lt;li&gt;customer sent partial funds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explain underpayment,&lt;/li&gt;
&lt;li&gt;ask customer to complete payment if the system supports it,&lt;/li&gt;
&lt;li&gt;escalate based on merchant policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Expired invoice
&lt;/h3&gt;

&lt;p&gt;The invoice expired before payment completion.&lt;/p&gt;

&lt;p&gt;Likely causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer abandoned payment,&lt;/li&gt;
&lt;li&gt;payment sent too late,&lt;/li&gt;
&lt;li&gt;network delay,&lt;/li&gt;
&lt;li&gt;user copied payment details but paid after expiry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;check Payment Information,&lt;/li&gt;
&lt;li&gt;check Payment History if needed,&lt;/li&gt;
&lt;li&gt;issue new invoice if policy allows,&lt;/li&gt;
&lt;li&gt;escalate late-payment cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Webhook received but not processed
&lt;/h3&gt;

&lt;p&gt;The support desk has a verified webhook, but the merchant app did not update the order.&lt;/p&gt;

&lt;p&gt;Likely causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merchant app returned error,&lt;/li&gt;
&lt;li&gt;database error,&lt;/li&gt;
&lt;li&gt;fulfillment job failed,&lt;/li&gt;
&lt;li&gt;idempotency bug,&lt;/li&gt;
&lt;li&gt;queue failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry internal processing,&lt;/li&gt;
&lt;li&gt;escalate to developer,&lt;/li&gt;
&lt;li&gt;notify merchant operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. No webhook but payment exists
&lt;/h3&gt;

&lt;p&gt;Payment exists when queried, but no webhook event exists in your local event store.&lt;/p&gt;

&lt;p&gt;Likely causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;callback URL misconfigured,&lt;/li&gt;
&lt;li&gt;endpoint downtime,&lt;/li&gt;
&lt;li&gt;webhook rejected,&lt;/li&gt;
&lt;li&gt;network or TLS issue,&lt;/li&gt;
&lt;li&gt;missed delivery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create event from verified Payment Information lookup,&lt;/li&gt;
&lt;li&gt;run backfill,&lt;/li&gt;
&lt;li&gt;flag callback health issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Static address payment needs matching
&lt;/h3&gt;

&lt;p&gt;A payment arrived to a static address, but the business action is unclear.&lt;/p&gt;

&lt;p&gt;Likely causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer used a deposit address outside the intended flow,&lt;/li&gt;
&lt;li&gt;missing memo/order reference,&lt;/li&gt;
&lt;li&gt;top-up not mapped to the right account,&lt;/li&gt;
&lt;li&gt;amount does not match expected deposit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;match by track ID, customer, amount, and time,&lt;/li&gt;
&lt;li&gt;escalate unresolved deposits,&lt;/li&gt;
&lt;li&gt;avoid automatic fulfillment if confidence is low.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. Customer used wrong instructions
&lt;/h3&gt;

&lt;p&gt;The customer claims payment but the available data does not match the invoice.&lt;/p&gt;

&lt;p&gt;Likely causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wrong network,&lt;/li&gt;
&lt;li&gt;wrong coin,&lt;/li&gt;
&lt;li&gt;wrong amount,&lt;/li&gt;
&lt;li&gt;old invoice URL,&lt;/li&gt;
&lt;li&gt;payment sent outside the expected flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ask for transaction details,&lt;/li&gt;
&lt;li&gt;check merchant policy,&lt;/li&gt;
&lt;li&gt;escalate carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This taxonomy is one of the most valuable parts of the product.&lt;/p&gt;

&lt;p&gt;The more cases you classify automatically, the less support time the merchant wastes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating an invoice with support metadata
&lt;/h2&gt;

&lt;p&gt;Your support desk becomes more useful if it is involved when the payment session is created.&lt;/p&gt;

&lt;p&gt;Here is a simplified Node.js example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.oxapay.com/v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MERCHANT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/orders/:orderId/pay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Load the order from your merchant database.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Order not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment/invoice`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;externalOrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/payment`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment-status`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Payment for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;externalOrderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not create payment invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;oxapayTrackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paymentUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expectedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expectedCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;internalStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rawResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;paymentUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, adjust field names to the exact response object returned by the API version you use.&lt;/p&gt;

&lt;p&gt;The principle is what matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store the OxaPay &lt;code&gt;track_id&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;connect it to the merchant order,&lt;/li&gt;
&lt;li&gt;store the expected amount,&lt;/li&gt;
&lt;li&gt;store the customer context,&lt;/li&gt;
&lt;li&gt;and configure a webhook URL that your support desk can observe.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Webhook receiver with HMAC validation
&lt;/h2&gt;

&lt;p&gt;Webhook security is not optional.&lt;/p&gt;

&lt;p&gt;OxaPay's SDK documentation describes HMAC validation using SHA-512 over the raw request body. That means you should verify the signature before trusting the payload.&lt;/p&gt;

&lt;p&gt;In Express, use a raw body parser for the webhook route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MERCHANT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/oxapay/payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calculatedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;calculatedHmac&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookDeliveryLogs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid HMAC signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payloadHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ingestPaymentWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookDeliveryLogs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;oxapayTrackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="c1"&gt;// Return 500 only if you intentionally want the provider to retry.&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;processing error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;bBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact header casing and payload field names should be verified against the current OxaPay documentation and your integration tests.&lt;/p&gt;

&lt;p&gt;The important practices are stable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify HMAC,&lt;/li&gt;
&lt;li&gt;use the raw body,&lt;/li&gt;
&lt;li&gt;deduplicate events,&lt;/li&gt;
&lt;li&gt;record failed processing,&lt;/li&gt;
&lt;li&gt;and return the correct HTTP response based on whether you want a retry.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Webhook ingestion logic
&lt;/h2&gt;

&lt;p&gt;A support desk should treat webhook events as immutable evidence.&lt;/p&gt;

&lt;p&gt;Do not overwrite history.&lt;/p&gt;

&lt;p&gt;Append events.&lt;/p&gt;

&lt;p&gt;Then update the current payment state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ingestPaymentWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Webhook payload missing track_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByPayloadHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// idempotent duplicate delivery&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByTrackId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;oxapayTrackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oxapay.payment.webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;oxapayStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createSupportCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown_payment_reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Webhook received for unknown track_id &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapOxaPayStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;oxapayStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;internalStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;classifyPaymentCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The support desk should never assume a single webhook is the whole truth.&lt;/p&gt;

&lt;p&gt;It is an event.&lt;/p&gt;

&lt;p&gt;Your system still needs lookup, backfill, and reconciliation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Payment information lookup
&lt;/h2&gt;

&lt;p&gt;Support agents need a “refresh from provider” button.&lt;/p&gt;

&lt;p&gt;When a ticket is opened, the agent should be able to query the latest payment information by &lt;code&gt;track_id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refreshPaymentInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Payment information lookup failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentProviderSnapshots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rawResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateByTrackId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;oxapayStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;internalStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mapOxaPayStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;selectedCoin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;selectedNetwork&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the merchant missed a webhook,&lt;/li&gt;
&lt;li&gt;the agent wants the latest state,&lt;/li&gt;
&lt;li&gt;a customer claims the payment changed,&lt;/li&gt;
&lt;li&gt;or a case has been open for too long.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Payment history backfill
&lt;/h2&gt;

&lt;p&gt;The support desk should run a scheduled sync job.&lt;/p&gt;

&lt;p&gt;OxaPay's Payment History endpoint supports filters such as time range, payment status, payment type, amount, and pagination.&lt;/p&gt;

&lt;p&gt;That makes it useful for backfilling records.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;backfillRecentPayments&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Payment history backfill failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;upsertPaymentFromProviderHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not rely only on the exact query parameter names in this sample. Confirm the current API schema in the OxaPay docs and adapt the code accordingly.&lt;/p&gt;

&lt;p&gt;The pattern is the important part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch recent payment history,&lt;/li&gt;
&lt;li&gt;compare provider state with local state,&lt;/li&gt;
&lt;li&gt;create missing sessions,&lt;/li&gt;
&lt;li&gt;flag mismatches,&lt;/li&gt;
&lt;li&gt;and open support cases when needed.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Classification engine
&lt;/h2&gt;

&lt;p&gt;The classification engine is the heart of the product.&lt;/p&gt;

&lt;p&gt;It turns raw payment data into support work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classifyPaymentCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createOrUpdateCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_without_order_match&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment session has no matching merchant order.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fully_paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fulfillmentStatus&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createOrUpdateCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid_not_fulfilled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment is fully paid, but fulfillment has not completed.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partially_paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createOrUpdateCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;underpaid_invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invoice is underpaid and needs merchant policy review.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired_without_payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createOrUpdateCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired_invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invoice expired before payment was completed.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_detected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createOrUpdateCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_detected_not_final&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment activity detected but not final yet.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where your product becomes more than a dashboard.&lt;/p&gt;

&lt;p&gt;It starts doing operational triage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agent dashboard design
&lt;/h2&gt;

&lt;p&gt;A useful support desk dashboard should not look like a developer log viewer.&lt;/p&gt;

&lt;p&gt;It should answer support questions quickly.&lt;/p&gt;

&lt;p&gt;Each payment page should show:&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment summary
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;customer,&lt;/li&gt;
&lt;li&gt;order ID,&lt;/li&gt;
&lt;li&gt;amount,&lt;/li&gt;
&lt;li&gt;invoice type,&lt;/li&gt;
&lt;li&gt;OxaPay &lt;code&gt;track_id&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;current status,&lt;/li&gt;
&lt;li&gt;selected coin,&lt;/li&gt;
&lt;li&gt;selected network,&lt;/li&gt;
&lt;li&gt;created time,&lt;/li&gt;
&lt;li&gt;expiry time,&lt;/li&gt;
&lt;li&gt;final paid time if available.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Operational state
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;order status,&lt;/li&gt;
&lt;li&gt;fulfillment status,&lt;/li&gt;
&lt;li&gt;webhook status,&lt;/li&gt;
&lt;li&gt;issue category,&lt;/li&gt;
&lt;li&gt;support priority,&lt;/li&gt;
&lt;li&gt;assigned agent,&lt;/li&gt;
&lt;li&gt;escalation state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Timeline
&lt;/h3&gt;

&lt;p&gt;A chronological list of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice creation,&lt;/li&gt;
&lt;li&gt;customer actions,&lt;/li&gt;
&lt;li&gt;provider status changes,&lt;/li&gt;
&lt;li&gt;webhook deliveries,&lt;/li&gt;
&lt;li&gt;internal processing,&lt;/li&gt;
&lt;li&gt;fulfillment attempts,&lt;/li&gt;
&lt;li&gt;support notes,&lt;/li&gt;
&lt;li&gt;escalation events,&lt;/li&gt;
&lt;li&gt;resolution.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recommended action
&lt;/h3&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Wait for final paid status.”&lt;/li&gt;
&lt;li&gt;“Retry fulfillment.”&lt;/li&gt;
&lt;li&gt;“Ask customer to complete underpaid invoice.”&lt;/li&gt;
&lt;li&gt;“Create a new invoice.”&lt;/li&gt;
&lt;li&gt;“Escalate to developer: webhook processed but fulfillment failed.”&lt;/li&gt;
&lt;li&gt;“Escalate to finance: static address deposit needs manual matching.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Response templates
&lt;/h3&gt;

&lt;p&gt;Support agents should be able to copy safe customer messages.&lt;/p&gt;

&lt;p&gt;This prevents inconsistent explanations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Customer-facing payment status page
&lt;/h2&gt;

&lt;p&gt;One of the most underrated features is a customer-facing status page.&lt;/p&gt;

&lt;p&gt;Instead of asking the customer to open a ticket immediately, give them a link like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/orders/ORD-12903/payment-status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice status,&lt;/li&gt;
&lt;li&gt;whether payment is still waiting,&lt;/li&gt;
&lt;li&gt;whether payment activity has been detected,&lt;/li&gt;
&lt;li&gt;whether the invoice expired,&lt;/li&gt;
&lt;li&gt;whether support review is needed,&lt;/li&gt;
&lt;li&gt;and what the customer should do next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not expose sensitive internal logs.&lt;/p&gt;

&lt;p&gt;Do not expose API payloads.&lt;/p&gt;

&lt;p&gt;Do not expose private notes.&lt;/p&gt;

&lt;p&gt;Show only safe, customer-readable information.&lt;/p&gt;

&lt;p&gt;Example status message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We have detected payment activity for this invoice, but the payment has not reached the final paid status yet. Your order will be updated automatically once the payment is completed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This invoice expired before full payment was completed. If you believe you already paid, please contact support with your transaction details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can reduce duplicate tickets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Support response templates
&lt;/h2&gt;

&lt;p&gt;Your product can include templates by case type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment detected but not final
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thanks for your message. We can see payment activity for your invoice, but the payment has not reached the final paid status yet. We are monitoring it and your order will update once the payment is completed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Paid but not fulfilled
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your payment has been received. The order delivery step did not complete automatically, so our team is reviewing it now. We will update your order as soon as possible.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Underpaid invoice
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The payment received for this invoice is lower than the expected amount. Our team needs to review the payment before the order can be completed. We will contact you with the next step.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Expired invoice
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This invoice expired before payment was completed. Please create a new payment request from the checkout page, or contact support if you already sent funds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Unknown transaction claim
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We could not match the information provided to an active invoice yet. Please send the transaction hash, coin, network, amount, and approximate payment time so our team can review it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These templates are not just convenience features.&lt;/p&gt;

&lt;p&gt;They reduce support mistakes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Permission model
&lt;/h2&gt;

&lt;p&gt;Payment support tools should have strict permissions.&lt;/p&gt;

&lt;p&gt;Recommended roles:&lt;/p&gt;

&lt;h3&gt;
  
  
  Viewer
&lt;/h3&gt;

&lt;p&gt;Can view payment status, timeline, and public-safe information.&lt;/p&gt;

&lt;p&gt;Cannot edit cases or trigger actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Support Agent
&lt;/h3&gt;

&lt;p&gt;Can add notes, change case status, send templates, and escalate cases.&lt;/p&gt;

&lt;p&gt;Cannot edit financial data or trigger payouts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operations Manager
&lt;/h3&gt;

&lt;p&gt;Can resolve cases, retry fulfillment, approve manual review, and export reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer Admin
&lt;/h3&gt;

&lt;p&gt;Can view webhook logs, API errors, payload metadata, and integration health.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finance Admin
&lt;/h3&gt;

&lt;p&gt;Can export reports and review refund/payout-related cases.&lt;/p&gt;

&lt;p&gt;Your MVP can start with fewer roles, but do not give every support agent access to everything.&lt;/p&gt;

&lt;p&gt;Crypto payment support involves sensitive financial data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security requirements
&lt;/h2&gt;

&lt;p&gt;A support desk handles payment data, customer data, API credentials, and operational decisions.&lt;/p&gt;

&lt;p&gt;Security cannot be an afterthought.&lt;/p&gt;

&lt;p&gt;Minimum requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encrypt API keys at rest.&lt;/li&gt;
&lt;li&gt;Verify webhook HMAC signatures.&lt;/li&gt;
&lt;li&gt;Store raw webhook payloads carefully.&lt;/li&gt;
&lt;li&gt;Mask sensitive data in the UI.&lt;/li&gt;
&lt;li&gt;Use role-based access control.&lt;/li&gt;
&lt;li&gt;Keep audit logs for case actions.&lt;/li&gt;
&lt;li&gt;Do not let support agents trigger payouts unless intentionally designed.&lt;/li&gt;
&lt;li&gt;Use idempotency for processing repeated events.&lt;/li&gt;
&lt;li&gt;Rate-limit public status pages.&lt;/li&gt;
&lt;li&gt;Avoid exposing internal provider payloads to customers.&lt;/li&gt;
&lt;li&gt;Log access to payment records.&lt;/li&gt;
&lt;li&gt;Separate production and test environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also be careful with customer-submitted transaction details.&lt;/p&gt;

&lt;p&gt;A customer may paste wallet addresses, transaction hashes, screenshots, emails, or other sensitive data into a ticket.&lt;/p&gt;

&lt;p&gt;Treat support records as sensitive operational data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Integration with existing support tools
&lt;/h2&gt;

&lt;p&gt;You do not need to replace Zendesk, Intercom, Freshdesk, Help Scout, Crisp, or a merchant's existing support stack.&lt;/p&gt;

&lt;p&gt;A better MVP is to integrate with them.&lt;/p&gt;

&lt;p&gt;Your product can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create internal support cases,&lt;/li&gt;
&lt;li&gt;generate a support link,&lt;/li&gt;
&lt;li&gt;push a summary into an existing ticket,&lt;/li&gt;
&lt;li&gt;attach payment status to a ticket,&lt;/li&gt;
&lt;li&gt;add private notes,&lt;/li&gt;
&lt;li&gt;or send agent response templates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer ticket in Intercom
        ↓
Agent enters order ID
        ↓
Your tool finds payment session
        ↓
Your tool classifies issue
        ↓
Agent copies recommended response
        ↓
Case remains linked for audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is easier to sell.&lt;/p&gt;

&lt;p&gt;Merchants do not need to migrate their support team.&lt;/p&gt;

&lt;p&gt;They just add crypto payment intelligence to the workflow they already use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Low-code version
&lt;/h2&gt;

&lt;p&gt;This product can also be offered as a lower-cost service with Make or n8n.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OxaPay payment webhook
        ↓
Make or n8n scenario
        ↓
Search payment/order record
        ↓
Classify status
        ↓
Create ticket in support tool
        ↓
Notify Telegram/Slack
        ↓
Update Google Sheet or Airtable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OxaPay's Make integration includes modules for payment webhooks, invoice generation, payment information, payment search, payout actions, static addresses, and other payment operations.&lt;/p&gt;

&lt;p&gt;That means developers can sell three product tiers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Template setup&lt;/strong&gt; for very small merchants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed automation&lt;/strong&gt; for growing merchants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom support desk SaaS&lt;/strong&gt; for higher-volume merchants.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a good business model because not every merchant needs a full SaaS product on day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  MVP scope
&lt;/h2&gt;

&lt;p&gt;Do not start by building a full support platform.&lt;/p&gt;

&lt;p&gt;Start with a narrow tool.&lt;/p&gt;

&lt;p&gt;A strong MVP includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merchant connection,&lt;/li&gt;
&lt;li&gt;invoice/order mapping,&lt;/li&gt;
&lt;li&gt;webhook receiver,&lt;/li&gt;
&lt;li&gt;HMAC validation,&lt;/li&gt;
&lt;li&gt;payment event storage,&lt;/li&gt;
&lt;li&gt;payment search,&lt;/li&gt;
&lt;li&gt;payment timeline,&lt;/li&gt;
&lt;li&gt;issue classification for 5 core cases,&lt;/li&gt;
&lt;li&gt;manual case notes,&lt;/li&gt;
&lt;li&gt;customer-safe status messages,&lt;/li&gt;
&lt;li&gt;and a simple daily unresolved-cases report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first five case types should be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;paid but not fulfilled,&lt;/li&gt;
&lt;li&gt;payment detected but not final,&lt;/li&gt;
&lt;li&gt;underpaid invoice,&lt;/li&gt;
&lt;li&gt;expired invoice,&lt;/li&gt;
&lt;li&gt;webhook received but fulfillment failed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not build refund workflows, payout workflows, multi-agent routing, or AI support automation in the first version.&lt;/p&gt;

&lt;p&gt;Those can come later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production version
&lt;/h2&gt;

&lt;p&gt;A production-grade version can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-merchant support,&lt;/li&gt;
&lt;li&gt;multi-agent permissions,&lt;/li&gt;
&lt;li&gt;ticketing platform integrations,&lt;/li&gt;
&lt;li&gt;static address support,&lt;/li&gt;
&lt;li&gt;payout/refund case tracking,&lt;/li&gt;
&lt;li&gt;SLA rules,&lt;/li&gt;
&lt;li&gt;webhook health monitoring,&lt;/li&gt;
&lt;li&gt;automated backfill jobs,&lt;/li&gt;
&lt;li&gt;unresolved payment queue,&lt;/li&gt;
&lt;li&gt;customer-facing status pages,&lt;/li&gt;
&lt;li&gt;merchant analytics,&lt;/li&gt;
&lt;li&gt;finance exports,&lt;/li&gt;
&lt;li&gt;support macros,&lt;/li&gt;
&lt;li&gt;internal escalation workflows,&lt;/li&gt;
&lt;li&gt;audit logs,&lt;/li&gt;
&lt;li&gt;and anomaly detection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An advanced version can also detect patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeated underpayment from a specific checkout flow,&lt;/li&gt;
&lt;li&gt;high expired invoice rate,&lt;/li&gt;
&lt;li&gt;webhook delivery failures,&lt;/li&gt;
&lt;li&gt;fulfillment failures by product type,&lt;/li&gt;
&lt;li&gt;coin/network combinations that cause more support tickets,&lt;/li&gt;
&lt;li&gt;and merchants with rising unresolved case volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turns the tool from support desk into payment operations intelligence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revenue model
&lt;/h2&gt;

&lt;p&gt;Do not promise guaranteed income.&lt;/p&gt;

&lt;p&gt;This is a business model, not a magic income stream.&lt;/p&gt;

&lt;p&gt;Developers can monetize it in several ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Setup fee
&lt;/h3&gt;

&lt;p&gt;Charge merchants to connect OxaPay, order systems, webhooks, and support tooling.&lt;/p&gt;

&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom stores,&lt;/li&gt;
&lt;li&gt;SaaS products,&lt;/li&gt;
&lt;li&gt;agencies,&lt;/li&gt;
&lt;li&gt;high-touch merchants.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Monthly support operations subscription
&lt;/h3&gt;

&lt;p&gt;Charge a monthly fee for the dashboard, monitoring, and unresolved case queue.&lt;/p&gt;

&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merchants with recurring crypto payment volume,&lt;/li&gt;
&lt;li&gt;digital commerce,&lt;/li&gt;
&lt;li&gt;hosting,&lt;/li&gt;
&lt;li&gt;SaaS,&lt;/li&gt;
&lt;li&gt;communities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Per-agent pricing
&lt;/h3&gt;

&lt;p&gt;Charge based on support team seats.&lt;/p&gt;

&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;larger merchants,&lt;/li&gt;
&lt;li&gt;marketplaces,&lt;/li&gt;
&lt;li&gt;agencies handling multiple clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Managed support add-on
&lt;/h3&gt;

&lt;p&gt;Offer hands-on monitoring, weekly reports, and escalation support.&lt;/p&gt;

&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merchants without technical staff,&lt;/li&gt;
&lt;li&gt;non-technical founders,&lt;/li&gt;
&lt;li&gt;agencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Agency license
&lt;/h3&gt;

&lt;p&gt;Sell the tool to agencies that manage crypto payment integrations for multiple clients.&lt;/p&gt;

&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;web agencies,&lt;/li&gt;
&lt;li&gt;ecommerce agencies,&lt;/li&gt;
&lt;li&gt;payment integration consultants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example positioning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We help crypto-accepting merchants reduce payment support confusion by giving their agents a searchable payment timeline, issue classification, and customer-safe response templates.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much stronger than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We integrate a crypto payment gateway.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pricing examples
&lt;/h2&gt;

&lt;p&gt;These are not guarantees.&lt;/p&gt;

&lt;p&gt;They are positioning examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small merchant package
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$300-$800 setup
$49-$149/month monitoring and support dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;small digital stores,&lt;/li&gt;
&lt;li&gt;solo founders,&lt;/li&gt;
&lt;li&gt;course sellers,&lt;/li&gt;
&lt;li&gt;communities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Growing merchant package
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$1,000-$3,000 setup
$199-$499/month operations dashboard + ticketing integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SaaS,&lt;/li&gt;
&lt;li&gt;hosting,&lt;/li&gt;
&lt;li&gt;subscription communities,&lt;/li&gt;
&lt;li&gt;stores with regular crypto order volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Agency or multi-client package
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$2,000-$5,000 implementation
$500+/month for multi-client support, reporting, and maintenance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agencies,&lt;/li&gt;
&lt;li&gt;payment consultants,&lt;/li&gt;
&lt;li&gt;businesses managing multiple merchant accounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real price depends on support volume, integration complexity, security requirements, and how much responsibility you take.&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes this article idea strong for developers?
&lt;/h2&gt;

&lt;p&gt;Many developer business ideas are too broad.&lt;/p&gt;

&lt;p&gt;This one is narrow.&lt;/p&gt;

&lt;p&gt;That is a strength.&lt;/p&gt;

&lt;p&gt;You are not trying to build a new payment gateway.&lt;/p&gt;

&lt;p&gt;You are building a tool around a painful operational workflow.&lt;/p&gt;

&lt;p&gt;The buyer already has a reason to care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customers are confused,&lt;/li&gt;
&lt;li&gt;support is slow,&lt;/li&gt;
&lt;li&gt;developers are interrupted,&lt;/li&gt;
&lt;li&gt;manual checking wastes time,&lt;/li&gt;
&lt;li&gt;and payment issues are risky.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best developer products often come from boring internal problems.&lt;/p&gt;

&lt;p&gt;Crypto payment support is one of those problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  A 21-day build plan
&lt;/h2&gt;

&lt;p&gt;Here is a realistic first build plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Days 1-3: Merchant and order model
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merchant account model,&lt;/li&gt;
&lt;li&gt;encrypted OxaPay API key storage,&lt;/li&gt;
&lt;li&gt;order import or order API,&lt;/li&gt;
&lt;li&gt;payment session model,&lt;/li&gt;
&lt;li&gt;basic admin login.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 4-6: Invoice and webhook layer
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice creation,&lt;/li&gt;
&lt;li&gt;OxaPay &lt;code&gt;track_id&lt;/code&gt; storage,&lt;/li&gt;
&lt;li&gt;webhook receiver,&lt;/li&gt;
&lt;li&gt;HMAC validation,&lt;/li&gt;
&lt;li&gt;raw event storage,&lt;/li&gt;
&lt;li&gt;idempotency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 7-9: Timeline and search
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment search,&lt;/li&gt;
&lt;li&gt;order search,&lt;/li&gt;
&lt;li&gt;payment timeline,&lt;/li&gt;
&lt;li&gt;event list,&lt;/li&gt;
&lt;li&gt;provider status display.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 10-12: Case classification
&lt;/h3&gt;

&lt;p&gt;Build automatic cases for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid but not fulfilled,&lt;/li&gt;
&lt;li&gt;payment detected but not final,&lt;/li&gt;
&lt;li&gt;underpaid,&lt;/li&gt;
&lt;li&gt;expired,&lt;/li&gt;
&lt;li&gt;unknown payment reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 13-15: Agent workflow
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;case notes,&lt;/li&gt;
&lt;li&gt;case assignment,&lt;/li&gt;
&lt;li&gt;case resolution,&lt;/li&gt;
&lt;li&gt;priority,&lt;/li&gt;
&lt;li&gt;response templates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 16-18: Backfill and refresh
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment Information refresh,&lt;/li&gt;
&lt;li&gt;Payment History backfill,&lt;/li&gt;
&lt;li&gt;mismatch detection,&lt;/li&gt;
&lt;li&gt;unresolved case report.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 19-21: Packaging and demo
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;demo merchant account,&lt;/li&gt;
&lt;li&gt;sample payment events,&lt;/li&gt;
&lt;li&gt;landing page,&lt;/li&gt;
&lt;li&gt;pricing page,&lt;/li&gt;
&lt;li&gt;onboarding checklist,&lt;/li&gt;
&lt;li&gt;agency demo script.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end of 21 days, you should not have a full company.&lt;/p&gt;

&lt;p&gt;You should have a sellable prototype.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo script for selling it
&lt;/h2&gt;

&lt;p&gt;A good demo should show the pain quickly.&lt;/p&gt;

&lt;p&gt;Use this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Show a customer ticket: “I paid, but my order is still pending.”&lt;/li&gt;
&lt;li&gt;Search by order ID.&lt;/li&gt;
&lt;li&gt;Open the payment timeline.&lt;/li&gt;
&lt;li&gt;Show that payment reached &lt;code&gt;paid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Show that fulfillment failed.&lt;/li&gt;
&lt;li&gt;Create or open the support case.&lt;/li&gt;
&lt;li&gt;Copy a safe response template.&lt;/li&gt;
&lt;li&gt;Retry fulfillment or escalate.&lt;/li&gt;
&lt;li&gt;Show unresolved payment report.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The merchant should immediately understand:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This saves my team time and prevents payment confusion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the sale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Metrics to track
&lt;/h2&gt;

&lt;p&gt;If you sell this product, track metrics that prove operational value.&lt;/p&gt;

&lt;p&gt;Good metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;average payment ticket handling time,&lt;/li&gt;
&lt;li&gt;number of unresolved payment cases,&lt;/li&gt;
&lt;li&gt;paid-but-not-fulfilled cases,&lt;/li&gt;
&lt;li&gt;webhook failures,&lt;/li&gt;
&lt;li&gt;expired invoice rate,&lt;/li&gt;
&lt;li&gt;underpaid invoice rate,&lt;/li&gt;
&lt;li&gt;cases resolved without developer escalation,&lt;/li&gt;
&lt;li&gt;repeated issue categories,&lt;/li&gt;
&lt;li&gt;time from payment to fulfillment,&lt;/li&gt;
&lt;li&gt;support tickets per 100 payments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics help the merchant justify paying for your product.&lt;/p&gt;

&lt;p&gt;They also help you improve the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Building a generic ticketing system
&lt;/h3&gt;

&lt;p&gt;Do not compete with Zendesk or Intercom.&lt;/p&gt;

&lt;p&gt;Build payment intelligence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Showing raw JSON to support agents
&lt;/h3&gt;

&lt;p&gt;Raw payloads are useful for developers, not agents.&lt;/p&gt;

&lt;p&gt;Show plain-language status and timelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Trusting webhooks without verification
&lt;/h3&gt;

&lt;p&gt;Always validate HMAC signatures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Depending only on webhooks
&lt;/h3&gt;

&lt;p&gt;Add Payment Information refresh and Payment History backfill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Giving support agents dangerous permissions
&lt;/h3&gt;

&lt;p&gt;Do not let support agents trigger financial actions unless the merchant explicitly wants that workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 6: Ignoring static address complexity
&lt;/h3&gt;

&lt;p&gt;Static addresses are powerful, but deposits may require different matching logic than one-time invoices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 7: Promising automatic resolution for every case
&lt;/h3&gt;

&lt;p&gt;Some crypto payment issues require manual review.&lt;/p&gt;

&lt;p&gt;Your tool should reduce confusion, not pretend uncertainty does not exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Legal and compliance boundaries
&lt;/h2&gt;

&lt;p&gt;Be careful with how you position this product.&lt;/p&gt;

&lt;p&gt;You are building support tooling.&lt;/p&gt;

&lt;p&gt;You are not necessarily becoming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a payment processor,&lt;/li&gt;
&lt;li&gt;a custodian,&lt;/li&gt;
&lt;li&gt;a financial advisor,&lt;/li&gt;
&lt;li&gt;a tax advisor,&lt;/li&gt;
&lt;li&gt;or the merchant of record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your product should help merchants see and manage support cases around payments.&lt;/p&gt;

&lt;p&gt;Do not make claims about compliance, chargeback elimination, guaranteed settlement, or legal handling unless you have the proper basis.&lt;/p&gt;

&lt;p&gt;For higher-risk flows involving refunds, payouts, customer funds, or revenue splitting, get legal review before positioning the product aggressively.&lt;/p&gt;




&lt;h2&gt;
  
  
  The final product positioning
&lt;/h2&gt;

&lt;p&gt;A weak pitch:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I built a dashboard for OxaPay payments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A better pitch:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I built a support desk that helps crypto-accepting merchants investigate payment issues, classify support cases, view payment timelines, and resolve paid-but-not-fulfilled, expired, underpaid, and webhook-related problems faster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A stronger developer-business pitch:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Crypto checkout is only the first step. Once merchants process real volume, they need support operations around payment status, fulfillment, customer claims, and unresolved cases. A Crypto Payment Support Desk turns OxaPay invoices, webhooks, payment information, and payment history into a support workflow that agents can actually use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the business.&lt;/p&gt;

&lt;p&gt;Not another checkout button.&lt;/p&gt;

&lt;p&gt;A support layer for merchants who accept crypto payments and need operational clarity.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  OxaPay Documentation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-status-table" rel="noopener noreferrer"&gt;OxaPay Payment Status Table&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Generate Static Address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;OxaPay PHP SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;OxaPay Laravel SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.make.com/oxapay-crypto-pay-gtw" rel="noopener noreferrer"&gt;OxaPay Make Integration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  General Product References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.zendesk.com/" rel="noopener noreferrer"&gt;Zendesk Developer Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.intercom.com/" rel="noopener noreferrer"&gt;Intercom Developer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.freshdesk.com/api/" rel="noopener noreferrer"&gt;Freshdesk API Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>crypto</category>
      <category>api</category>
    </item>
    <item>
      <title>Build a Crypto Payment Module for SaaS Apps</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:47:01 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-crypto-payment-module-for-saas-apps-4poe</link>
      <guid>https://dev.to/kevins1988/build-a-crypto-payment-module-for-saas-apps-4poe</guid>
      <description>&lt;p&gt;Most SaaS products do not need a “crypto payment button.”&lt;/p&gt;

&lt;p&gt;They need a payment module.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;A button can redirect a customer to a payment page.&lt;/p&gt;

&lt;p&gt;A module has to know which user is paying, which workspace should be upgraded, which plan should become active, when access should expire, how failed or expired payments should be handled, how support can inspect payment status, and how finance can export records later.&lt;/p&gt;

&lt;p&gt;That is where developers can build a serious product.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Crypto Payment Module for SaaS Apps&lt;/strong&gt; is a reusable layer that lets SaaS builders add crypto payments without building the whole payment lifecycle from scratch.&lt;/p&gt;

&lt;p&gt;In this article, I will use &lt;strong&gt;OxaPay&lt;/strong&gt; as the example crypto payment infrastructure because its documentation exposes the primitives needed for this kind of module: invoice generation, white-label payments, static addresses, webhooks, payment information, payment history, SDKs for PHP, Python and Laravel, and automation integrations.&lt;/p&gt;

&lt;p&gt;This is not a “get rich with crypto APIs” article.&lt;/p&gt;

&lt;p&gt;It is a practical blueprint for developers who want to build something SaaS founders, indie hackers, agencies, and product teams may actually pay for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;A Crypto Payment Module gives SaaS apps a production-ready way to accept crypto payments and translate payment events into SaaS account states.&lt;/p&gt;

&lt;p&gt;Instead of selling this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can integrate crypto payments into your app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You sell this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can give your SaaS a reusable crypto billing module with invoices, payment status tracking, webhook verification, plan activation, grace periods, admin tools, and payment history sync.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much stronger offer.&lt;/p&gt;

&lt;p&gt;A SaaS founder does not only care that a payment happened.&lt;/p&gt;

&lt;p&gt;They care that the right account is upgraded, the right plan is applied, the right billing period is extended, the right user sees the right status, and the support team can understand what happened when something goes wrong.&lt;/p&gt;

&lt;p&gt;That is what your module should solve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is a real developer business opportunity
&lt;/h2&gt;

&lt;p&gt;Many SaaS builders are comfortable shipping product features but do not want to become payment infrastructure developers.&lt;/p&gt;

&lt;p&gt;Crypto payments create extra operational questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which invoice belongs to which user?&lt;/li&gt;
&lt;li&gt;What happens if the invoice expires?&lt;/li&gt;
&lt;li&gt;What happens if the customer pays late?&lt;/li&gt;
&lt;li&gt;What if the payment is confirming but not paid yet?&lt;/li&gt;
&lt;li&gt;Should access be granted immediately or only after final paid status?&lt;/li&gt;
&lt;li&gt;How do we avoid activating the same plan twice if the webhook is retried?&lt;/li&gt;
&lt;li&gt;How do we show payment history inside the SaaS dashboard?&lt;/li&gt;
&lt;li&gt;How does support investigate a customer who says they paid?&lt;/li&gt;
&lt;li&gt;How do we export crypto payment records for operations or finance?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A payment provider gives you the payment primitives.&lt;/p&gt;

&lt;p&gt;Your module turns those primitives into SaaS business logic.&lt;/p&gt;

&lt;p&gt;That is where the value is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who would pay for this?
&lt;/h2&gt;

&lt;p&gt;This idea is not for every business.&lt;/p&gt;

&lt;p&gt;It is strongest for SaaS products that already have global users, developer-heavy users, creator communities, digital services, or customers who prefer stablecoin or crypto payment options.&lt;/p&gt;

&lt;p&gt;Potential buyers include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;indie SaaS founders&lt;/li&gt;
&lt;li&gt;micro-SaaS builders&lt;/li&gt;
&lt;li&gt;AI tool developers&lt;/li&gt;
&lt;li&gt;hosting panels&lt;/li&gt;
&lt;li&gt;VPN and proxy panels&lt;/li&gt;
&lt;li&gt;API-as-a-service platforms&lt;/li&gt;
&lt;li&gt;software license sellers&lt;/li&gt;
&lt;li&gt;B2B SaaS tools with international customers&lt;/li&gt;
&lt;li&gt;Telegram or Discord-based SaaS communities&lt;/li&gt;
&lt;li&gt;agencies building SaaS products for clients&lt;/li&gt;
&lt;li&gt;Laravel, Node.js, Django, or Next.js SaaS boilerplate sellers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The buyer is not paying for “crypto.”&lt;/p&gt;

&lt;p&gt;They are paying for a shorter path from payment to active subscription.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you are building
&lt;/h2&gt;

&lt;p&gt;The product can be packaged in several ways.&lt;/p&gt;

&lt;p&gt;You can build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A framework module&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Example: Laravel package, Node.js module, Django app, or Next.js starter component.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A SaaS billing add-on&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A hosted service that SaaS apps connect to through API keys and webhooks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A boilerplate feature pack&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A paid template that includes crypto checkout, webhook handling, billing state, and admin screens.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A custom implementation service&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A productized service for SaaS founders who want crypto payments added to their app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open-source core plus paid support&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A free module with paid implementation, hosted dashboards, or premium integrations.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The technical foundation is similar in all versions.&lt;/p&gt;

&lt;p&gt;Your module needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a crypto invoice for a selected SaaS plan&lt;/li&gt;
&lt;li&gt;store the payment session locally&lt;/li&gt;
&lt;li&gt;attach the payment to a user, workspace, plan, and billing period&lt;/li&gt;
&lt;li&gt;receive and verify payment webhooks&lt;/li&gt;
&lt;li&gt;update subscription state only after the correct payment status&lt;/li&gt;
&lt;li&gt;handle retries and duplicate events safely&lt;/li&gt;
&lt;li&gt;allow support to inspect payment records&lt;/li&gt;
&lt;li&gt;optionally sync payment history for backfill and reconciliation&lt;/li&gt;
&lt;li&gt;expose UI components or API endpoints the SaaS app can use&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The OxaPay primitives you can use
&lt;/h2&gt;

&lt;p&gt;OxaPay is useful as an example because the documentation provides several building blocks that map well to SaaS payment modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate Invoice
&lt;/h3&gt;

&lt;p&gt;OxaPay’s Generate Invoice endpoint creates a new invoice and returns a payment URL. The request can include fields such as amount, currency, lifetime, callback URL, return URL, email, order ID, and description depending on the payment flow.&lt;/p&gt;

&lt;p&gt;For a SaaS module, this is the default primitive for one-time plan payments, renewals, upgrades, and invoice-based subscription periods.&lt;/p&gt;

&lt;p&gt;Reference:&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payment/generate-invoice&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Generate White Label
&lt;/h3&gt;

&lt;p&gt;OxaPay’s White Label endpoint returns payment details such as address, payment amount, currency, network, QR code, expiration time, and related information so the developer can build the payment interface inside their own app instead of redirecting the user to a hosted invoice page.&lt;/p&gt;

&lt;p&gt;This is useful if your SaaS module needs an embedded checkout experience.&lt;/p&gt;

&lt;p&gt;Reference:&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payment/generate-white-label&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Generate Static Address
&lt;/h3&gt;

&lt;p&gt;OxaPay’s Static Address endpoint can generate a reusable address linked to a track ID. If a callback URL is provided, the merchant server can receive notifications for payments made to that address. OxaPay notes that static addresses with no transactions for six months may be revoked.&lt;/p&gt;

&lt;p&gt;For SaaS apps, static addresses can be useful for wallet balance top-ups, account credit systems, or long-lived customer deposit flows. They should not be treated as a replacement for all subscription logic.&lt;/p&gt;

&lt;p&gt;Reference:&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payment/generate-static-address&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Webhook
&lt;/h3&gt;

&lt;p&gt;OxaPay webhooks send JSON notifications to the configured callback URL when payment status changes. The docs explain that merchants should validate the callback signature using HMAC SHA-512 over the raw POST body, with the signature sent in an &lt;code&gt;HMAC&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;This is one of the most important parts of the SaaS module.&lt;/p&gt;

&lt;p&gt;Reference:&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;https://docs.oxapay.com/webhook&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Payment Information
&lt;/h3&gt;

&lt;p&gt;The Payment Information endpoint retrieves the details of a specific payment using its &lt;code&gt;track_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For SaaS products, this is useful when support needs to inspect a payment or when your module needs to verify a payment state outside the webhook path.&lt;/p&gt;

&lt;p&gt;Reference:&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payment/payment-information&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Payment History
&lt;/h3&gt;

&lt;p&gt;The Payment History endpoint returns payment records and supports pagination. This is useful for backfill jobs, admin reports, reconciliation, and finding missed webhook events.&lt;/p&gt;

&lt;p&gt;Reference:&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payment/payment-history&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  SDKs
&lt;/h3&gt;

&lt;p&gt;OxaPay provides SDKs for PHP, Python, and Laravel. The Laravel SDK is especially relevant if your target market includes Laravel SaaS builders. The SDK documentation lists available methods such as &lt;code&gt;generateInvoice&lt;/code&gt;, &lt;code&gt;generateWhiteLabel&lt;/code&gt;, &lt;code&gt;generateStaticAddress&lt;/code&gt;, payment information, payment history, and webhook verification.&lt;/p&gt;

&lt;p&gt;References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP SDK: &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python SDK: &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Laravel SDK: &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Important limitation: this is not automatic card-style recurring billing
&lt;/h2&gt;

&lt;p&gt;This point is critical.&lt;/p&gt;

&lt;p&gt;A crypto payment module should not pretend to work like card subscriptions where the merchant can automatically charge the customer every month without customer action.&lt;/p&gt;

&lt;p&gt;In most crypto payment flows, the customer still needs to send a payment or complete a new invoice.&lt;/p&gt;

&lt;p&gt;So your module should define subscription logic like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create invoice for first billing period&lt;/li&gt;
&lt;li&gt;activate plan after confirmed paid status&lt;/li&gt;
&lt;li&gt;calculate &lt;code&gt;current_period_start&lt;/code&gt; and &lt;code&gt;current_period_end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;send renewal reminders before expiry&lt;/li&gt;
&lt;li&gt;create renewal invoice when needed&lt;/li&gt;
&lt;li&gt;apply grace period if payment is late&lt;/li&gt;
&lt;li&gt;downgrade or suspend access if renewal does not happen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not a weakness.&lt;/p&gt;

&lt;p&gt;It is simply the correct model.&lt;/p&gt;

&lt;p&gt;Your module owns the subscription state.&lt;/p&gt;

&lt;p&gt;OxaPay provides payment infrastructure.&lt;/p&gt;


&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;A production-ready SaaS crypto payment module usually has this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SaaS app
  ↓
User selects plan
  ↓
Payment module creates local checkout session
  ↓
OxaPay invoice or white-label payment is generated
  ↓
Customer pays
  ↓
OxaPay sends webhook to callback URL
  ↓
Module validates HMAC signature
  ↓
Module stores event idempotently
  ↓
Module updates payment session
  ↓
Module activates or extends SaaS subscription
  ↓
Admin dashboard and user dashboard show updated status
  ↓
Backfill job syncs Payment History for missed events
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing is that OxaPay is not your database.&lt;/p&gt;

&lt;p&gt;Your module should maintain its own internal record of users, plans, invoices, payment sessions, webhook events, subscriptions, and audit logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core module components
&lt;/h2&gt;

&lt;p&gt;A strong module should include these parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Plan configuration
&lt;/h3&gt;

&lt;p&gt;The SaaS app needs to define plans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro_monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pro Monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"billing_interval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"features"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"projects:50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"team_members:5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api_calls:100000"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is your internal product model.&lt;/p&gt;

&lt;p&gt;OxaPay should not be the source of truth for SaaS plan permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Checkout session
&lt;/h3&gt;

&lt;p&gt;When a user starts payment, create a local checkout session before calling OxaPay.&lt;/p&gt;

&lt;p&gt;The session should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user ID&lt;/li&gt;
&lt;li&gt;workspace ID&lt;/li&gt;
&lt;li&gt;plan ID&lt;/li&gt;
&lt;li&gt;amount&lt;/li&gt;
&lt;li&gt;currency&lt;/li&gt;
&lt;li&gt;billing period&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;provider name&lt;/li&gt;
&lt;li&gt;provider track ID&lt;/li&gt;
&lt;li&gt;return URL&lt;/li&gt;
&lt;li&gt;expiration timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Invoice generation
&lt;/h3&gt;

&lt;p&gt;The module creates an OxaPay invoice and stores the returned &lt;code&gt;track_id&lt;/code&gt; and payment URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Webhook receiver
&lt;/h3&gt;

&lt;p&gt;The webhook receiver validates the HMAC signature, stores the event, and updates the payment session.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Subscription state machine
&lt;/h3&gt;

&lt;p&gt;Your module updates the SaaS subscription only when the payment reaches the correct paid state.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Renewal logic
&lt;/h3&gt;

&lt;p&gt;A scheduled job checks subscriptions that are close to expiration and sends renewal reminders or generates new invoices.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Backfill and reconciliation
&lt;/h3&gt;

&lt;p&gt;Payment History can be used to backfill records and detect missed webhook events.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Admin dashboard
&lt;/h3&gt;

&lt;p&gt;SaaS admins need to inspect payment status, subscription state, webhook logs, failed payments, and customer issues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Suggested database schema
&lt;/h2&gt;

&lt;p&gt;Here is a practical starting point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;saas_plans&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;billing_interval&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;workspace_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;plan_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;saas_plans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;current_period_start&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;current_period_end&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;grace_until&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cancel_at_period_end&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;crypto_checkout_sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;workspace_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;subscription_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;plan_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'oxapay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;crypto_webhook_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'oxapay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hmac_valid&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;subscription_audit_logs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;subscription_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;previous_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;new_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema is intentionally simple.&lt;/p&gt;

&lt;p&gt;A production version may add organizations, teams, invoices, credit balances, coupons, taxes, reseller IDs, customer emails, and accounting exports.&lt;/p&gt;

&lt;p&gt;But this is enough for an MVP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Subscription states
&lt;/h2&gt;

&lt;p&gt;Your module should define its own internal subscription states.&lt;/p&gt;

&lt;p&gt;Do not expose raw provider statuses directly as SaaS subscription states.&lt;/p&gt;

&lt;p&gt;A simple state machine could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trialing
  ↓
pending_payment
  ↓
active
  ↓
past_due
  ↓
grace_period
  ↓
suspended
  ↓
canceled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Provider payment statuses are inputs.&lt;/p&gt;

&lt;p&gt;Subscription states are business decisions.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Paid&lt;/code&gt; payment status may activate or extend a subscription.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Expired&lt;/code&gt; payment status may keep the subscription unchanged if the user already has an active period.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Confirming&lt;/code&gt; may show “payment detected” but should not unlock high-risk access unless your merchant policy allows it.&lt;/li&gt;
&lt;li&gt;An invalid webhook should never change subscription state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation is what makes your module safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Payment session states
&lt;/h2&gt;

&lt;p&gt;The checkout session also needs its own states.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created
invoice_created
waiting
confirming
paid
expired
failed
review_required
canceled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A payment session is not the same as a subscription.&lt;/p&gt;

&lt;p&gt;A user may have many payment sessions for one subscription.&lt;/p&gt;

&lt;p&gt;A subscription should only change when a payment session reaches an acceptable terminal state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating an invoice from Node.js
&lt;/h2&gt;

&lt;p&gt;Here is a simplified example using Node.js and Express-style code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./db.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MERCHANT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;APP_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createCryptoCheckoutSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;planId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;saasPlans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid plan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`saas_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;plan_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;provider_order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/payment`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/billing/crypto/return?session_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; subscription for workspace &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment/invoice`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MERCHANT_API_KEY&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Could not create crypto invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expired_at&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expired_at&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact response field names can vary by API version or endpoint response shape, so your production code should inspect the actual response from the current docs and store raw provider metadata as well.&lt;/p&gt;

&lt;p&gt;The important pattern is this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create local session first&lt;/li&gt;
&lt;li&gt;generate OxaPay invoice second&lt;/li&gt;
&lt;li&gt;store provider track ID&lt;/li&gt;
&lt;li&gt;redirect user to payment URL&lt;/li&gt;
&lt;li&gt;wait for webhook before activating access&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Webhook verification with HMAC
&lt;/h2&gt;

&lt;p&gt;The webhook receiver is the most important security boundary.&lt;/p&gt;

&lt;p&gt;OxaPay docs explain that webhook callbacks should be validated with HMAC SHA-512 using the raw POST body and the relevant API key as the shared secret. The signature is sent in the &lt;code&gt;HMAC&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Here is a simplified Express example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./db.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;activateSubscriptionFromPayment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./subscription-service.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`oxapay_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoWebhookEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertIgnore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hmac_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleOxaPayPaymentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleOxaPayPaymentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByProviderTrackId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconciliationQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown_track_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providerStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lockedSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lockById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lockedSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;paid_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;activateSubscriptionFromPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lockedSession&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, be more defensive.&lt;/p&gt;

&lt;p&gt;Handle missing HMAC headers, invalid hex strings, timing-safe comparison length mismatch, malformed JSON, replay protection, duplicate status transitions, and provider response differences.&lt;/p&gt;

&lt;p&gt;The central rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never activate SaaS access from an unverified webhook.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Activating or extending the subscription
&lt;/h2&gt;

&lt;p&gt;Here is a simplified subscription activation function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;activateSubscriptionFromPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;saasPlans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription_id&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lockById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActiveOrLatestByUserAndWorkspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_id&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`sub_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;plan_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current_period_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current_period_end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;addBillingInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing_interval&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_period_end&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_period_end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_period_end&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;plan_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current_period_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current_period_end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;addBillingInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing_interval&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;grace_until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptionAuditLogs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`audit_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_activated_subscription&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;previous_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay_paid_webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;checkout_session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider_track_id&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addBillingInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yearly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFullYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weekly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unsupported billing interval: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part SaaS builders usually underestimate.&lt;/p&gt;

&lt;p&gt;The payment provider tells you that money was received.&lt;/p&gt;

&lt;p&gt;Your module decides what that means for the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  Renewal logic
&lt;/h2&gt;

&lt;p&gt;A crypto SaaS payment module needs a renewal job.&lt;/p&gt;

&lt;p&gt;A simple version runs daily.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runSubscriptionRenewalJob&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threeDaysFromNow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiringSubscriptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;current_period_end_before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;threeDaysFromNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cancel_at_period_end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;expiringSubscriptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingOpenSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cryptoCheckoutSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOpenRenewalSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingOpenSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createRenewalInvoiceForSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiredSubscriptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;current_period_end_before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;expiredSubscriptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;graceUntil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grace_period&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;grace_until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;graceUntil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;graceExpired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grace_period&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;grace_until_before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;graceExpired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suspended&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Renewal is not just a billing concern.&lt;/p&gt;

&lt;p&gt;It is a product experience concern.&lt;/p&gt;

&lt;p&gt;A good module should provide hooks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onRenewalInvoiceCreated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onPaymentDetected&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onSubscriptionExtended&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onSubscriptionPastDue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onGracePeriodStarted&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onSubscriptionSuspended&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That lets SaaS apps send emails, in-app notifications, Telegram messages, Discord messages, or CRM updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using Payment History for backfill
&lt;/h2&gt;

&lt;p&gt;Webhook systems are never the only source of truth.&lt;/p&gt;

&lt;p&gt;A production module should include a scheduled sync job using Payment History.&lt;/p&gt;

&lt;p&gt;This job can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;find missed webhook events&lt;/li&gt;
&lt;li&gt;update sessions stuck in confirming&lt;/li&gt;
&lt;li&gt;detect payments that were paid but not activated&lt;/li&gt;
&lt;li&gt;generate daily admin reports&lt;/li&gt;
&lt;li&gt;help support investigate payment issues&lt;/li&gt;
&lt;li&gt;reconcile provider records with local records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified backfill process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every 15 minutes:
  1. call Payment History with recent date/page filters
  2. for each provider payment:
     - find local checkout session by track_id or order_id
     - compare provider status with local status
     - if provider is paid and local is not active, add to reconciliation queue
     - if provider is expired and local is still pending, update local state
  3. store sync cursor
  4. alert admin if mismatch count exceeds threshold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the differences between a toy integration and a module people can trust.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hosted invoice vs white-label checkout
&lt;/h2&gt;

&lt;p&gt;Your module can support two checkout modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosted invoice mode
&lt;/h3&gt;

&lt;p&gt;The SaaS app redirects the customer to the invoice URL returned by OxaPay.&lt;/p&gt;

&lt;p&gt;This is faster to build and easier to support.&lt;/p&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MVPs&lt;/li&gt;
&lt;li&gt;indie SaaS&lt;/li&gt;
&lt;li&gt;internal tools&lt;/li&gt;
&lt;li&gt;lower-budget implementations&lt;/li&gt;
&lt;li&gt;first version of your module&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  White-label mode
&lt;/h3&gt;

&lt;p&gt;Your module renders the payment details inside the SaaS app using the response from the White Label endpoint.&lt;/p&gt;

&lt;p&gt;This gives a more native UX but requires more frontend and support logic.&lt;/p&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;premium SaaS apps&lt;/li&gt;
&lt;li&gt;branded customer portals&lt;/li&gt;
&lt;li&gt;agencies&lt;/li&gt;
&lt;li&gt;high-conversion checkout flows&lt;/li&gt;
&lt;li&gt;SaaS apps that do not want to redirect users away&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good module can start with hosted invoices and add white-label checkout later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Static address mode for SaaS wallet credits
&lt;/h2&gt;

&lt;p&gt;Some SaaS apps do not sell fixed subscription periods.&lt;/p&gt;

&lt;p&gt;They sell usage credits.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI image generation credits&lt;/li&gt;
&lt;li&gt;API call credits&lt;/li&gt;
&lt;li&gt;proxy bandwidth credits&lt;/li&gt;
&lt;li&gt;hosting balance&lt;/li&gt;
&lt;li&gt;ad campaign balance&lt;/li&gt;
&lt;li&gt;SMS or email sending credits&lt;/li&gt;
&lt;li&gt;cloud compute credits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this model, a static address can be useful.&lt;/p&gt;

&lt;p&gt;The module could create a static address per workspace or customer, then credit the customer balance when payment callbacks arrive.&lt;/p&gt;

&lt;p&gt;The flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User opens billing page
  ↓
Module shows assigned static deposit address
  ↓
User sends crypto payment
  ↓
OxaPay sends callback_url notification
  ↓
Module validates webhook
  ↓
Module credits internal wallet balance
  ↓
User consumes credits inside SaaS app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is different from subscriptions.&lt;/p&gt;

&lt;p&gt;Do not mix the two models unless the product actually needs both.&lt;/p&gt;

&lt;p&gt;Subscriptions are time-based access.&lt;/p&gt;

&lt;p&gt;Credits are usage-based balance.&lt;/p&gt;

&lt;p&gt;Your module can support both, but it should keep them separate internally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Admin dashboard requirements
&lt;/h2&gt;

&lt;p&gt;A SaaS payment module should include an admin dashboard or at least admin APIs.&lt;/p&gt;

&lt;p&gt;Useful screens include:&lt;/p&gt;

&lt;h3&gt;
  
  
  Billing overview
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;active subscriptions&lt;/li&gt;
&lt;li&gt;pending crypto invoices&lt;/li&gt;
&lt;li&gt;paid invoices&lt;/li&gt;
&lt;li&gt;expired invoices&lt;/li&gt;
&lt;li&gt;monthly crypto revenue&lt;/li&gt;
&lt;li&gt;payment method distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Customer billing profile
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;current plan&lt;/li&gt;
&lt;li&gt;subscription status&lt;/li&gt;
&lt;li&gt;current period end&lt;/li&gt;
&lt;li&gt;checkout sessions&lt;/li&gt;
&lt;li&gt;provider track IDs&lt;/li&gt;
&lt;li&gt;payment history&lt;/li&gt;
&lt;li&gt;manual actions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Webhook log
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;received events&lt;/li&gt;
&lt;li&gt;HMAC validity&lt;/li&gt;
&lt;li&gt;provider status&lt;/li&gt;
&lt;li&gt;processed/unprocessed state&lt;/li&gt;
&lt;li&gt;duplicate events&lt;/li&gt;
&lt;li&gt;errors&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reconciliation queue
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;paid but not activated&lt;/li&gt;
&lt;li&gt;unknown track ID&lt;/li&gt;
&lt;li&gt;amount mismatch&lt;/li&gt;
&lt;li&gt;expired but user claims paid&lt;/li&gt;
&lt;li&gt;webhook invalid&lt;/li&gt;
&lt;li&gt;provider status differs from local status&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Plan management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;plan name&lt;/li&gt;
&lt;li&gt;price&lt;/li&gt;
&lt;li&gt;interval&lt;/li&gt;
&lt;li&gt;feature flags&lt;/li&gt;
&lt;li&gt;active/inactive status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers often skip admin screens.&lt;/p&gt;

&lt;p&gt;That is a mistake.&lt;/p&gt;

&lt;p&gt;Admin visibility is what makes the module sellable to real SaaS operators.&lt;/p&gt;




&lt;h2&gt;
  
  
  User-facing billing UI
&lt;/h2&gt;

&lt;p&gt;The user-facing side should be simple.&lt;/p&gt;

&lt;p&gt;It needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current plan&lt;/li&gt;
&lt;li&gt;renewal date&lt;/li&gt;
&lt;li&gt;subscription status&lt;/li&gt;
&lt;li&gt;payment button&lt;/li&gt;
&lt;li&gt;active invoice status&lt;/li&gt;
&lt;li&gt;payment instructions&lt;/li&gt;
&lt;li&gt;payment history&lt;/li&gt;
&lt;li&gt;retry payment action&lt;/li&gt;
&lt;li&gt;contact support link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good user-facing status flow could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No subscription → Choose plan
Pending payment → Complete payment
Confirming → Payment detected, waiting for confirmation
Active → Plan active until date
Past due → Renewal required
Grace period → Access continues temporarily
Suspended → Pay to restore access
Canceled → Subscription ended
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crypto payment UX needs clear language.&lt;/p&gt;

&lt;p&gt;Do not make users guess whether they should wait, retry, or contact support.&lt;/p&gt;




&lt;h2&gt;
  
  
  Framework packaging ideas
&lt;/h2&gt;

&lt;p&gt;The module can be packaged differently depending on your target audience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Laravel package
&lt;/h3&gt;

&lt;p&gt;This is a strong option because OxaPay has an official Laravel SDK.&lt;/p&gt;

&lt;p&gt;Package features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;migrations&lt;/li&gt;
&lt;li&gt;config file&lt;/li&gt;
&lt;li&gt;webhook route&lt;/li&gt;
&lt;li&gt;billing middleware&lt;/li&gt;
&lt;li&gt;subscription model&lt;/li&gt;
&lt;li&gt;Blade or Inertia components&lt;/li&gt;
&lt;li&gt;cashier-like helper methods&lt;/li&gt;
&lt;li&gt;admin routes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createCryptoCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pro_monthly'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cryptoSubscription&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cryptoSubscription&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasCryptoFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api_calls:100000'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Node.js package
&lt;/h3&gt;

&lt;p&gt;Good for Express, NestJS, Fastify, Next.js API routes, or SaaS starters.&lt;/p&gt;

&lt;p&gt;Package features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice client&lt;/li&gt;
&lt;li&gt;webhook validator&lt;/li&gt;
&lt;li&gt;subscription state machine&lt;/li&gt;
&lt;li&gt;database adapter interface&lt;/li&gt;
&lt;li&gt;event hooks&lt;/li&gt;
&lt;li&gt;React billing components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cryptoBilling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro_monthly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cryptoBilling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasActiveSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// allow access&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Django app
&lt;/h3&gt;

&lt;p&gt;Good for Python SaaS builders.&lt;/p&gt;

&lt;p&gt;Package features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;models&lt;/li&gt;
&lt;li&gt;migrations&lt;/li&gt;
&lt;li&gt;webhook view&lt;/li&gt;
&lt;li&gt;admin integration&lt;/li&gt;
&lt;li&gt;Celery renewal jobs&lt;/li&gt;
&lt;li&gt;Django signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crypto_billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;plan_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pro_monthly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crypto_subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# allow access
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Next.js starter module
&lt;/h3&gt;

&lt;p&gt;Good for indie hackers and SaaS boilerplate sellers.&lt;/p&gt;

&lt;p&gt;Package features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;billing page&lt;/li&gt;
&lt;li&gt;API route for checkout creation&lt;/li&gt;
&lt;li&gt;webhook route&lt;/li&gt;
&lt;li&gt;Prisma schema&lt;/li&gt;
&lt;li&gt;React components&lt;/li&gt;
&lt;li&gt;admin page&lt;/li&gt;
&lt;li&gt;cron route for renewal reminders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This may be the easiest product to sell as a paid template.&lt;/p&gt;




&lt;h2&gt;
  
  
  MVP scope
&lt;/h2&gt;

&lt;p&gt;Do not start by building every billing feature.&lt;/p&gt;

&lt;p&gt;Start with one clear use case:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let a SaaS user pay for a monthly plan with crypto and activate access after confirmed payment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A good MVP includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plan table&lt;/li&gt;
&lt;li&gt;checkout session table&lt;/li&gt;
&lt;li&gt;subscription table&lt;/li&gt;
&lt;li&gt;OxaPay invoice creation&lt;/li&gt;
&lt;li&gt;payment webhook receiver&lt;/li&gt;
&lt;li&gt;HMAC verification&lt;/li&gt;
&lt;li&gt;idempotent event handling&lt;/li&gt;
&lt;li&gt;subscription activation&lt;/li&gt;
&lt;li&gt;user billing page&lt;/li&gt;
&lt;li&gt;admin payment log&lt;/li&gt;
&lt;li&gt;daily renewal reminder job&lt;/li&gt;
&lt;li&gt;simple support view&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not include in v1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;coupons&lt;/li&gt;
&lt;li&gt;tax engine&lt;/li&gt;
&lt;li&gt;affiliate payouts&lt;/li&gt;
&lt;li&gt;complex usage-based billing&lt;/li&gt;
&lt;li&gt;multi-currency pricing rules&lt;/li&gt;
&lt;li&gt;custom enterprise invoicing&lt;/li&gt;
&lt;li&gt;full accounting exports&lt;/li&gt;
&lt;li&gt;multi-provider abstraction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those can come later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Advanced version
&lt;/h2&gt;

&lt;p&gt;Once the MVP works, you can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;white-label checkout mode&lt;/li&gt;
&lt;li&gt;static address balance top-ups&lt;/li&gt;
&lt;li&gt;usage-based credits&lt;/li&gt;
&lt;li&gt;payment history backfill&lt;/li&gt;
&lt;li&gt;admin reconciliation queue&lt;/li&gt;
&lt;li&gt;team/workspace billing&lt;/li&gt;
&lt;li&gt;plan upgrades and downgrades&lt;/li&gt;
&lt;li&gt;prorated upgrade credits&lt;/li&gt;
&lt;li&gt;renewal reminder emails&lt;/li&gt;
&lt;li&gt;Slack/Telegram/Discord notifications&lt;/li&gt;
&lt;li&gt;API usage limit enforcement&lt;/li&gt;
&lt;li&gt;hosted customer billing portal&lt;/li&gt;
&lt;li&gt;export CSV for finance&lt;/li&gt;
&lt;li&gt;multi-tenant merchant support&lt;/li&gt;
&lt;li&gt;plugin for popular SaaS boilerplates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how the project grows from “integration” to “product.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Revenue models for developers
&lt;/h2&gt;

&lt;p&gt;There are several ways to monetize this module.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Paid boilerplate
&lt;/h3&gt;

&lt;p&gt;Sell a complete Next.js, Laravel, or Django crypto billing starter.&lt;/p&gt;

&lt;p&gt;Good for indie hackers and SaaS builders.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Open-source core + paid setup
&lt;/h3&gt;

&lt;p&gt;Release the core module publicly, then charge for implementation, customization, support, and hosted admin tools.&lt;/p&gt;

&lt;p&gt;Good if you want developer adoption.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Productized implementation service
&lt;/h3&gt;

&lt;p&gt;Offer a fixed-scope package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Crypto billing module for your SaaS:
- plan setup
- OxaPay invoice integration
- webhook verification
- subscription activation
- billing page
- admin payment log
- renewal reminder flow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good for freelance developers and small agencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Hosted billing connector
&lt;/h3&gt;

&lt;p&gt;Build a small SaaS that sits between OxaPay and client SaaS apps.&lt;/p&gt;

&lt;p&gt;Client apps call your API to create checkout sessions and receive subscription state through your SDK or webhooks.&lt;/p&gt;

&lt;p&gt;This is harder, but more scalable.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Framework-specific premium package
&lt;/h3&gt;

&lt;p&gt;Sell a polished package for one ecosystem.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laravel crypto billing package&lt;/li&gt;
&lt;li&gt;Next.js crypto SaaS billing kit&lt;/li&gt;
&lt;li&gt;Django crypto subscription app&lt;/li&gt;
&lt;li&gt;Node.js crypto billing module&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Niche specificity makes the offer easier to understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing examples
&lt;/h2&gt;

&lt;p&gt;These numbers are not guaranteed revenue.&lt;/p&gt;

&lt;p&gt;They are practical pricing structures developers can test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Freelance implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Basic setup: $500–$1,500
Includes invoice creation, webhook handling, and plan activation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Advanced SaaS billing module
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Custom build: $2,000–$8,000+
Includes admin dashboard, renewal logic, reconciliation, and white-label checkout.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Paid boilerplate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Template license: $49–$299
Higher if it includes dashboard, docs, and production deployment guide.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Monthly support
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maintenance: $100–$1,000/month
Depends on merchant volume, SLA, and support responsibility.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hosted module
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SaaS pricing: $29–$299/month per merchant/app
Depends on usage, number of payment sessions, seats, and support tier.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The best pricing depends on positioning.&lt;/p&gt;

&lt;p&gt;A generic “crypto payment script” sells cheaply.&lt;/p&gt;

&lt;p&gt;A framework-specific billing module that saves weeks of engineering can command much more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical risks
&lt;/h2&gt;

&lt;p&gt;A serious article needs to be honest about risks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook security
&lt;/h3&gt;

&lt;p&gt;Invalid or unverified webhooks must never activate access.&lt;/p&gt;

&lt;p&gt;Always validate HMAC against the raw body.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate events
&lt;/h3&gt;

&lt;p&gt;Webhook systems may retry delivery.&lt;/p&gt;

&lt;p&gt;Use idempotency keys and database locks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Status confusion
&lt;/h3&gt;

&lt;p&gt;Do not treat every payment status as successful.&lt;/p&gt;

&lt;p&gt;Define exactly which provider status activates access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscription edge cases
&lt;/h3&gt;

&lt;p&gt;Handle renewal timing, grace periods, late payments, upgrade/downgrade behavior, and canceled subscriptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Support burden
&lt;/h3&gt;

&lt;p&gt;Customers may pay late, pay on the wrong network, underpay, or close the checkout page.&lt;/p&gt;

&lt;p&gt;Your module needs support visibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compliance and accounting
&lt;/h3&gt;

&lt;p&gt;You are building software infrastructure, not legal or tax advice.&lt;/p&gt;

&lt;p&gt;Let merchants handle their own accounting, tax, jurisdiction, and compliance obligations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key management
&lt;/h3&gt;

&lt;p&gt;Store API keys in environment variables or encrypted secret storage.&lt;/p&gt;

&lt;p&gt;Do not expose merchant keys to frontend code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static address misuse
&lt;/h3&gt;

&lt;p&gt;Static addresses are useful for balance top-ups, but they require careful tracking and should not be casually mixed with invoice-based subscriptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing checklist
&lt;/h2&gt;

&lt;p&gt;Before selling this module, test at least these cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invoice creation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;valid plan creates invoice&lt;/li&gt;
&lt;li&gt;inactive plan fails&lt;/li&gt;
&lt;li&gt;missing user fails&lt;/li&gt;
&lt;li&gt;provider error is handled&lt;/li&gt;
&lt;li&gt;local session is not lost if provider call fails&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Webhook handling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;valid HMAC accepted&lt;/li&gt;
&lt;li&gt;invalid HMAC rejected&lt;/li&gt;
&lt;li&gt;duplicate webhook does not double-activate subscription&lt;/li&gt;
&lt;li&gt;malformed JSON rejected safely&lt;/li&gt;
&lt;li&gt;unknown track ID goes to reconciliation queue&lt;/li&gt;
&lt;li&gt;paid status activates subscription&lt;/li&gt;
&lt;li&gt;expired status does not activate subscription&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Subscription logic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;first payment creates active subscription&lt;/li&gt;
&lt;li&gt;renewal payment extends current period&lt;/li&gt;
&lt;li&gt;late renewal restores suspended account&lt;/li&gt;
&lt;li&gt;expired invoice does not cancel existing active period early&lt;/li&gt;
&lt;li&gt;plan upgrade changes plan correctly&lt;/li&gt;
&lt;li&gt;grace period starts after expiry&lt;/li&gt;
&lt;li&gt;suspended account blocks premium features&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Backfill
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Payment History sync finds missed paid payment&lt;/li&gt;
&lt;li&gt;mismatch creates reconciliation task&lt;/li&gt;
&lt;li&gt;already processed payment is ignored&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  UI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;user sees pending payment&lt;/li&gt;
&lt;li&gt;user sees confirming state&lt;/li&gt;
&lt;li&gt;user sees active plan&lt;/li&gt;
&lt;li&gt;user sees renewal date&lt;/li&gt;
&lt;li&gt;admin can inspect payment session&lt;/li&gt;
&lt;li&gt;support can search by track ID or order ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing these cases is part of the product.&lt;/p&gt;

&lt;p&gt;It is also part of what customers are paying you for.&lt;/p&gt;




&lt;h2&gt;
  
  
  21-day build plan
&lt;/h2&gt;

&lt;p&gt;Here is a realistic build plan for an MVP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Days 1–3: Product scope
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;choose one framework&lt;/li&gt;
&lt;li&gt;define buyer persona&lt;/li&gt;
&lt;li&gt;define plan model&lt;/li&gt;
&lt;li&gt;write integration requirements&lt;/li&gt;
&lt;li&gt;choose hosted invoice or white-label checkout for v1&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 4–6: Database and billing state
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;create plan schema&lt;/li&gt;
&lt;li&gt;create subscription schema&lt;/li&gt;
&lt;li&gt;create checkout session schema&lt;/li&gt;
&lt;li&gt;create webhook event schema&lt;/li&gt;
&lt;li&gt;define state transitions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 7–9: Invoice flow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;implement OxaPay invoice creation&lt;/li&gt;
&lt;li&gt;store track ID and payment URL&lt;/li&gt;
&lt;li&gt;build billing page&lt;/li&gt;
&lt;li&gt;redirect user to payment&lt;/li&gt;
&lt;li&gt;handle return URL&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 10–12: Webhook flow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;implement raw body parsing&lt;/li&gt;
&lt;li&gt;validate HMAC&lt;/li&gt;
&lt;li&gt;store webhook events&lt;/li&gt;
&lt;li&gt;update session status&lt;/li&gt;
&lt;li&gt;activate subscription on paid status&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 13–15: Renewal logic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;build renewal reminder job&lt;/li&gt;
&lt;li&gt;create renewal invoices&lt;/li&gt;
&lt;li&gt;handle grace period&lt;/li&gt;
&lt;li&gt;suspend expired accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 16–18: Admin and support tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;build admin payment log&lt;/li&gt;
&lt;li&gt;build subscription overview&lt;/li&gt;
&lt;li&gt;add search by user/order/track ID&lt;/li&gt;
&lt;li&gt;add reconciliation queue&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 19–21: Hardening and packaging
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;add tests&lt;/li&gt;
&lt;li&gt;write installation guide&lt;/li&gt;
&lt;li&gt;document environment variables&lt;/li&gt;
&lt;li&gt;create demo app&lt;/li&gt;
&lt;li&gt;write deployment guide&lt;/li&gt;
&lt;li&gt;define pricing package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After 21 days, you should not have a perfect billing company.&lt;/p&gt;

&lt;p&gt;You should have a focused module that solves one painful problem well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example positioning for the product
&lt;/h2&gt;

&lt;p&gt;Here is a clear offer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A crypto billing module for SaaS apps that need invoice-based crypto payments, webhook-verified plan activation, renewal reminders, grace periods, and admin payment visibility.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A more technical version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Drop-in crypto subscription infrastructure for Laravel, Node.js, Django, and Next.js SaaS apps, powered by OxaPay invoices, webhooks, payment history, and subscription-state logic.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A service version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I help SaaS founders add crypto payments without rebuilding billing logic. The setup includes payment invoices, webhook verification, subscription activation, renewal reminders, and a support-ready admin view.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is much stronger than “I can accept crypto in your SaaS.”&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes this different from a normal checkout integration?
&lt;/h2&gt;

&lt;p&gt;A normal integration ends at payment.&lt;/p&gt;

&lt;p&gt;A SaaS payment module continues after payment.&lt;/p&gt;

&lt;p&gt;It knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who paid&lt;/li&gt;
&lt;li&gt;what plan they bought&lt;/li&gt;
&lt;li&gt;which workspace should be upgraded&lt;/li&gt;
&lt;li&gt;when the billing period ends&lt;/li&gt;
&lt;li&gt;whether the payment has been verified&lt;/li&gt;
&lt;li&gt;whether the webhook has already been processed&lt;/li&gt;
&lt;li&gt;whether the subscription should be active, past due, or suspended&lt;/li&gt;
&lt;li&gt;whether support needs to investigate anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the difference between integration work and productized infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing the module API
&lt;/h2&gt;

&lt;p&gt;If this becomes a real developer product, the public API matters.&lt;/p&gt;

&lt;p&gt;A SaaS builder should not need to know every OxaPay endpoint to use your module.&lt;/p&gt;

&lt;p&gt;They should call simple billing methods.&lt;/p&gt;

&lt;p&gt;For example, a Node.js version could expose this interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CreateCheckoutInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;successUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cancelUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CryptoBillingModule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateCheckoutInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;paymentUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;providerTrackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;handleWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;getSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;currentPeriodEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;requireFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the layer customers actually buy.&lt;/p&gt;

&lt;p&gt;OxaPay remains behind the module.&lt;/p&gt;

&lt;p&gt;The SaaS app uses your clean billing interface.&lt;/p&gt;

&lt;p&gt;That makes the product easier to adopt, document, test, and sell.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature gating inside the SaaS app
&lt;/h2&gt;

&lt;p&gt;A payment module is not complete until the SaaS app can enforce access.&lt;/p&gt;

&lt;p&gt;Plan activation only matters if the product uses that state to unlock or restrict features.&lt;/p&gt;

&lt;p&gt;Here is a simple middleware-style example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireActiveSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requiredFeature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cryptoBilling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSubscription&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_id&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;402&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscription_required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please renew your plan to access this feature.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cryptoBilling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requireFeature&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requiredFeature&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;feature_not_in_plan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your current plan does not include this feature.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the module becomes useful to SaaS teams.&lt;/p&gt;

&lt;p&gt;It does not merely record payments.&lt;/p&gt;

&lt;p&gt;It controls product access.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: AI SaaS credit model
&lt;/h2&gt;

&lt;p&gt;Suppose an AI SaaS product sells both monthly plans and usage credits.&lt;/p&gt;

&lt;p&gt;The module could support two flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscription flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User buys Pro Monthly
  ↓
OxaPay invoice created
  ↓
Webhook paid event received
  ↓
Subscription active until next billing date
  ↓
Feature gates unlock Pro features
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Credit top-up flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User buys 1,000 AI credits
  ↓
OxaPay invoice or static address payment created
  ↓
Webhook paid event received
  ↓
Internal credit ledger increases by 1,000
  ↓
Each generation consumes credits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database should keep these separate.&lt;/p&gt;

&lt;p&gt;A subscription controls access.&lt;/p&gt;

&lt;p&gt;A credit ledger controls usage.&lt;/p&gt;

&lt;p&gt;This separation helps avoid confusing billing bugs later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: API SaaS plan enforcement
&lt;/h2&gt;

&lt;p&gt;For an API-as-a-service product, the module can control rate limits.&lt;/p&gt;

&lt;p&gt;Example plan definitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"starter_monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Starter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"monthly_api_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro_monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"monthly_api_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;79&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When payment is confirmed, the module updates the subscription.&lt;/p&gt;

&lt;p&gt;The API gateway checks subscription status and plan limits before serving requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming API request
  ↓
Identify workspace
  ↓
Check active subscription
  ↓
Check plan quota
  ↓
Allow or reject request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the module is not just a checkout system.&lt;/p&gt;

&lt;p&gt;It becomes part of revenue enforcement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-tenant considerations
&lt;/h2&gt;

&lt;p&gt;Many SaaS products are workspace-based.&lt;/p&gt;

&lt;p&gt;That means one user may belong to multiple workspaces, and each workspace may have a separate subscription.&lt;/p&gt;

&lt;p&gt;Your module should decide early whether billing belongs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;individual users&lt;/li&gt;
&lt;li&gt;workspaces&lt;/li&gt;
&lt;li&gt;organizations&lt;/li&gt;
&lt;li&gt;teams&lt;/li&gt;
&lt;li&gt;projects&lt;/li&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not hard-code everything to &lt;code&gt;user_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A better design is to let the implementer choose the billing owner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BillingOwner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workspace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;organization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the module usable across more SaaS products.&lt;/p&gt;

&lt;p&gt;It also reduces painful refactoring later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation experience
&lt;/h2&gt;

&lt;p&gt;If you sell this as a developer product, installation experience matters as much as the code.&lt;/p&gt;

&lt;p&gt;A good setup guide should include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Install package
2. Add environment variables
3. Run migrations
4. Define plans
5. Register webhook route
6. Configure OxaPay callback URL
7. Add billing page component
8. Add feature-gating middleware
9. Test invoice creation
10. Test webhook activation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Laravel package, that might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require yourname/crypto-saas-billing
php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;crypto-billing-config
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Node.js package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;crypto-saas-billing
npx crypto-billing init
npx crypto-billing migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a Next.js starter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app my-saas &lt;span class="nt"&gt;--example&lt;/span&gt; your-crypto-billing-template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The easier the setup, the more likely developers are to adopt it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Documentation pages your product should include
&lt;/h2&gt;

&lt;p&gt;A serious module needs documentation, not just code.&lt;/p&gt;

&lt;p&gt;Minimum docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quick start&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Plan configuration&lt;/li&gt;
&lt;li&gt;Creating checkout sessions&lt;/li&gt;
&lt;li&gt;Handling webhooks&lt;/li&gt;
&lt;li&gt;Subscription states&lt;/li&gt;
&lt;li&gt;Renewal behavior&lt;/li&gt;
&lt;li&gt;Feature gating&lt;/li&gt;
&lt;li&gt;Admin dashboard&lt;/li&gt;
&lt;li&gt;Reconciliation jobs&lt;/li&gt;
&lt;li&gt;Testing webhooks locally&lt;/li&gt;
&lt;li&gt;Deployment checklist&lt;/li&gt;
&lt;li&gt;Troubleshooting guide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The troubleshooting guide is especially important.&lt;/p&gt;

&lt;p&gt;Real users will ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why is my subscription still pending?&lt;/li&gt;
&lt;li&gt;Why did the invoice expire?&lt;/li&gt;
&lt;li&gt;Why did the webhook fail?&lt;/li&gt;
&lt;li&gt;Why did the user pay but access was not activated?&lt;/li&gt;
&lt;li&gt;How do I manually restore access?&lt;/li&gt;
&lt;li&gt;How do I find the provider track ID?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good documentation reduces support cost.&lt;/p&gt;

&lt;p&gt;It also makes the module feel like a real product.&lt;/p&gt;




&lt;h2&gt;
  
  
  When not to build this
&lt;/h2&gt;

&lt;p&gt;This is not the right product if your target users need automatic card-style recurring billing, complex tax handling, enterprise invoicing, or native fiat payment methods.&lt;/p&gt;

&lt;p&gt;It is also not ideal for SaaS apps that have no reason to accept crypto payments.&lt;/p&gt;

&lt;p&gt;The best use cases are products where crypto payment preference is real, international access matters, or the customer base already understands wallets and stablecoins.&lt;/p&gt;

&lt;p&gt;Strong niches include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI tools&lt;/li&gt;
&lt;li&gt;developer APIs&lt;/li&gt;
&lt;li&gt;hosting panels&lt;/li&gt;
&lt;li&gt;proxy and VPN panels&lt;/li&gt;
&lt;li&gt;software licenses&lt;/li&gt;
&lt;li&gt;creator tools&lt;/li&gt;
&lt;li&gt;Telegram/Discord SaaS&lt;/li&gt;
&lt;li&gt;digital service platforms&lt;/li&gt;
&lt;li&gt;global B2B micro-SaaS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A narrow module for a real niche is better than a generic module for everyone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Developer takeaway
&lt;/h2&gt;

&lt;p&gt;A crypto payment module for SaaS apps is not just a wrapper around a payment API.&lt;/p&gt;

&lt;p&gt;It is a billing-state system.&lt;/p&gt;

&lt;p&gt;OxaPay can provide the payment primitives: invoices, white-label payment details, static addresses, webhooks, payment information, payment history, and SDKs.&lt;/p&gt;

&lt;p&gt;Your module provides the SaaS logic: plans, checkout sessions, subscription states, renewals, grace periods, admin screens, support workflows, and reconciliation.&lt;/p&gt;

&lt;p&gt;That combination is valuable because SaaS builders do not want to rebuild payment operations from scratch.&lt;/p&gt;

&lt;p&gt;If you are a developer looking for a real product opportunity around crypto payments, this is one of the strongest ones:&lt;/p&gt;

&lt;p&gt;Build the module that turns crypto payments into working SaaS subscriptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;OxaPay Generate White Label API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Generate Static Address API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;OxaPay PHP SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;OxaPay Laravel SDK&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>api</category>
      <category>crypto</category>
    </item>
    <item>
      <title>Build a Merchant Crypto Launch Kit for Agencies</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Mon, 20 Jul 2026 07:05:34 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-merchant-crypto-launch-kit-for-agencies-4o50</link>
      <guid>https://dev.to/kevins1988/build-a-merchant-crypto-launch-kit-for-agencies-4o50</guid>
      <description>&lt;p&gt;Most developers think about crypto payments as an integration problem.&lt;/p&gt;

&lt;p&gt;A merchant needs to accept crypto.&lt;br&gt;
You connect an API.&lt;br&gt;
You test a checkout.&lt;br&gt;
You ship it.&lt;/p&gt;

&lt;p&gt;That works for one client.&lt;/p&gt;

&lt;p&gt;It does not automatically create a repeatable business.&lt;/p&gt;

&lt;p&gt;A better opportunity is to build a &lt;strong&gt;Merchant Crypto Launch Kit&lt;/strong&gt; for agencies.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instead of selling one-off crypto payment integrations directly to merchants, build a repeatable implementation kit that agencies can resell, deploy, and support for their own clients.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Agencies already have access to merchants. They build websites, ecommerce stores, SaaS products, membership platforms, hosting portals, digital product funnels, and creator monetization systems. Many of them do not want to become crypto payment infrastructure experts. They need a safe, repeatable, documented way to deliver crypto payment functionality without rebuilding the same operational layer for every client.&lt;/p&gt;

&lt;p&gt;That is where a developer can create a valuable product.&lt;/p&gt;

&lt;p&gt;This article breaks down how to build that product.&lt;/p&gt;

&lt;p&gt;I will use &lt;a href="https://oxapay.com/" rel="noopener noreferrer"&gt;OxaPay&lt;/a&gt; as the example infrastructure because its documentation exposes the payment primitives a developer needs: hosted invoices, white-label payments, static addresses, webhooks, payment history, payment information, payout APIs, SDKs, plugins, and automation integrations.&lt;/p&gt;

&lt;p&gt;This is not a “get rich with APIs” article.&lt;/p&gt;

&lt;p&gt;It is a practical blueprint for building a merchant-facing crypto payment launch system that agencies can actually sell.&lt;/p&gt;


&lt;h2&gt;
  
  
  The business idea
&lt;/h2&gt;

&lt;p&gt;A Merchant Crypto Launch Kit is a packaged system that helps agencies add crypto payment flows to client projects.&lt;/p&gt;

&lt;p&gt;It can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checkout setup&lt;/li&gt;
&lt;li&gt;plugin installation&lt;/li&gt;
&lt;li&gt;hosted invoice flows&lt;/li&gt;
&lt;li&gt;branded payment pages&lt;/li&gt;
&lt;li&gt;webhook handling&lt;/li&gt;
&lt;li&gt;order activation logic&lt;/li&gt;
&lt;li&gt;payment status tracking&lt;/li&gt;
&lt;li&gt;reporting exports&lt;/li&gt;
&lt;li&gt;support playbooks&lt;/li&gt;
&lt;li&gt;merchant onboarding documents&lt;/li&gt;
&lt;li&gt;refund and issue escalation SOPs&lt;/li&gt;
&lt;li&gt;deployment checklists&lt;/li&gt;
&lt;li&gt;reusable code templates&lt;/li&gt;
&lt;li&gt;agency-facing documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agency does not buy “some API code.”&lt;/p&gt;

&lt;p&gt;The agency buys a repeatable delivery system.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;One-off code is hard to resell.&lt;br&gt;
A launch kit can be sold repeatedly.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why agencies are a good customer
&lt;/h2&gt;

&lt;p&gt;Selling directly to merchants is possible, but it is often slow. A small merchant may not understand the difference between a payment link, a plugin, a webhook, a callback, an invoice status, and a reconciliation report.&lt;/p&gt;

&lt;p&gt;Agencies already sit between technical infrastructure and merchant needs.&lt;/p&gt;

&lt;p&gt;They usually understand the client’s store, CMS, checkout, product catalog, subscription flow, support process, and backend stack.&lt;/p&gt;

&lt;p&gt;But they may not want to own the crypto payment layer.&lt;/p&gt;

&lt;p&gt;That creates a clear business opportunity for developers.&lt;/p&gt;

&lt;p&gt;An agency may pay for a launch kit because it helps them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add a new service to their client offering&lt;/li&gt;
&lt;li&gt;avoid learning crypto payment infrastructure from scratch&lt;/li&gt;
&lt;li&gt;deliver projects faster&lt;/li&gt;
&lt;li&gt;reduce implementation mistakes&lt;/li&gt;
&lt;li&gt;standardize client onboarding&lt;/li&gt;
&lt;li&gt;avoid fragile one-off integrations&lt;/li&gt;
&lt;li&gt;support multiple clients with one repeatable workflow&lt;/li&gt;
&lt;li&gt;sell crypto payment setup as a premium add-on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The developer’s product is not just code.&lt;br&gt;
It is operational leverage.&lt;/p&gt;


&lt;h2&gt;
  
  
  What problem are you solving?
&lt;/h2&gt;

&lt;p&gt;A merchant asking for crypto payments is rarely asking only for a checkout button.&lt;/p&gt;

&lt;p&gt;Behind that request are many operational questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which payment method should the client use?&lt;/li&gt;
&lt;li&gt;Should the agency use a plugin, hosted invoice, white-label checkout, or static address?&lt;/li&gt;
&lt;li&gt;What happens after the customer pays?&lt;/li&gt;
&lt;li&gt;How does the store know the order is paid?&lt;/li&gt;
&lt;li&gt;What should happen if the invoice expires?&lt;/li&gt;
&lt;li&gt;How should support handle a customer who says they paid late?&lt;/li&gt;
&lt;li&gt;Where should payment records be stored?&lt;/li&gt;
&lt;li&gt;Who receives payment alerts?&lt;/li&gt;
&lt;li&gt;How does the merchant export payment data?&lt;/li&gt;
&lt;li&gt;How does the agency test the integration before handoff?&lt;/li&gt;
&lt;li&gt;What should the agency document for the merchant’s staff?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A launch kit packages answers to these questions.&lt;/p&gt;

&lt;p&gt;That is the value.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why OxaPay works as example infrastructure
&lt;/h2&gt;

&lt;p&gt;For this kind of developer product, you need payment primitives that can support both simple and advanced deployments.&lt;/p&gt;

&lt;p&gt;OxaPay has several useful primitives for this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OxaPay primitive&lt;/th&gt;
&lt;th&gt;Why it matters for an agency launch kit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generate Invoice&lt;/td&gt;
&lt;td&gt;Create a hosted payment URL for a transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate White Label&lt;/td&gt;
&lt;td&gt;Build a branded payment interface in the agency/client UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate Static Address&lt;/td&gt;
&lt;td&gt;Assign a reusable crypto payment address for a customer or account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook&lt;/td&gt;
&lt;td&gt;Receive payment status updates via callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment Information&lt;/td&gt;
&lt;td&gt;Query a specific payment by &lt;code&gt;track_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment History&lt;/td&gt;
&lt;td&gt;Backfill or audit payment records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugins&lt;/td&gt;
&lt;td&gt;Add crypto payment support to ecommerce platforms faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDKs&lt;/td&gt;
&lt;td&gt;Build reusable integration code in common languages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payout API&lt;/td&gt;
&lt;td&gt;Add optional payout workflows for advanced clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make / n8n integrations&lt;/td&gt;
&lt;td&gt;Support low-code agency automation workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;Generate Invoice endpoint&lt;/a&gt; creates an invoice and returns a payment URL. That is useful for a fast agency implementation where the client does not need a custom payment UI.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;Generate White Label endpoint&lt;/a&gt; returns payment details such as address, currency, amount, and expiration information, which makes it suitable when the agency wants a branded checkout experience inside the client’s interface.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;Generate Static Address endpoint&lt;/a&gt; creates a reusable address linked to a &lt;code&gt;track_id&lt;/code&gt;; if a &lt;code&gt;callback_url&lt;/code&gt; is provided, OxaPay can notify the merchant server about payments to that address.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;Webhook documentation&lt;/a&gt; explains that merchants can set a &lt;code&gt;callback_url&lt;/code&gt;, receive payment status updates, and should return HTTP 200 with &lt;code&gt;ok&lt;/code&gt;. It also describes retry behavior for failed webhook delivery.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;Payment History endpoint&lt;/a&gt; allows retrieving account payments with filters such as status, type, amount, date range, network, and pagination. This is important for backfills, reports, and support workflows.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/plugins" rel="noopener noreferrer"&gt;plugins documentation&lt;/a&gt; shows that OxaPay supports ecommerce plugin workflows, which can be useful for agency clients using WooCommerce, WHMCS, WISECP, Clientexec, PrestaShop, and similar platforms.&lt;/p&gt;

&lt;p&gt;That gives you enough infrastructure to build a reusable agency product rather than a one-off integration.&lt;/p&gt;


&lt;h2&gt;
  
  
  The core positioning
&lt;/h2&gt;

&lt;p&gt;A weak offer sounds like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can integrate OxaPay into your client’s website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A stronger offer sounds like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I help agencies launch crypto payment acceptance for merchant clients with a repeatable kit that includes checkout setup, webhook handling, payment status tracking, merchant documentation, support workflows, and launch QA.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is much easier to sell to an agency.&lt;/p&gt;

&lt;p&gt;The agency is not only buying integration work.&lt;br&gt;
It is buying a service line it can resell.&lt;/p&gt;


&lt;h2&gt;
  
  
  Who can use this launch kit?
&lt;/h2&gt;

&lt;p&gt;Your launch kit can serve different kinds of agencies.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ecommerce agencies
&lt;/h3&gt;

&lt;p&gt;These agencies build stores on WooCommerce, PrestaShop, Magento-like stacks, custom Laravel carts, or headless commerce frameworks.&lt;/p&gt;

&lt;p&gt;They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checkout integration&lt;/li&gt;
&lt;li&gt;order status sync&lt;/li&gt;
&lt;li&gt;payment confirmation handling&lt;/li&gt;
&lt;li&gt;order notes&lt;/li&gt;
&lt;li&gt;merchant instructions&lt;/li&gt;
&lt;li&gt;refund/support SOPs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Hosting and SaaS agencies
&lt;/h3&gt;

&lt;p&gt;These agencies build or manage hosting portals, SaaS billing flows, license systems, dashboards, and client portals.&lt;/p&gt;

&lt;p&gt;They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice creation&lt;/li&gt;
&lt;li&gt;account activation after payment&lt;/li&gt;
&lt;li&gt;payment status pages&lt;/li&gt;
&lt;li&gt;renewal reminders&lt;/li&gt;
&lt;li&gt;static address options&lt;/li&gt;
&lt;li&gt;payment history exports&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Creator and community agencies
&lt;/h3&gt;

&lt;p&gt;These agencies build paid communities, Telegram funnels, Discord memberships, course portals, and creator monetization systems.&lt;/p&gt;

&lt;p&gt;They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid access flows&lt;/li&gt;
&lt;li&gt;payment-based unlock logic&lt;/li&gt;
&lt;li&gt;reminders&lt;/li&gt;
&lt;li&gt;access revocation&lt;/li&gt;
&lt;li&gt;support tools&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Web3-adjacent studios
&lt;/h3&gt;

&lt;p&gt;These agencies already work with crypto-related brands but may not want to build payment operations for every client.&lt;/p&gt;

&lt;p&gt;They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reusable checkout components&lt;/li&gt;
&lt;li&gt;branded UI&lt;/li&gt;
&lt;li&gt;webhook modules&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;client handoff documents&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Small development agencies
&lt;/h3&gt;

&lt;p&gt;These teams can use the kit to create a new paid service category.&lt;/p&gt;

&lt;p&gt;They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast deployment&lt;/li&gt;
&lt;li&gt;reusable code&lt;/li&gt;
&lt;li&gt;installation guides&lt;/li&gt;
&lt;li&gt;predictable scope&lt;/li&gt;
&lt;li&gt;clear client deliverables&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What the launch kit should include
&lt;/h2&gt;

&lt;p&gt;A serious Merchant Crypto Launch Kit should not be just a GitHub repository.&lt;/p&gt;

&lt;p&gt;It should be a complete deployment system.&lt;/p&gt;

&lt;p&gt;At minimum, I would include seven layers.&lt;/p&gt;


&lt;h2&gt;
  
  
  Layer 1: Client discovery checklist
&lt;/h2&gt;

&lt;p&gt;Before writing code, the agency needs to know which payment pattern fits the merchant.&lt;/p&gt;

&lt;p&gt;Your kit should include a discovery questionnaire.&lt;/p&gt;

&lt;p&gt;Example questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business model:
- What does the client sell?
- One-time products, subscriptions, services, credits, donations, or invoices?
- Does the customer need instant access after payment?
- Is the payment amount fixed, variable, or user-defined?

Platform:
- WooCommerce, WHMCS, PrestaShop, custom app, SaaS, Telegram, Discord, or other?
- Does the platform already have an OxaPay plugin?
- Does the client need a hosted payment page or branded checkout?

Payment operations:
- Who handles failed or expired payments?
- Who receives payment notifications?
- Does the client need daily exports?
- Does the client need support search by order ID or invoice ID?

Technical:
- Can the client provide an HTTPS webhook endpoint?
- Where should API keys be stored?
- What system should be updated after successful payment?

Handoff:
- Who on the client team will use the dashboard?
- What support scripts do they need?
- What testing evidence does the agency need before launch?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checklist makes the agency look professional and reduces bad implementation choices.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2: Integration decision tree
&lt;/h2&gt;

&lt;p&gt;The agency needs to know which OxaPay method to use.&lt;/p&gt;

&lt;p&gt;A simple decision tree can be part of your launch kit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If the merchant uses a supported ecommerce platform:
    Start with the official plugin.

If the merchant needs a fast hosted payment page:
    Use Generate Invoice.

If the merchant needs a branded checkout inside their own UI:
    Use Generate White Label.

If the merchant needs reusable customer/account deposit addresses:
    Use Static Address.

If the merchant needs payment-driven automation:
    Use Webhooks + internal workflow handlers.

If the merchant needs reporting or backfill:
    Use Payment History and Payment Information.

If the merchant pays contractors, affiliates, or partners:
    Consider Payout API as an advanced module.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one page can save agencies hours of uncertainty.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3: Checkout templates
&lt;/h2&gt;

&lt;p&gt;The launch kit should include ready-to-use checkout templates.&lt;/p&gt;

&lt;p&gt;You can create three versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Hosted invoice checkout
&lt;/h3&gt;

&lt;p&gt;This is the simplest version.&lt;/p&gt;

&lt;p&gt;The merchant app creates an invoice and redirects the customer to the hosted payment URL.&lt;/p&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simple stores&lt;/li&gt;
&lt;li&gt;agencies that need fast delivery&lt;/li&gt;
&lt;li&gt;service invoices&lt;/li&gt;
&lt;li&gt;MVPs&lt;/li&gt;
&lt;li&gt;clients that do not need branded payment UI&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Branded white-label checkout
&lt;/h3&gt;

&lt;p&gt;This version keeps the payment experience inside the merchant’s interface.&lt;/p&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;premium clients&lt;/li&gt;
&lt;li&gt;SaaS products&lt;/li&gt;
&lt;li&gt;dashboards&lt;/li&gt;
&lt;li&gt;custom ecommerce&lt;/li&gt;
&lt;li&gt;clients with strong brand requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Static address account funding
&lt;/h3&gt;

&lt;p&gt;This version assigns a reusable payment address to a customer or account.&lt;/p&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;account top-ups&lt;/li&gt;
&lt;li&gt;wallet-style balances&lt;/li&gt;
&lt;li&gt;repeated deposits&lt;/li&gt;
&lt;li&gt;user accounts&lt;/li&gt;
&lt;li&gt;internal credit systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your launch kit should explain when to use each pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 4: Webhook receiver module
&lt;/h2&gt;

&lt;p&gt;Every serious payment launch kit needs a webhook receiver.&lt;/p&gt;

&lt;p&gt;Payment systems are event-driven. A payment can move from created to paying to paid. A customer may pay late. A callback may be retried. A webhook may arrive twice. A server may be temporarily down.&lt;/p&gt;

&lt;p&gt;Your kit should not trust only frontend redirects.&lt;/p&gt;

&lt;p&gt;It should include a backend module that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;receives webhooks over HTTPS&lt;/li&gt;
&lt;li&gt;verifies the HMAC signature&lt;/li&gt;
&lt;li&gt;stores the raw payload&lt;/li&gt;
&lt;li&gt;handles duplicate callbacks&lt;/li&gt;
&lt;li&gt;updates internal payment state&lt;/li&gt;
&lt;li&gt;triggers business actions only once&lt;/li&gt;
&lt;li&gt;returns &lt;code&gt;200 ok&lt;/code&gt; when processed&lt;/li&gt;
&lt;li&gt;pushes failed events into a retry queue&lt;/li&gt;
&lt;li&gt;logs event history for support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OxaPay’s webhook docs explain that the merchant sets a &lt;code&gt;callback_url&lt;/code&gt;, receives JSON payment updates, and should return HTTP 200 with &lt;code&gt;ok&lt;/code&gt;. The docs also describe retry attempts when delivery fails.&lt;/p&gt;

&lt;p&gt;That means your kit should be built around idempotent webhook handling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 5: Merchant dashboard starter
&lt;/h2&gt;

&lt;p&gt;Agencies often ship checkout and forget operations.&lt;/p&gt;

&lt;p&gt;That is a mistake.&lt;/p&gt;

&lt;p&gt;Even a simple merchant dashboard can make the launch kit more valuable.&lt;/p&gt;

&lt;p&gt;A starter dashboard can show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recent invoices&lt;/li&gt;
&lt;li&gt;paid payments&lt;/li&gt;
&lt;li&gt;paying payments&lt;/li&gt;
&lt;li&gt;expired/unresolved payments&lt;/li&gt;
&lt;li&gt;order ID&lt;/li&gt;
&lt;li&gt;customer email&lt;/li&gt;
&lt;li&gt;amount&lt;/li&gt;
&lt;li&gt;currency/network&lt;/li&gt;
&lt;li&gt;payment type&lt;/li&gt;
&lt;li&gt;track ID&lt;/li&gt;
&lt;li&gt;last webhook time&lt;/li&gt;
&lt;li&gt;fulfillment status&lt;/li&gt;
&lt;li&gt;export CSV button&lt;/li&gt;
&lt;li&gt;support notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not need to be complex.&lt;/p&gt;

&lt;p&gt;The goal is to prevent the merchant from asking the agency:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Where do I see what happened with this payment?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A dashboard reduces support load and increases perceived value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 6: Support SOPs
&lt;/h2&gt;

&lt;p&gt;A launch kit for agencies should include support playbooks.&lt;/p&gt;

&lt;p&gt;This is one of the most underrated parts.&lt;/p&gt;

&lt;p&gt;Crypto payment support can be confusing for non-technical teams.&lt;/p&gt;

&lt;p&gt;Your SOPs can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer says they paid but order is not active&lt;/li&gt;
&lt;li&gt;invoice expired before payment arrived&lt;/li&gt;
&lt;li&gt;payment is paying but not paid&lt;/li&gt;
&lt;li&gt;wrong network used&lt;/li&gt;
&lt;li&gt;underpaid amount&lt;/li&gt;
&lt;li&gt;duplicate order attempt&lt;/li&gt;
&lt;li&gt;webhook failed&lt;/li&gt;
&lt;li&gt;merchant cannot find order&lt;/li&gt;
&lt;li&gt;customer needs payment instructions&lt;/li&gt;
&lt;li&gt;finance team needs export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each SOP should include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Issue:
What the customer says.

Check:
Where support should look first.

Data needed:
Order ID, track ID, customer email, amount, coin, network, timestamp.

Action:
What support can do.

Escalation:
When to send to developer/agency/OxaPay support.

Customer response:
A safe response template.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agencies can hand this to clients.&lt;br&gt;
That alone can justify part of the fee.&lt;/p&gt;


&lt;h2&gt;
  
  
  Layer 7: Deployment and QA checklist
&lt;/h2&gt;

&lt;p&gt;The agency needs a repeatable deployment checklist.&lt;/p&gt;

&lt;p&gt;Your kit should include one.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment:
[ ] API keys stored in environment variables
[ ] API keys never committed to repository
[ ] HTTPS webhook endpoint configured
[ ] Webhook route protected with HMAC validation
[ ] Production and test environments separated

Checkout:
[ ] Invoice amount matches order amount
[ ] Currency handling confirmed
[ ] Expiration behavior tested
[ ] Return URL tested
[ ] Customer instructions reviewed

Webhook:
[ ] Raw request body captured
[ ] HMAC validation tested
[ ] Duplicate webhook handling tested
[ ] Paid event triggers fulfillment once
[ ] Failed webhook delivery retry behavior understood

Operations:
[ ] Payment history sync job configured
[ ] Admin dashboard available
[ ] Support SOP shared
[ ] Export tested
[ ] Merchant team trained

Launch:
[ ] Real small-value payment tested
[ ] Logs reviewed
[ ] Client sign-off collected
[ ] Post-launch monitoring enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This transforms your code into a sellable implementation system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical architecture
&lt;/h2&gt;

&lt;p&gt;A typical Merchant Crypto Launch Kit can use this architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agency client project
    |
    |-- Checkout module
    |     |-- Hosted invoice flow
    |     |-- White-label checkout flow
    |     |-- Static address flow
    |
    |-- Webhook receiver
    |     |-- HMAC verification
    |     |-- Raw event storage
    |     |-- Payment state updates
    |     |-- Fulfillment trigger
    |
    |-- Merchant admin panel
    |     |-- payment records
    |     |-- order matching
    |     |-- support notes
    |     |-- exports
    |
    |-- Backfill worker
    |     |-- Payment History sync
    |     |-- Payment Information lookup
    |
    |-- Agency handoff layer
          |-- setup checklist
          |-- support SOP
          |-- client documentation
          |-- QA report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agency sees a repeatable system.&lt;br&gt;
The merchant sees a working payment operation.&lt;br&gt;
The developer owns the reusable infrastructure.&lt;/p&gt;


&lt;h2&gt;
  
  
  Suggested database schema
&lt;/h2&gt;

&lt;p&gt;A launch kit does not need a large schema at first.&lt;/p&gt;

&lt;p&gt;Start with these tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;agency_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_merchant_key_ref&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;webhook_secret_ref&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_intents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- invoice, white_label, static_address&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pay_currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;checkout_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload_hash&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;verified&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fulfillment_actions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payment_intents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;action_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- activate_order, send_license, unlock_access, notify_team&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;support_notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payment_intent_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payment_intents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_by&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple agency clients&lt;/li&gt;
&lt;li&gt;order-to-payment matching&lt;/li&gt;
&lt;li&gt;webhook idempotency&lt;/li&gt;
&lt;li&gt;support investigation&lt;/li&gt;
&lt;li&gt;fulfillment tracking&lt;/li&gt;
&lt;li&gt;dashboard views&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can extend it later with billing, agency plans, audit logs, payout modules, and role-based permissions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hosted invoice example
&lt;/h2&gt;

&lt;p&gt;Here is a simplified Node.js example for creating a hosted invoice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/merchants/:merchantId/create-invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customerEmail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// In production, load this from a secure vault or encrypted config store.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMerchantOxaPayKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1/payment/invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://your-launch-kit.com/webhooks/oxapay/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://merchant-site.com/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to create invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;savePaymentIntent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;checkoutUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paymentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;customerEmail&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;checkoutUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact response fields should be confirmed against the current OxaPay response shape during implementation, but the important product pattern is stable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the agency client creates an order&lt;/li&gt;
&lt;li&gt;your kit creates an invoice&lt;/li&gt;
&lt;li&gt;the customer pays&lt;/li&gt;
&lt;li&gt;OxaPay sends a webhook&lt;/li&gt;
&lt;li&gt;your kit updates the order and support dashboard&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Webhook receiver example
&lt;/h2&gt;

&lt;p&gt;Webhook handling is the most important technical module in the kit.&lt;/p&gt;

&lt;p&gt;Here is a simplified Express implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Raw body is required for HMAC validation.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/:merchantId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calculated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;calculated&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/:merchantId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webhookSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMerchantWebhookSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;webhookSecret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payloadHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;hasWebhookEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;storeWebhookEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;oxapayStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updatePaymentStateFromWebhook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enqueueFulfillmentOnce&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify the HMAC before trusting the payload&lt;/li&gt;
&lt;li&gt;store the raw event&lt;/li&gt;
&lt;li&gt;deduplicate callbacks&lt;/li&gt;
&lt;li&gt;trigger fulfillment only once&lt;/li&gt;
&lt;li&gt;return &lt;code&gt;ok&lt;/code&gt; after successful processing&lt;/li&gt;
&lt;li&gt;keep enough data for support and audit trails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the kind of module agencies do not want to rewrite for every client.&lt;/p&gt;




&lt;h2&gt;
  
  
  White-label checkout module
&lt;/h2&gt;

&lt;p&gt;Some agencies will want a branded payment experience.&lt;/p&gt;

&lt;p&gt;For that, your launch kit can include a white-label payment component.&lt;/p&gt;

&lt;p&gt;The flow is different from hosted invoice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer clicks Pay
    ↓
Client app requests white-label payment details
    ↓
OxaPay returns address, currency, amount, expiration, and related details
    ↓
Client UI renders the payment instructions
    ↓
Webhook confirms payment status
    ↓
Client app activates order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A white-label checkout module should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment address display&lt;/li&gt;
&lt;li&gt;QR code&lt;/li&gt;
&lt;li&gt;amount&lt;/li&gt;
&lt;li&gt;selected coin/network&lt;/li&gt;
&lt;li&gt;expiration timer&lt;/li&gt;
&lt;li&gt;copy-to-clipboard button&lt;/li&gt;
&lt;li&gt;status polling fallback&lt;/li&gt;
&lt;li&gt;support link&lt;/li&gt;
&lt;li&gt;instructions for wrong network / late payment risks&lt;/li&gt;
&lt;li&gt;webhook-driven final confirmation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This module can be sold as a premium part of the agency kit because it gives the agency more control over design and brand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Static address module
&lt;/h2&gt;

&lt;p&gt;Static addresses are useful for account-based products.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SaaS account top-ups&lt;/li&gt;
&lt;li&gt;internal balances&lt;/li&gt;
&lt;li&gt;user wallet deposits&lt;/li&gt;
&lt;li&gt;repeated payments from the same customer&lt;/li&gt;
&lt;li&gt;client portal funding&lt;/li&gt;
&lt;li&gt;hosting credit balances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A static address module should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer/account ID mapping&lt;/li&gt;
&lt;li&gt;address creation&lt;/li&gt;
&lt;li&gt;network and currency selection&lt;/li&gt;
&lt;li&gt;callback URL configuration&lt;/li&gt;
&lt;li&gt;address list sync&lt;/li&gt;
&lt;li&gt;revocation workflow&lt;/li&gt;
&lt;li&gt;deposit history&lt;/li&gt;
&lt;li&gt;support search by address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OxaPay’s static address documentation notes that a generated static address is linked to a &lt;code&gt;track_id&lt;/code&gt;, can use a &lt;code&gt;callback_url&lt;/code&gt;, and can be used to receive payments regardless of amount or timing. It also notes that static addresses with no transactions for six months will be revoked.&lt;/p&gt;

&lt;p&gt;That last detail matters.&lt;/p&gt;

&lt;p&gt;Your launch kit should include an operational note for agencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Static addresses are not a replacement for account management.
They need lifecycle handling, address list sync, and support visibility.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Plugin deployment module
&lt;/h2&gt;

&lt;p&gt;For agencies, plugins are often the fastest route.&lt;/p&gt;

&lt;p&gt;Your launch kit can include plugin-specific deployment guides for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WooCommerce&lt;/li&gt;
&lt;li&gt;WHMCS&lt;/li&gt;
&lt;li&gt;PrestaShop&lt;/li&gt;
&lt;li&gt;WISECP&lt;/li&gt;
&lt;li&gt;Clientexec&lt;/li&gt;
&lt;li&gt;Easy Digital Downloads&lt;/li&gt;
&lt;li&gt;Paid Memberships Pro&lt;/li&gt;
&lt;li&gt;Restrict Content Pro&lt;/li&gt;
&lt;li&gt;Gravity Forms&lt;/li&gt;
&lt;li&gt;other supported platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The value here is not only installation.&lt;/p&gt;

&lt;p&gt;A good plugin launch module includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pre-install requirements&lt;/li&gt;
&lt;li&gt;API key placement instructions&lt;/li&gt;
&lt;li&gt;payment status mapping&lt;/li&gt;
&lt;li&gt;checkout copy&lt;/li&gt;
&lt;li&gt;test order process&lt;/li&gt;
&lt;li&gt;client training notes&lt;/li&gt;
&lt;li&gt;support scripts&lt;/li&gt;
&lt;li&gt;launch QA&lt;/li&gt;
&lt;li&gt;rollback plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agencies can sell this as a fixed-price service.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Crypto Payment Launch for WooCommerce
Includes:
- OxaPay plugin setup
- checkout configuration
- test payment
- order status verification
- client handoff doc
- support SOP
- 7-day launch monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much more sellable than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Install a plugin.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Payment history backfill
&lt;/h2&gt;

&lt;p&gt;A professional launch kit should not depend only on webhooks.&lt;/p&gt;

&lt;p&gt;Webhooks can fail.&lt;br&gt;
Servers can be down.&lt;br&gt;
Callbacks can be missed.&lt;br&gt;
A client may ask for a report later.&lt;/p&gt;

&lt;p&gt;Use Payment History as a backfill mechanism.&lt;/p&gt;

&lt;p&gt;A scheduled job can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch recent payments&lt;/li&gt;
&lt;li&gt;filter by time range&lt;/li&gt;
&lt;li&gt;compare with local records&lt;/li&gt;
&lt;li&gt;find missing events&lt;/li&gt;
&lt;li&gt;update payment status&lt;/li&gt;
&lt;li&gt;create support alerts&lt;/li&gt;
&lt;li&gt;generate daily reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pseudo-flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every 10 minutes:
    fetch recent OxaPay payments
    for each payment:
        find local payment_intent by track_id or order_id
        if missing:
            create unresolved payment record
        if status changed:
            update local status
        if paid but not fulfilled:
            enqueue fulfillment check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the kit production-friendly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The agency-facing admin portal
&lt;/h2&gt;

&lt;p&gt;If you want to turn this into a serious product, build an agency portal.&lt;/p&gt;

&lt;p&gt;The portal can let the agency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add merchant clients&lt;/li&gt;
&lt;li&gt;store API configuration securely&lt;/li&gt;
&lt;li&gt;choose integration type&lt;/li&gt;
&lt;li&gt;generate client onboarding docs&lt;/li&gt;
&lt;li&gt;view launch checklist progress&lt;/li&gt;
&lt;li&gt;monitor webhook health&lt;/li&gt;
&lt;li&gt;view payment status per client&lt;/li&gt;
&lt;li&gt;export support reports&lt;/li&gt;
&lt;li&gt;create handoff packages&lt;/li&gt;
&lt;li&gt;manage support playbooks&lt;/li&gt;
&lt;li&gt;clone templates across clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where your launch kit becomes a SaaS or managed platform instead of a folder of scripts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Client handoff package
&lt;/h2&gt;

&lt;p&gt;A strong launch kit should generate a handoff package for each merchant.&lt;/p&gt;

&lt;p&gt;Example contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merchant Crypto Payment Handoff Package

1. Payment flow summary
2. Supported payment method
3. Checkout screenshots
4. Test payment result
5. Webhook endpoint status
6. Admin dashboard URL
7. How to find a payment
8. How to handle common customer issues
9. Escalation process
10. Security notes
11. Who owns API keys
12. Who handles support
13. Post-launch monitoring period
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agencies love anything that reduces post-launch confusion.&lt;/p&gt;

&lt;p&gt;If your kit creates these assets automatically, it becomes more valuable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revenue model for developers
&lt;/h2&gt;

&lt;p&gt;There are several ways to monetize this product.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Fixed-price implementation kit
&lt;/h3&gt;

&lt;p&gt;Sell the kit as a done-for-you package to agencies.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$500-$2,000 per agency client launch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This depends on complexity, market, client type, and support level.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Agency license
&lt;/h3&gt;

&lt;p&gt;Charge agencies a monthly fee to use your kit.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$99-$499/month per agency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The higher end requires dashboard, support, templates, and updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Per-merchant license
&lt;/h3&gt;

&lt;p&gt;Charge based on how many merchant clients the agency manages.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$20-$100/month per active merchant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works if your product includes monitoring, dashboard, alerts, or reporting.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. White-label agency package
&lt;/h3&gt;

&lt;p&gt;Allow agencies to resell your kit under their own brand.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Setup fee + monthly white-label license
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be attractive if agencies want to offer “crypto payment launch” without revealing backend tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Support and monitoring retainer
&lt;/h3&gt;

&lt;p&gt;Charge for ongoing support.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monthly monitoring
Webhook health checks
Payment issue investigation
Reporting support
Client handoff updates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be more stable than project-only revenue.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Custom modules
&lt;/h3&gt;

&lt;p&gt;Charge separately for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;white-label checkout UI&lt;/li&gt;
&lt;li&gt;static address account funding&lt;/li&gt;
&lt;li&gt;payout workflow&lt;/li&gt;
&lt;li&gt;reconciliation dashboard&lt;/li&gt;
&lt;li&gt;Telegram/Discord access automation&lt;/li&gt;
&lt;li&gt;accounting export&lt;/li&gt;
&lt;li&gt;CRM integration&lt;/li&gt;
&lt;li&gt;custom reporting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is to avoid selling unlimited custom work inside one fixed package.&lt;/p&gt;




&lt;h2&gt;
  
  
  Suggested package tiers
&lt;/h2&gt;

&lt;p&gt;Here is a practical packaging model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starter kit
&lt;/h3&gt;

&lt;p&gt;For agencies that need simple client launches.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugin setup guide&lt;/li&gt;
&lt;li&gt;hosted invoice integration&lt;/li&gt;
&lt;li&gt;webhook starter&lt;/li&gt;
&lt;li&gt;basic QA checklist&lt;/li&gt;
&lt;li&gt;handoff document template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WooCommerce&lt;/li&gt;
&lt;li&gt;small ecommerce&lt;/li&gt;
&lt;li&gt;service invoices&lt;/li&gt;
&lt;li&gt;basic merchant sites&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pro kit
&lt;/h3&gt;

&lt;p&gt;For agencies doing custom client work.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hosted invoice flow&lt;/li&gt;
&lt;li&gt;white-label checkout component&lt;/li&gt;
&lt;li&gt;webhook receiver&lt;/li&gt;
&lt;li&gt;payment dashboard&lt;/li&gt;
&lt;li&gt;support SOPs&lt;/li&gt;
&lt;li&gt;Payment History backfill&lt;/li&gt;
&lt;li&gt;client handoff generator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SaaS&lt;/li&gt;
&lt;li&gt;custom ecommerce&lt;/li&gt;
&lt;li&gt;digital product stores&lt;/li&gt;
&lt;li&gt;membership platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Agency platform
&lt;/h3&gt;

&lt;p&gt;For agencies managing multiple merchant clients.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-merchant dashboard&lt;/li&gt;
&lt;li&gt;client configuration&lt;/li&gt;
&lt;li&gt;webhook health monitoring&lt;/li&gt;
&lt;li&gt;launch checklist per client&lt;/li&gt;
&lt;li&gt;reporting exports&lt;/li&gt;
&lt;li&gt;role-based agency users&lt;/li&gt;
&lt;li&gt;reusable templates&lt;/li&gt;
&lt;li&gt;white-label docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ecommerce agencies&lt;/li&gt;
&lt;li&gt;SaaS implementation agencies&lt;/li&gt;
&lt;li&gt;Web3 studios&lt;/li&gt;
&lt;li&gt;hosting solution agencies&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  MVP build plan
&lt;/h2&gt;

&lt;p&gt;Here is a realistic 21-day MVP plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Days 1-3: Discovery and scope
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agency onboarding questionnaire&lt;/li&gt;
&lt;li&gt;integration decision tree&lt;/li&gt;
&lt;li&gt;launch checklist&lt;/li&gt;
&lt;li&gt;first client handoff template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not write code first.&lt;br&gt;
Package the service first.&lt;/p&gt;
&lt;h3&gt;
  
  
  Days 4-7: Hosted invoice module
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create invoice API route&lt;/li&gt;
&lt;li&gt;local payment intent record&lt;/li&gt;
&lt;li&gt;redirect to payment URL&lt;/li&gt;
&lt;li&gt;basic order mapping&lt;/li&gt;
&lt;li&gt;configuration per merchant&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Days 8-11: Webhook module
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;raw body handling&lt;/li&gt;
&lt;li&gt;HMAC verification&lt;/li&gt;
&lt;li&gt;event storage&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;status update logic&lt;/li&gt;
&lt;li&gt;fulfillment queue&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Days 12-14: Dashboard
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payments table&lt;/li&gt;
&lt;li&gt;order search&lt;/li&gt;
&lt;li&gt;status filters&lt;/li&gt;
&lt;li&gt;webhook event view&lt;/li&gt;
&lt;li&gt;support notes&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Days 15-17: Agency templates
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plugin launch SOP&lt;/li&gt;
&lt;li&gt;client handoff document&lt;/li&gt;
&lt;li&gt;support scripts&lt;/li&gt;
&lt;li&gt;QA checklist&lt;/li&gt;
&lt;li&gt;merchant training note&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Days 18-19: Backfill and monitoring
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment History sync&lt;/li&gt;
&lt;li&gt;missing payment detection&lt;/li&gt;
&lt;li&gt;webhook health alert&lt;/li&gt;
&lt;li&gt;paid-but-not-fulfilled check&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Days 20-21: Demo package
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;demo merchant&lt;/li&gt;
&lt;li&gt;sample agency project&lt;/li&gt;
&lt;li&gt;sales page&lt;/li&gt;
&lt;li&gt;onboarding form&lt;/li&gt;
&lt;li&gt;short walkthrough video&lt;/li&gt;
&lt;li&gt;pricing page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end, you should have something an agency can understand and buy.&lt;/p&gt;


&lt;h2&gt;
  
  
  What to avoid
&lt;/h2&gt;

&lt;p&gt;A launch kit can fail if it becomes too broad.&lt;/p&gt;

&lt;p&gt;Avoid these mistakes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mistake 1: Selling “crypto payment gateway setup”
&lt;/h3&gt;

&lt;p&gt;That sounds like a commodity.&lt;/p&gt;

&lt;p&gt;Sell operational launch, not technical setup.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mistake 2: Supporting every platform on day one
&lt;/h3&gt;

&lt;p&gt;Start with one or two platforms.&lt;/p&gt;

&lt;p&gt;Good starting points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WooCommerce&lt;/li&gt;
&lt;li&gt;WHMCS&lt;/li&gt;
&lt;li&gt;custom Node/Laravel checkout&lt;/li&gt;
&lt;li&gt;digital download store&lt;/li&gt;
&lt;li&gt;SaaS billing flow&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Mistake 3: Ignoring support workflows
&lt;/h3&gt;

&lt;p&gt;Merchants will have questions after launch.&lt;/p&gt;

&lt;p&gt;If your kit does not include support SOPs, agencies will feel exposed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mistake 4: Triggering fulfillment from frontend redirects
&lt;/h3&gt;

&lt;p&gt;Always use backend confirmation.&lt;/p&gt;

&lt;p&gt;Frontend redirects are user-experience signals, not payment settlement logic.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mistake 5: No idempotency
&lt;/h3&gt;

&lt;p&gt;Webhook callbacks can be retried or duplicated.&lt;/p&gt;

&lt;p&gt;Your code must not fulfill the same order twice.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mistake 6: No backfill strategy
&lt;/h3&gt;

&lt;p&gt;Use Payment History to reconcile records.&lt;/p&gt;

&lt;p&gt;A launch kit that cannot recover from missed events is not production-friendly.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mistake 7: Overpromising compliance
&lt;/h3&gt;

&lt;p&gt;You are building payment implementation tooling.&lt;br&gt;
You are not automatically solving tax, legal, sanctions, licensing, or merchant-of-record responsibilities.&lt;/p&gt;

&lt;p&gt;Be clear about boundaries.&lt;/p&gt;


&lt;h2&gt;
  
  
  Security checklist
&lt;/h2&gt;

&lt;p&gt;At minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Use HTTPS for webhook endpoints
[ ] Verify HMAC signatures
[ ] Store API keys in environment variables or a secret manager
[ ] Never expose merchant API keys to the frontend
[ ] Store raw webhook events for auditability
[ ] Make fulfillment idempotent
[ ] Separate test/staging from production
[ ] Restrict dashboard access by role
[ ] Log admin actions
[ ] Add alerting for webhook failures
[ ] Review platform-specific data retention needs
[ ] Rotate keys when agency/client access changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OxaPay’s SDK documentation also emphasizes verifying webhook HMAC, using HTTPS, storing keys outside code, and rotating keys regularly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes this a real business?
&lt;/h2&gt;

&lt;p&gt;A Merchant Crypto Launch Kit becomes a real business when it has three things.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Repeatability
&lt;/h3&gt;

&lt;p&gt;You can deploy the same base system across multiple agency clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Clear buyer
&lt;/h3&gt;

&lt;p&gt;Your buyer is not a random merchant.&lt;br&gt;
Your buyer is an agency with multiple merchants.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Operational value
&lt;/h3&gt;

&lt;p&gt;You reduce the agency’s implementation time, delivery risk, support burden, and documentation work.&lt;/p&gt;

&lt;p&gt;That is what agencies pay for.&lt;/p&gt;


&lt;h2&gt;
  
  
  Example sales positioning
&lt;/h2&gt;

&lt;p&gt;Here is a practical version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We help ecommerce and SaaS agencies launch crypto payment acceptance for merchant clients without rebuilding payment logic from scratch.

Our launch kit includes:
- OxaPay invoice or white-label checkout setup
- webhook receiver and payment status handling
- merchant dashboard starter
- support SOPs
- launch QA checklist
- client handoff documentation
- optional plugin deployment for supported platforms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A shorter version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Crypto payment launch infrastructure for agencies serving ecommerce, SaaS, and digital product merchants.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a serious offer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Developer takeaway
&lt;/h2&gt;

&lt;p&gt;The opportunity is not to build yet another crypto payment gateway.&lt;/p&gt;

&lt;p&gt;The opportunity is to build the missing delivery layer around crypto payment infrastructure.&lt;/p&gt;

&lt;p&gt;Agencies need repeatable systems.&lt;br&gt;
Merchants need working operations.&lt;br&gt;
Developers can sit between both.&lt;/p&gt;

&lt;p&gt;OxaPay provides the payment primitives: invoices, white-label payments, static addresses, webhooks, plugins, history endpoints, SDKs, and payout APIs.&lt;/p&gt;

&lt;p&gt;Your product can package those primitives into a launch system agencies can sell again and again.&lt;/p&gt;

&lt;p&gt;That is a much better business than doing one-off integration work forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;OxaPay Generate White Label API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Generate Static Address API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/plugins" rel="noopener noreferrer"&gt;OxaPay Plugins Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>crypto</category>
      <category>backend</category>
      <category>api</category>
    </item>
    <item>
      <title>Build a Payment Automation Studio for Crypto Merchants</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Thu, 16 Jul 2026 07:30:20 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-payment-automation-studio-for-crypto-merchants-3k6m</link>
      <guid>https://dev.to/kevins1988/build-a-payment-automation-studio-for-crypto-merchants-3k6m</guid>
      <description>&lt;p&gt;Most developers treat crypto payments as a checkout integration.&lt;/p&gt;

&lt;p&gt;Create an invoice.&lt;/p&gt;

&lt;p&gt;Redirect the customer.&lt;/p&gt;

&lt;p&gt;Wait for a webhook.&lt;/p&gt;

&lt;p&gt;Mark the order as paid.&lt;/p&gt;

&lt;p&gt;That is useful, but it is not where the larger business opportunity is.&lt;/p&gt;

&lt;p&gt;For merchants, payment is rarely the final step. A payment is usually the trigger for a business action.&lt;/p&gt;

&lt;p&gt;A paid invoice may need to activate a SaaS plan.&lt;/p&gt;

&lt;p&gt;A confirmed transaction may need to send a license key.&lt;/p&gt;

&lt;p&gt;An expired invoice may need to send a reminder.&lt;/p&gt;

&lt;p&gt;A paid order may need to update a CRM, notify a sales team, add a customer to a Telegram group, unlock a Discord role, create a support ticket, update a spreadsheet, or generate a finance report.&lt;/p&gt;

&lt;p&gt;That is where developers can build something more valuable than another payment button.&lt;/p&gt;

&lt;p&gt;They can build a &lt;strong&gt;Payment Automation Studio for crypto merchants&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, I will use &lt;strong&gt;OxaPay&lt;/strong&gt; as the example crypto payment infrastructure because its documentation gives developers the primitives needed for this kind of product: invoice generation, payment webhooks, payment information lookup, payment history, static addresses, payouts, SDKs, n8n workflows, and Make modules.&lt;/p&gt;

&lt;p&gt;The goal is not to sell a fantasy like “build one template and earn passive income forever.”&lt;/p&gt;

&lt;p&gt;The goal is to design a real merchant-facing automation product or service that developers can sell, maintain, and expand.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;A Payment Automation Studio is a system that connects crypto payment events to business workflows.&lt;/p&gt;

&lt;p&gt;Instead of telling a merchant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can integrate crypto payments into your website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You offer something stronger:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can automate what happens after a crypto payment is created, paid, expired, failed, reviewed, or paid out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;Merchants do not only pay for APIs. They pay for reduced manual work, fewer support tickets, faster fulfillment, cleaner records, and better operational control.&lt;/p&gt;

&lt;p&gt;A developer can build automation packages such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paid invoice → activate SaaS plan&lt;/li&gt;
&lt;li&gt;Paid invoice → send license key&lt;/li&gt;
&lt;li&gt;Paid invoice → deliver digital download&lt;/li&gt;
&lt;li&gt;Paid invoice → add user to Telegram group&lt;/li&gt;
&lt;li&gt;Paid invoice → assign Discord role&lt;/li&gt;
&lt;li&gt;Paid invoice → update CRM record&lt;/li&gt;
&lt;li&gt;Paid invoice → notify finance team&lt;/li&gt;
&lt;li&gt;Expired invoice → send reminder&lt;/li&gt;
&lt;li&gt;Failed payment → create support ticket&lt;/li&gt;
&lt;li&gt;Static address payment → top up user balance&lt;/li&gt;
&lt;li&gt;Payout completed → notify contractor or affiliate&lt;/li&gt;
&lt;li&gt;Payment history sync → generate daily report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not just payment integration.&lt;/p&gt;

&lt;p&gt;It is payment operations automation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this can become a real developer business
&lt;/h2&gt;

&lt;p&gt;A merchant can usually figure out how to create a payment link.&lt;/p&gt;

&lt;p&gt;The hard part is connecting that payment event to the rest of the business.&lt;/p&gt;

&lt;p&gt;A small digital product seller might need automatic delivery.&lt;/p&gt;

&lt;p&gt;A SaaS founder might need plan activation.&lt;/p&gt;

&lt;p&gt;A Telegram creator might need access control.&lt;/p&gt;

&lt;p&gt;A Discord community might need role management.&lt;/p&gt;

&lt;p&gt;A marketplace might need payout notifications.&lt;/p&gt;

&lt;p&gt;A finance team might need daily exports.&lt;/p&gt;

&lt;p&gt;A support team might need alerts when a payment is stuck.&lt;/p&gt;

&lt;p&gt;These are not abstract developer problems. They are operational problems.&lt;/p&gt;

&lt;p&gt;Operational problems create service revenue because merchants often do not want to build and maintain workflow logic themselves.&lt;/p&gt;

&lt;p&gt;A developer can monetize this in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup fees for automation implementation&lt;/li&gt;
&lt;li&gt;monthly monitoring retainers&lt;/li&gt;
&lt;li&gt;hosted SaaS subscription&lt;/li&gt;
&lt;li&gt;template licensing&lt;/li&gt;
&lt;li&gt;workflow maintenance packages&lt;/li&gt;
&lt;li&gt;custom connector development&lt;/li&gt;
&lt;li&gt;agency white-label implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important point is that the developer is not selling “code.”&lt;/p&gt;

&lt;p&gt;They are selling a working payment workflow that reduces manual effort.&lt;/p&gt;




&lt;h2&gt;
  
  
  What OxaPay provides as the payment layer
&lt;/h2&gt;

&lt;p&gt;For a Payment Automation Studio, you need payment primitives that can be triggered, tracked, and connected to workflows.&lt;/p&gt;

&lt;p&gt;OxaPay provides several useful primitives in its documentation.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Generate Invoice&lt;/strong&gt; endpoint creates a new invoice and returns a payment URL that the customer can use to complete the transaction. The request can include values such as amount, currency, callback URL, return URL, order ID, description, lifetime, fee handling, and underpaid coverage depending on the integration design. See the OxaPay docs for Generate Invoice: &lt;code&gt;https://docs.oxapay.com/api-reference/payment/generate-invoice&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Webhook&lt;/strong&gt; documentation explains how to receive payment status updates through an HTTPS POST callback. The callback URL is provided in merchant requests, and OxaPay sends payment updates to that URL when status changes happen. See: &lt;code&gt;https://docs.oxapay.com/webhook&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Payment Information&lt;/strong&gt; endpoint lets a developer retrieve details about a specific payment using its &lt;code&gt;track_id&lt;/code&gt;. This is important when an automation workflow needs to verify the latest payment state before executing a sensitive action. See: &lt;code&gt;https://docs.oxapay.com/api-reference/payment/payment-information&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Payment History&lt;/strong&gt; endpoint lets a developer retrieve payment records with filters such as time range, status, type, amount, currency, network, and pagination. This is useful for backfill jobs, reports, reconciliation, and workflow recovery. See: &lt;code&gt;https://docs.oxapay.com/api-reference/payment/payment-history&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Generate Static Address&lt;/strong&gt; endpoint creates a reusable payment address linked to a &lt;code&gt;track_id&lt;/code&gt;, and can send callbacks for payments made to that address. This is useful for balance top-ups, customer wallets, account funding flows, and repeated payments where a reusable address makes more sense than a one-time invoice. See: &lt;code&gt;https://docs.oxapay.com/api-reference/payment/generate-static-address&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Generate Payout&lt;/strong&gt; endpoint can be used when the automation includes outbound crypto payments, such as contractor notifications, affiliate payouts, creator payouts, or post-payment settlement flows. See: &lt;code&gt;https://docs.oxapay.com/api-reference/payout/generate-payout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;OxaPay also provides SDKs for languages such as PHP, Python, and Laravel, with webhook handling support documented in the SDK pages. See the Python SDK documentation: &lt;code&gt;https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For low-code builders, OxaPay also appears in workflow automation contexts. The Make app documentation lists modules such as Generate an invoice, Generate a payout, Generate a static address, Generate a white label, Get payment information, Search payments, Watch payment webhook, and Watch payout webhook. See: &lt;code&gt;https://apps.make.com/oxapay-crypto-pay-gtw&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;OxaPay also has an official n8n community node repository for automating payment flows, payouts, swaps, account utilities, and webhook-driven workflows inside n8n. See: &lt;code&gt;https://github.com/OxaPay/n8n-nodes-oxapay&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These primitives are enough to build a serious automation product.&lt;/p&gt;

&lt;p&gt;But the value of the product is not the API call.&lt;/p&gt;

&lt;p&gt;The value is what happens after the API call.&lt;/p&gt;




&lt;h2&gt;
  
  
  The product you are building
&lt;/h2&gt;

&lt;p&gt;A Payment Automation Studio can be packaged in three ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A managed automation service
&lt;/h3&gt;

&lt;p&gt;This is the fastest path for freelancers and small agencies.&lt;/p&gt;

&lt;p&gt;You build custom workflows for each merchant using OxaPay, n8n, Make, or custom scripts.&lt;/p&gt;

&lt;p&gt;The merchant pays for setup and ongoing monitoring.&lt;/p&gt;

&lt;p&gt;This model works well when the merchant has a specific operational workflow, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“When payment is confirmed, send the license key.”&lt;/li&gt;
&lt;li&gt;“When payment expires, notify the sales team.”&lt;/li&gt;
&lt;li&gt;“When payment is paid, add the customer to our Telegram group.”&lt;/li&gt;
&lt;li&gt;“Every morning, send yesterday’s crypto payment report.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to build a SaaS platform on day one.&lt;/p&gt;

&lt;p&gt;You need repeatable workflow packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A hosted automation SaaS
&lt;/h3&gt;

&lt;p&gt;This is harder, but more scalable.&lt;/p&gt;

&lt;p&gt;You build a web app where merchants can connect their payment account, choose workflow templates, configure destinations, and monitor executions.&lt;/p&gt;

&lt;p&gt;The product includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;workflow templates&lt;/li&gt;
&lt;li&gt;trigger configuration&lt;/li&gt;
&lt;li&gt;action connectors&lt;/li&gt;
&lt;li&gt;webhook ingestion&lt;/li&gt;
&lt;li&gt;execution logs&lt;/li&gt;
&lt;li&gt;retry queue&lt;/li&gt;
&lt;li&gt;alerting&lt;/li&gt;
&lt;li&gt;merchant dashboard&lt;/li&gt;
&lt;li&gt;billing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This requires more engineering, but it can support recurring revenue.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A hybrid template plus implementation business
&lt;/h3&gt;

&lt;p&gt;This is often the most practical model.&lt;/p&gt;

&lt;p&gt;You build reusable templates for common merchant workflows, but you also sell implementation and support.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;n8n workflow template: paid invoice → Google Sheet + Telegram alert&lt;/li&gt;
&lt;li&gt;Make scenario: paid invoice → email receipt + CRM update&lt;/li&gt;
&lt;li&gt;custom Node.js service: paid invoice → license key delivery&lt;/li&gt;
&lt;li&gt;hosted dashboard: payment events + workflow logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can sell templates as entry-level products and convert serious merchants into managed clients.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who would pay for this?
&lt;/h2&gt;

&lt;p&gt;The best customers are merchants with repeated payment-triggered work.&lt;/p&gt;

&lt;p&gt;A one-time seller may not care.&lt;/p&gt;

&lt;p&gt;A merchant with recurring operational tasks will care.&lt;/p&gt;

&lt;p&gt;Good customer segments include:&lt;/p&gt;

&lt;h3&gt;
  
  
  Digital product sellers
&lt;/h3&gt;

&lt;p&gt;They sell files, license keys, templates, scripts, plugins, courses, reports, or paid downloads.&lt;/p&gt;

&lt;p&gt;Their automation need is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Payment confirmed → deliver the product automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  SaaS founders
&lt;/h3&gt;

&lt;p&gt;They want crypto payments to activate plans, extend access, unlock features, or update account status.&lt;/p&gt;

&lt;p&gt;Their automation need is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Payment confirmed → update subscription state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Hosting, VPN, and server resellers
&lt;/h3&gt;

&lt;p&gt;They need payment confirmation to provision service, renew accounts, suspend expired subscriptions, or notify support.&lt;/p&gt;

&lt;p&gt;Their automation need is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Payment confirmed → provision or renew service.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Telegram and Discord communities
&lt;/h3&gt;

&lt;p&gt;They monetize private access, paid groups, VIP channels, creator communities, or trading communities.&lt;/p&gt;

&lt;p&gt;Their automation need is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Payment confirmed → grant access. Expiry reached → revoke access.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Agencies and service providers
&lt;/h3&gt;

&lt;p&gt;They receive international payments and need internal workflows.&lt;/p&gt;

&lt;p&gt;Their automation need is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Payment confirmed → update project status, notify account manager, create invoice record, and send receipt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Marketplaces and affiliate programs
&lt;/h3&gt;

&lt;p&gt;They receive payments and may need payout notifications, partner dashboards, and revenue reports.&lt;/p&gt;

&lt;p&gt;Their automation need is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Payment received → update ledger. Payout sent → notify partner.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Finance and operations teams
&lt;/h3&gt;

&lt;p&gt;They care less about checkout and more about records.&lt;/p&gt;

&lt;p&gt;Their automation need is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Payment events → clean reports, alerts, exports, and exception queues.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The automation architecture
&lt;/h2&gt;

&lt;p&gt;A production-grade Payment Automation Studio needs more than a webhook endpoint.&lt;/p&gt;

&lt;p&gt;It needs an event-driven architecture.&lt;/p&gt;

&lt;p&gt;Here is the basic model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer action
     ↓
Merchant app / checkout / bot
     ↓
OxaPay invoice, white-label payment, or static address
     ↓
Payment status changes
     ↓
Webhook event received
     ↓
Event normalized and stored
     ↓
Rules engine checks conditions
     ↓
Workflow action runs
     ↓
Result logged
     ↓
Retry / alert / report if needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key components are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trigger layer&lt;/strong&gt; — receives webhooks, scheduled jobs, manual actions, or polling results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event store&lt;/strong&gt; — saves raw and normalized payment events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules engine&lt;/strong&gt; — decides whether an action should run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow runner&lt;/strong&gt; — executes actions such as email, CRM update, product delivery, or access grant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connector layer&lt;/strong&gt; — integrates with Telegram, Discord, Google Sheets, HubSpot, Airtable, Shopify, WHMCS, email services, or custom APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution log&lt;/strong&gt; — records every action, result, error, and retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin dashboard&lt;/strong&gt; — lets merchants monitor workflows and resolve failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill worker&lt;/strong&gt; — checks Payment History to recover missed or delayed events.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mistake many developers make is building only step 1 and step 4.&lt;/p&gt;

&lt;p&gt;They receive a webhook and immediately run an action.&lt;/p&gt;

&lt;p&gt;That works in demos.&lt;/p&gt;

&lt;p&gt;It fails in production because webhooks can be duplicated, delayed, retried, or temporarily fail.&lt;/p&gt;

&lt;p&gt;A real automation studio must treat every payment event as data first, then run actions safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The workflow model
&lt;/h2&gt;

&lt;p&gt;Every automation can be described as a simple object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deliver license key after paid invoice&lt;/span&gt;
&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oxapay&lt;/span&gt;
  &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment.status_changed&lt;/span&gt;
&lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment.status&lt;/span&gt;
    &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;equals&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Paid&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payment.order_id&lt;/span&gt;
    &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exists&lt;/span&gt;
&lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal.find_order&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;payment.order_id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;license.assign_key&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;product_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order.product_id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;customer_email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order.customer_email&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email.send&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order.customer_email&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;license_delivery&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;merchant.notify&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;telegram&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order.id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;was&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;paid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;delivered."&lt;/span&gt;
&lt;span class="na"&gt;retry_policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;max_attempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;exponential&lt;/span&gt;
&lt;span class="na"&gt;failure_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support.create_ticket&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure makes the product easier to sell and maintain.&lt;/p&gt;

&lt;p&gt;Merchants understand workflows.&lt;/p&gt;

&lt;p&gt;Developers can turn workflows into reusable templates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core workflow templates you can sell
&lt;/h2&gt;

&lt;p&gt;A Payment Automation Studio becomes valuable when it has repeatable workflow templates.&lt;/p&gt;

&lt;p&gt;Here are practical templates developers can productize.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template 1: Paid invoice to digital delivery
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;digital product sellers&lt;/li&gt;
&lt;li&gt;template sellers&lt;/li&gt;
&lt;li&gt;course sellers&lt;/li&gt;
&lt;li&gt;software sellers&lt;/li&gt;
&lt;li&gt;PDF/report sellers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer buys product
→ invoice created
→ customer pays
→ OxaPay webhook sends Paid event
→ system verifies track_id
→ system finds order_id
→ product file or license key is delivered
→ merchant receives confirmation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why merchants pay:&lt;/p&gt;

&lt;p&gt;Manual delivery creates delays and support tickets. Automatic delivery improves customer experience and reduces human work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template 2: Paid invoice to SaaS activation
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;indie SaaS&lt;/li&gt;
&lt;li&gt;subscription tools&lt;/li&gt;
&lt;li&gt;membership apps&lt;/li&gt;
&lt;li&gt;developer tools&lt;/li&gt;
&lt;li&gt;AI tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User selects plan
→ invoice created
→ payment becomes Paid
→ user account is upgraded
→ subscription period is extended
→ customer receives confirmation
→ admin dashboard records the payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why merchants pay:&lt;/p&gt;

&lt;p&gt;SaaS businesses cannot rely on manual plan activation. They need the billing state and product access state to stay aligned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template 3: Paid invoice to Telegram or Discord access
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid communities&lt;/li&gt;
&lt;li&gt;creator memberships&lt;/li&gt;
&lt;li&gt;VIP groups&lt;/li&gt;
&lt;li&gt;education channels&lt;/li&gt;
&lt;li&gt;trading communities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User selects access plan
→ invoice created
→ payment becomes Paid
→ bot grants access
→ expiry date is stored
→ reminder is sent before expiry
→ access is revoked if renewal does not happen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why merchants pay:&lt;/p&gt;

&lt;p&gt;The pain is not the payment. The pain is access management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template 4: Expired invoice to reminder sequence
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sales teams&lt;/li&gt;
&lt;li&gt;high-ticket services&lt;/li&gt;
&lt;li&gt;digital sellers&lt;/li&gt;
&lt;li&gt;course sellers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invoice created
→ no payment before expiration
→ status becomes Expired
→ reminder email or Telegram message is sent
→ sales team is notified if amount is high
→ optional new invoice is created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why merchants pay:&lt;/p&gt;

&lt;p&gt;Expired invoices are often lost revenue. A reminder workflow can recover some of that demand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template 5: Payment event to CRM update
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agencies&lt;/li&gt;
&lt;li&gt;B2B services&lt;/li&gt;
&lt;li&gt;SaaS teams&lt;/li&gt;
&lt;li&gt;sales-led businesses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment becomes Paid
→ contact is created or updated
→ deal is moved to paid stage
→ payment amount is logged
→ account manager is notified
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why merchants pay:&lt;/p&gt;

&lt;p&gt;Sales and finance teams need payment data inside the tools they already use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template 6: Payment event to finance export
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;finance teams&lt;/li&gt;
&lt;li&gt;operations teams&lt;/li&gt;
&lt;li&gt;ecommerce merchants&lt;/li&gt;
&lt;li&gt;multi-channel sellers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment event received
→ normalized payment record stored
→ daily export generated
→ report sent to finance
→ unresolved records flagged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why merchants pay:&lt;/p&gt;

&lt;p&gt;Manual exports become painful when payment volume grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template 7: Payout completed to partner notification
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;affiliate programs&lt;/li&gt;
&lt;li&gt;marketplaces&lt;/li&gt;
&lt;li&gt;creator platforms&lt;/li&gt;
&lt;li&gt;agencies&lt;/li&gt;
&lt;li&gt;contractor networks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payout request created
→ payout status changes
→ payout webhook received
→ partner is notified
→ payout record is logged
→ finance export is updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why merchants pay:&lt;/p&gt;

&lt;p&gt;Partners care about payout visibility. Support teams care about fewer payout questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template 8: Static address payment to balance top-up
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wallets&lt;/li&gt;
&lt;li&gt;internal credit systems&lt;/li&gt;
&lt;li&gt;gaming credits&lt;/li&gt;
&lt;li&gt;usage-based SaaS&lt;/li&gt;
&lt;li&gt;customer account balances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer receives static address
→ customer sends funds later
→ OxaPay callback arrives
→ transaction is verified
→ customer balance is increased
→ receipt is generated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why merchants pay:&lt;/p&gt;

&lt;p&gt;Reusable address flows are better for account funding and repeated deposits than one-time checkout links.&lt;/p&gt;




&lt;h2&gt;
  
  
  Low-code implementation with n8n or Make
&lt;/h2&gt;

&lt;p&gt;You do not need to build everything from scratch on day one.&lt;/p&gt;

&lt;p&gt;A developer can start with n8n or Make and turn repeatable workflows into service packages.&lt;/p&gt;

&lt;p&gt;OxaPay’s Make app documentation lists modules for generating invoices, payouts, static addresses, white-label payments, getting payment information, searching payments, and watching payment and payout webhooks.&lt;/p&gt;

&lt;p&gt;A typical Make scenario might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Watch payment webhook
→ Filter: status equals Paid
→ Get payment information by track_id
→ Search order in Google Sheets / Airtable / Shopify / internal API
→ Send product delivery email
→ Notify merchant in Telegram
→ Add row to finance sheet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical n8n workflow might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Webhook Trigger
→ OxaPay payment verification
→ IF status is Paid
→ HTTP Request to merchant API
→ Email node
→ Telegram node
→ Google Sheets node
→ Error workflow on failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is excellent for a service business because you can build quickly, learn merchant problems, and later decide which workflows deserve a custom SaaS product.&lt;/p&gt;

&lt;p&gt;But low-code automation has limits.&lt;/p&gt;

&lt;p&gt;For higher-volume merchants, you may need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stronger idempotency&lt;/li&gt;
&lt;li&gt;custom retry logic&lt;/li&gt;
&lt;li&gt;better audit logs&lt;/li&gt;
&lt;li&gt;encrypted credential storage&lt;/li&gt;
&lt;li&gt;user permissions&lt;/li&gt;
&lt;li&gt;workflow versioning&lt;/li&gt;
&lt;li&gt;execution history&lt;/li&gt;
&lt;li&gt;custom dashboards&lt;/li&gt;
&lt;li&gt;backfill workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where the hosted product opportunity appears.&lt;/p&gt;




&lt;h2&gt;
  
  
  Custom product architecture
&lt;/h2&gt;

&lt;p&gt;If you build a hosted Payment Automation Studio, the architecture should be more controlled.&lt;/p&gt;

&lt;p&gt;A good production design looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[OxaPay Webhook]
      ↓
[Webhook Receiver]
      ↓
[Signature Verification]
      ↓
[Raw Event Store]
      ↓
[Event Normalizer]
      ↓
[Idempotency Check]
      ↓
[Workflow Matcher]
      ↓
[Job Queue]
      ↓
[Workflow Runner]
      ↓
[Connector Actions]
      ↓
[Execution Log + Alerts]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should not execute business actions directly inside the webhook request.&lt;/p&gt;

&lt;p&gt;The webhook receiver should respond quickly after storing and validating the event.&lt;/p&gt;

&lt;p&gt;The actual workflow execution should happen in a background job.&lt;/p&gt;

&lt;p&gt;This protects the system from timeout issues, duplicate callbacks, slow third-party APIs, and temporary failures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Suggested database schema
&lt;/h2&gt;

&lt;p&gt;A minimal database schema could look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_merchant_key_encrypted&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;webhook_secret_encrypted&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'oxapay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;signature_valid&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;workflow_templates&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;template_json&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;workflows&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;template_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;workflow_templates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;trigger_config&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;condition_config&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;action_config&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;workflow_executions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;workflow_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;workflows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payment_event_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payment_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempt_count&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;action_executions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;workflow_execution_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;workflow_executions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;action_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;response_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;finished_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema keeps four things separate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;raw payment events&lt;/li&gt;
&lt;li&gt;workflow definitions&lt;/li&gt;
&lt;li&gt;workflow executions&lt;/li&gt;
&lt;li&gt;individual action results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That separation is important.&lt;/p&gt;

&lt;p&gt;When something goes wrong, the merchant should be able to see whether the payment event was received, whether the workflow matched, which action failed, and whether it was retried.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating an invoice from your studio
&lt;/h2&gt;

&lt;p&gt;Your automation studio may need to create invoices on behalf of merchants.&lt;/p&gt;

&lt;p&gt;For example, a merchant might call your API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/invoices
Content-Type: application/json

{
  "merchant_id": "m_123",
  "order_id": "ORD-9001",
  "amount": 49,
  "currency": "USD",
  "customer_email": "customer@example.com",
  "workflow": "digital_delivery"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your backend creates the invoice through OxaPay and stores the returned &lt;code&gt;track_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example Node.js implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOxaPayInvoice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1/payment/invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Payment for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-studio.example.com/webhooks/oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://your-merchant.example.com/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to create OxaPay invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/invoices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadMerchant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createOxaPayInvoice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oxapay_merchant_key_encrypted&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveInvoiceMapping&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, you should adapt field names to the exact response structure returned by the API version you are using.&lt;/p&gt;

&lt;p&gt;You should also avoid exposing merchant API keys to frontend code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Receiving payment webhooks safely
&lt;/h2&gt;

&lt;p&gt;The webhook receiver is the most important part of the system.&lt;/p&gt;

&lt;p&gt;It should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read the raw request body&lt;/li&gt;
&lt;li&gt;verify the signature or HMAC according to the payment provider documentation&lt;/li&gt;
&lt;li&gt;store the raw payload&lt;/li&gt;
&lt;li&gt;normalize the event&lt;/li&gt;
&lt;li&gt;create an idempotency key&lt;/li&gt;
&lt;li&gt;avoid duplicate workflow execution&lt;/li&gt;
&lt;li&gt;enqueue background jobs&lt;/li&gt;
&lt;li&gt;respond quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example Express implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Queue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bullmq&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Keep raw body for signature verification.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workflow-executions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyHmac&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receivedSignature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedSignature&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tx_hash&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_tx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-oxapay-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findMerchantByTrackIdOrCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Store unknown events separately for investigation.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;storeUnknownWebhook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_secret_encrypted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signatureValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyHmac&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;receivedSignature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signatureValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;storeInvalidWebhook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;storePaymentEventOnce&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment.status_changed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;signatureValid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasDuplicate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duplicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;workflowQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run-payment-workflows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentEventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Webhook processing failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact signature header and validation details should follow the current OxaPay webhook documentation and SDK examples. The important design principle is stable: verify first, store second, execute later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Matching workflows to events
&lt;/h2&gt;

&lt;p&gt;After storing a payment event, the workflow runner decides which automations should run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runPaymentWorkflows&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paymentEventId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPaymentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentEventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Optional: verify latest payment state before sensitive actions.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latestPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getOxaPayPaymentInformation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listEnabledWorkflowsForMerchant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflow&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;workflows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;triggerMatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trigger_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;conditionsPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;condition_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;latestPayment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createWorkflowExecution&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workflowId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentEventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeWorkflowActions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;latestPayment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMerchant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For sensitive actions such as granting access, sending a license key, creating a payout, or upgrading a SaaS plan, it is wise to verify the latest state with Payment Information before running the final action.&lt;/p&gt;

&lt;p&gt;That extra API call costs a little more latency, but it reduces operational mistakes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connector design
&lt;/h2&gt;

&lt;p&gt;A Payment Automation Studio needs connectors.&lt;/p&gt;

&lt;p&gt;But you do not need to support every platform on day one.&lt;/p&gt;

&lt;p&gt;Start with a small connector set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;li&gt;Telegram&lt;/li&gt;
&lt;li&gt;Discord&lt;/li&gt;
&lt;li&gt;Google Sheets&lt;/li&gt;
&lt;li&gt;webhook / HTTP request&lt;/li&gt;
&lt;li&gt;internal order API&lt;/li&gt;
&lt;li&gt;license key delivery&lt;/li&gt;
&lt;li&gt;CRM update&lt;/li&gt;
&lt;li&gt;support ticket creation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each connector should follow the same interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email.send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;telegram.send_message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendTelegramMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http.request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;renderObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;renderObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;license.assign_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;assignLicenseKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design lets you add connectors gradually without rewriting the workflow engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backfill jobs: do not rely only on webhooks
&lt;/h2&gt;

&lt;p&gt;A serious automation product should not rely only on webhooks.&lt;/p&gt;

&lt;p&gt;Webhooks are event-driven and useful, but a production system should also have a backfill mechanism.&lt;/p&gt;

&lt;p&gt;A backfill job can call Payment History and look for records that were missed, delayed, or not processed correctly.&lt;/p&gt;

&lt;p&gt;Example logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;backfillRecentPayments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMerchant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oxapay_merchant_key_encrypted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.oxapay.com/v1/payment?from_date=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;size=200&amp;amp;sort_by=pay_date&amp;amp;sort_type=desc`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;hasPaymentBeenProcessed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;syntheticEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;storePaymentEventOnce&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment.backfill_detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;signatureValid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`backfill:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;workflowQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run-payment-workflows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;paymentEventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;syntheticEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the difference between a demo and a merchant-grade automation service.&lt;/p&gt;

&lt;p&gt;A demo assumes the webhook always arrives.&lt;/p&gt;

&lt;p&gt;A real product verifies that the business action actually happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The admin dashboard
&lt;/h2&gt;

&lt;p&gt;Do not hide automation failures from merchants.&lt;/p&gt;

&lt;p&gt;A useful dashboard should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recent payment events&lt;/li&gt;
&lt;li&gt;workflow executions&lt;/li&gt;
&lt;li&gt;successful actions&lt;/li&gt;
&lt;li&gt;failed actions&lt;/li&gt;
&lt;li&gt;retry attempts&lt;/li&gt;
&lt;li&gt;unresolved events&lt;/li&gt;
&lt;li&gt;expired invoices&lt;/li&gt;
&lt;li&gt;paid payments without fulfillment&lt;/li&gt;
&lt;li&gt;invalid webhook attempts&lt;/li&gt;
&lt;li&gt;connector failures&lt;/li&gt;
&lt;li&gt;daily summary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dashboard does not need to be beautiful at first.&lt;/p&gt;

&lt;p&gt;It needs to answer operational questions.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did the customer pay?&lt;/p&gt;

&lt;p&gt;Did the workflow run?&lt;/p&gt;

&lt;p&gt;Was the email sent?&lt;/p&gt;

&lt;p&gt;Was the SaaS plan activated?&lt;/p&gt;

&lt;p&gt;Did Telegram access fail?&lt;/p&gt;

&lt;p&gt;Should a support agent intervene?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is what merchants pay for.&lt;/p&gt;




&lt;h2&gt;
  
  
  MVP scope
&lt;/h2&gt;

&lt;p&gt;A realistic MVP should not try to become Zapier for crypto merchants.&lt;/p&gt;

&lt;p&gt;Start narrow.&lt;/p&gt;

&lt;p&gt;Build three templates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Paid invoice → email + merchant notification&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paid invoice → license key delivery&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expired invoice → reminder + support alert&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Support four connectors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;li&gt;Telegram&lt;/li&gt;
&lt;li&gt;Google Sheets&lt;/li&gt;
&lt;li&gt;generic webhook / HTTP request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Support core payment capabilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create invoice&lt;/li&gt;
&lt;li&gt;receive payment webhook&lt;/li&gt;
&lt;li&gt;verify payment information&lt;/li&gt;
&lt;li&gt;store events&lt;/li&gt;
&lt;li&gt;execute workflow&lt;/li&gt;
&lt;li&gt;retry failed action&lt;/li&gt;
&lt;li&gt;show logs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is enough to sell the first version.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production version
&lt;/h2&gt;

&lt;p&gt;After the MVP, the product can grow into a serious automation studio.&lt;/p&gt;

&lt;p&gt;Advanced features could include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;workflow builder UI&lt;/li&gt;
&lt;li&gt;conditional branching&lt;/li&gt;
&lt;li&gt;merchant-specific templates&lt;/li&gt;
&lt;li&gt;connector marketplace&lt;/li&gt;
&lt;li&gt;team permissions&lt;/li&gt;
&lt;li&gt;encrypted credential vault&lt;/li&gt;
&lt;li&gt;workflow versioning&lt;/li&gt;
&lt;li&gt;webhook replay&lt;/li&gt;
&lt;li&gt;manual approval steps&lt;/li&gt;
&lt;li&gt;scheduled workflows&lt;/li&gt;
&lt;li&gt;static address top-up flows&lt;/li&gt;
&lt;li&gt;payout event automation&lt;/li&gt;
&lt;li&gt;advanced finance exports&lt;/li&gt;
&lt;li&gt;customer-facing payment status pages&lt;/li&gt;
&lt;li&gt;alert rules&lt;/li&gt;
&lt;li&gt;SLA monitoring&lt;/li&gt;
&lt;li&gt;audit trails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The direction depends on your target niche.&lt;/p&gt;

&lt;p&gt;A studio for SaaS merchants needs subscription state and plan activation.&lt;/p&gt;

&lt;p&gt;A studio for communities needs access expiry and member removal.&lt;/p&gt;

&lt;p&gt;A studio for finance teams needs reports and reconciliation.&lt;/p&gt;

&lt;p&gt;A studio for agencies needs white-label templates and client management.&lt;/p&gt;

&lt;p&gt;Do not build all versions at once.&lt;/p&gt;

&lt;p&gt;Pick one niche first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revenue model
&lt;/h2&gt;

&lt;p&gt;There is no guaranteed income from this type of product.&lt;/p&gt;

&lt;p&gt;Revenue depends on niche, distribution, trust, implementation quality, support quality, and merchant payment volume.&lt;/p&gt;

&lt;p&gt;But the monetization paths are real.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup fee
&lt;/h3&gt;

&lt;p&gt;You charge for implementing a workflow.&lt;/p&gt;

&lt;p&gt;Example packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic payment alert workflow&lt;/li&gt;
&lt;li&gt;Digital product delivery workflow&lt;/li&gt;
&lt;li&gt;SaaS activation workflow&lt;/li&gt;
&lt;li&gt;CRM sync workflow&lt;/li&gt;
&lt;li&gt;Telegram/Discord access workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is easiest for freelancers and agencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monthly monitoring retainer
&lt;/h3&gt;

&lt;p&gt;You charge to monitor workflows, fix failures, update connectors, and provide support.&lt;/p&gt;

&lt;p&gt;This is often more valuable than the initial setup because merchants want reliability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosted SaaS subscription
&lt;/h3&gt;

&lt;p&gt;You charge merchants monthly for access to your platform.&lt;/p&gt;

&lt;p&gt;Pricing can be based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;number of workflows&lt;/li&gt;
&lt;li&gt;number of executions&lt;/li&gt;
&lt;li&gt;number of connected apps&lt;/li&gt;
&lt;li&gt;number of team members&lt;/li&gt;
&lt;li&gt;advanced logs and reports&lt;/li&gt;
&lt;li&gt;support level&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Template licensing
&lt;/h3&gt;

&lt;p&gt;You sell n8n or Make templates, then upsell setup.&lt;/p&gt;

&lt;p&gt;This can work as a lead generation channel, but template sales alone are usually weaker than implementation and support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agency white-label model
&lt;/h3&gt;

&lt;p&gt;You sell your automation kit to agencies that already serve merchants.&lt;/p&gt;

&lt;p&gt;This can be strong because agencies already have distribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-operation pricing
&lt;/h3&gt;

&lt;p&gt;For advanced merchants, you can charge based on workflow volume.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;per 1,000 workflow executions&lt;/li&gt;
&lt;li&gt;per active merchant&lt;/li&gt;
&lt;li&gt;per payment event processed&lt;/li&gt;
&lt;li&gt;per connected account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This model requires good infrastructure and clear billing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing examples
&lt;/h2&gt;

&lt;p&gt;These are not guarantees. They are positioning examples.&lt;/p&gt;

&lt;p&gt;A simple implementation might be priced as a small fixed setup project.&lt;/p&gt;

&lt;p&gt;A more advanced workflow with custom API calls, retry handling, reporting, and dashboard visibility can justify a higher project fee.&lt;/p&gt;

&lt;p&gt;A managed automation package can be priced monthly because the developer is not just delivering code. They are maintaining operational workflows.&lt;/p&gt;

&lt;p&gt;A possible packaging model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starter Automation
- 1 payment workflow
- email or Telegram notification
- basic logs
- fixed setup fee

Growth Automation
- 3 to 5 workflows
- CRM or spreadsheet sync
- retry handling
- monthly monitoring

Operations Automation
- custom workflow logic
- dashboard
- support queue
- backfill job
- monthly retainer

Platform Plan
- hosted workflow studio
- multiple connectors
- execution logs
- team access
- usage-based pricing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pricing should be tied to the business value of the workflow.&lt;/p&gt;

&lt;p&gt;A workflow that sends a Telegram alert is not worth the same as a workflow that activates paid SaaS access or prevents fulfillment errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  A 21-day build plan
&lt;/h2&gt;

&lt;p&gt;Here is a practical build plan for the first version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Days 1–3: Define the niche
&lt;/h3&gt;

&lt;p&gt;Choose one customer type.&lt;/p&gt;

&lt;p&gt;Good starting niches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;digital product sellers&lt;/li&gt;
&lt;li&gt;SaaS founders&lt;/li&gt;
&lt;li&gt;Telegram communities&lt;/li&gt;
&lt;li&gt;agencies&lt;/li&gt;
&lt;li&gt;hosting/VPN sellers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write down the first three workflows they need.&lt;/p&gt;

&lt;p&gt;Do not build a general automation platform yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Days 4–6: Build invoice creation and webhook ingestion
&lt;/h3&gt;

&lt;p&gt;Implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice creation&lt;/li&gt;
&lt;li&gt;order mapping&lt;/li&gt;
&lt;li&gt;webhook receiver&lt;/li&gt;
&lt;li&gt;HMAC validation&lt;/li&gt;
&lt;li&gt;raw event storage&lt;/li&gt;
&lt;li&gt;normalized event storage&lt;/li&gt;
&lt;li&gt;idempotency key&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 7–9: Build workflow runner
&lt;/h3&gt;

&lt;p&gt;Implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;condition matching&lt;/li&gt;
&lt;li&gt;action registry&lt;/li&gt;
&lt;li&gt;job queue&lt;/li&gt;
&lt;li&gt;retry logic&lt;/li&gt;
&lt;li&gt;execution logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with two actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send email&lt;/li&gt;
&lt;li&gt;send Telegram message&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 10–12: Build the first real template
&lt;/h3&gt;

&lt;p&gt;Build one complete workflow for one merchant problem.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Paid invoice → verify payment → send license key → notify merchant → log delivery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Days 13–15: Build dashboard basics
&lt;/h3&gt;

&lt;p&gt;Show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment events&lt;/li&gt;
&lt;li&gt;workflow executions&lt;/li&gt;
&lt;li&gt;failed actions&lt;/li&gt;
&lt;li&gt;retry status&lt;/li&gt;
&lt;li&gt;recent payments&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 16–18: Add backfill and error handling
&lt;/h3&gt;

&lt;p&gt;Implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment History backfill&lt;/li&gt;
&lt;li&gt;failed workflow retry&lt;/li&gt;
&lt;li&gt;unresolved event queue&lt;/li&gt;
&lt;li&gt;invalid webhook log&lt;/li&gt;
&lt;li&gt;alert on repeated failure&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 19–21: Package and sell
&lt;/h3&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one landing page&lt;/li&gt;
&lt;li&gt;one demo video&lt;/li&gt;
&lt;li&gt;three workflow packages&lt;/li&gt;
&lt;li&gt;one technical case study&lt;/li&gt;
&lt;li&gt;one onboarding checklist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then sell it as a service before trying to scale it as SaaS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical risks
&lt;/h2&gt;

&lt;p&gt;Payment automation touches business-critical flows.&lt;/p&gt;

&lt;p&gt;You should treat it seriously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate events
&lt;/h3&gt;

&lt;p&gt;Payment callbacks can be retried or delivered more than once.&lt;/p&gt;

&lt;p&gt;Use idempotency keys and unique constraints.&lt;/p&gt;

&lt;p&gt;Never send the same license key twice because the same event arrived twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Out-of-order events
&lt;/h3&gt;

&lt;p&gt;A later status may arrive before an earlier status in some distributed systems.&lt;/p&gt;

&lt;p&gt;Your internal state machine should prevent moving from a final state back to a weaker state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slow third-party APIs
&lt;/h3&gt;

&lt;p&gt;Email services, CRMs, Telegram, Discord, and ecommerce APIs can fail or rate limit you.&lt;/p&gt;

&lt;p&gt;Use background jobs and retries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sensitive credentials
&lt;/h3&gt;

&lt;p&gt;Merchant API keys, webhook secrets, bot tokens, SMTP credentials, and CRM tokens must be encrypted.&lt;/p&gt;

&lt;p&gt;Do not store them as plain text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong workflow execution
&lt;/h3&gt;

&lt;p&gt;A workflow that runs for the wrong order can create real damage.&lt;/p&gt;

&lt;p&gt;Verify &lt;code&gt;track_id&lt;/code&gt;, &lt;code&gt;order_id&lt;/code&gt;, merchant ownership, payment status, and expected amount before sensitive actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual overrides
&lt;/h3&gt;

&lt;p&gt;Merchants need a way to resolve exceptions.&lt;/p&gt;

&lt;p&gt;Do not pretend every workflow can be fully automatic.&lt;/p&gt;

&lt;p&gt;Some events should go to a review queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compliance and policy boundaries
&lt;/h3&gt;

&lt;p&gt;You are not the merchant’s legal, tax, or compliance team.&lt;/p&gt;

&lt;p&gt;Your product should provide records and controls, but merchants are responsible for their own legal and accounting requirements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security checklist
&lt;/h2&gt;

&lt;p&gt;Before selling this to real merchants, implement these basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use HTTPS for all webhook endpoints.&lt;/li&gt;
&lt;li&gt;Verify webhook signatures or HMAC according to provider docs.&lt;/li&gt;
&lt;li&gt;Store raw webhook payloads for audit purposes.&lt;/li&gt;
&lt;li&gt;Use idempotency keys for every event.&lt;/li&gt;
&lt;li&gt;Encrypt all merchant credentials.&lt;/li&gt;
&lt;li&gt;Separate merchant data by tenant.&lt;/li&gt;
&lt;li&gt;Restrict dashboard access with strong authentication.&lt;/li&gt;
&lt;li&gt;Add audit logs for workflow changes.&lt;/li&gt;
&lt;li&gt;Do not expose merchant API keys to frontend code.&lt;/li&gt;
&lt;li&gt;Use queues for workflow execution.&lt;/li&gt;
&lt;li&gt;Add rate limits to public endpoints.&lt;/li&gt;
&lt;li&gt;Add alerting for repeated workflow failures.&lt;/li&gt;
&lt;li&gt;Verify latest payment state before sensitive actions.&lt;/li&gt;
&lt;li&gt;Keep payout automation behind stricter permissions.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What makes this different from a simple webhook script?
&lt;/h2&gt;

&lt;p&gt;A simple webhook script says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If status is Paid, do X.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Payment Automation Studio says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive event
→ verify event
→ store event
→ deduplicate event
→ verify latest payment state
→ match workflow
→ execute actions
→ retry failures
→ log everything
→ notify humans when needed
→ backfill missing events
→ produce reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the difference between a script and a product.&lt;/p&gt;

&lt;p&gt;Scripts are useful.&lt;/p&gt;

&lt;p&gt;Products are sellable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Best first niche
&lt;/h2&gt;

&lt;p&gt;If I were building this from scratch, I would not start with all merchants.&lt;/p&gt;

&lt;p&gt;I would start with one of these:&lt;/p&gt;

&lt;h3&gt;
  
  
  Digital product sellers
&lt;/h3&gt;

&lt;p&gt;They have a clear pain: payment should trigger delivery.&lt;/p&gt;

&lt;p&gt;The workflow is easy to understand and easy to demonstrate.&lt;/p&gt;

&lt;h3&gt;
  
  
  SaaS founders
&lt;/h3&gt;

&lt;p&gt;They need plan activation and account state updates.&lt;/p&gt;

&lt;p&gt;The problem is valuable, but integrations vary more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Telegram or Discord communities
&lt;/h3&gt;

&lt;p&gt;They need paid access automation.&lt;/p&gt;

&lt;p&gt;The value is clear because manual access management is annoying.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agencies
&lt;/h3&gt;

&lt;p&gt;They already have merchant clients.&lt;/p&gt;

&lt;p&gt;Selling a white-label automation package to agencies can be easier than acquiring merchants one by one.&lt;/p&gt;

&lt;p&gt;For most developers, the safest first version is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Paid invoice → digital delivery + merchant notification + execution log.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then expand.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to position the service
&lt;/h2&gt;

&lt;p&gt;Do not market it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I connect OxaPay to your website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds like a one-time technical task.&lt;/p&gt;

&lt;p&gt;Market it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I automate what happens after your crypto payment is created, confirmed, expired, or paid out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I build payment workflows that turn crypto payment events into business actions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I help crypto merchants reduce manual payment work with invoice, webhook, fulfillment, CRM, reporting, and notification automation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This positioning allows higher-value pricing because the merchant is not buying an integration.&lt;/p&gt;

&lt;p&gt;They are buying operational automation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example landing page promise
&lt;/h2&gt;

&lt;p&gt;A simple developer landing page could say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Crypto Payment Automation for Digital Merchants

I help merchants automate what happens after crypto payments:
- deliver products automatically
- activate SaaS plans
- update CRMs and spreadsheets
- notify teams in Telegram or Discord
- recover expired invoices
- generate payment reports

Built with OxaPay invoices, webhooks, payment history, and automation workflows.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much more specific than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I integrate crypto payments.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specific sells better.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final developer takeaway
&lt;/h2&gt;

&lt;p&gt;The business opportunity is not simply accepting crypto payments.&lt;/p&gt;

&lt;p&gt;The opportunity is building the operational layer around those payments.&lt;/p&gt;

&lt;p&gt;A merchant does not just need an invoice.&lt;/p&gt;

&lt;p&gt;They need the invoice to trigger delivery, access, reporting, support, CRM updates, finance records, reminders, and payout notifications.&lt;/p&gt;

&lt;p&gt;OxaPay can provide the payment infrastructure primitives: invoice creation, webhooks, payment lookup, payment history, static addresses, payouts, SDKs, n8n workflows, and Make modules.&lt;/p&gt;

&lt;p&gt;A developer can turn those primitives into workflow products.&lt;/p&gt;

&lt;p&gt;Start small.&lt;/p&gt;

&lt;p&gt;Pick one niche.&lt;/p&gt;

&lt;p&gt;Build three workflows.&lt;/p&gt;

&lt;p&gt;Add logs, retries, and backfill.&lt;/p&gt;

&lt;p&gt;Sell it as a managed service before turning it into a full SaaS.&lt;/p&gt;

&lt;p&gt;That is how a Payment Automation Studio becomes more than a template library.&lt;/p&gt;

&lt;p&gt;It becomes a real business around crypto payment operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Generate Static Address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/generate-payout" rel="noopener noreferrer"&gt;OxaPay Generate Payout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;OxaPay Laravel SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.make.com/oxapay-crypto-pay-gtw" rel="noopener noreferrer"&gt;OxaPay Make App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/OxaPay/n8n-nodes-oxapay" rel="noopener noreferrer"&gt;OxaPay n8n Community Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/" rel="noopener noreferrer"&gt;n8n Webhook Node&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>webdev</category>
      <category>crypto</category>
      <category>api</category>
    </item>
    <item>
      <title>Build a Crypto Payment Reconciliation Tool for Merchants</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Wed, 15 Jul 2026 10:36:38 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-crypto-payment-reconciliation-tool-for-merchants-1eo</link>
      <guid>https://dev.to/kevins1988/build-a-crypto-payment-reconciliation-tool-for-merchants-1eo</guid>
      <description>&lt;p&gt;Most developers think about crypto payments as a checkout problem.&lt;/p&gt;

&lt;p&gt;Create an invoice. Show a payment link. Wait for a callback. Mark the order as paid.&lt;/p&gt;

&lt;p&gt;That is enough for a demo.&lt;/p&gt;

&lt;p&gt;It is not enough for a real merchant.&lt;/p&gt;

&lt;p&gt;Once a business starts receiving meaningful crypto payment volume, the hard question is no longer only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did the customer pay?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The harder questions are:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which order does this payment belong to?&lt;/p&gt;

&lt;p&gt;Was the amount exact, underpaid, late, duplicated, manually accepted, refunded, or still waiting?&lt;/p&gt;

&lt;p&gt;Did the order system, support team, finance team, and payment provider all agree on the same state?&lt;/p&gt;

&lt;p&gt;Can the merchant prove what happened when a customer opens a support ticket?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the opportunity.&lt;/p&gt;

&lt;p&gt;A developer can build a &lt;strong&gt;crypto payment reconciliation tool&lt;/strong&gt; for merchants.&lt;/p&gt;

&lt;p&gt;Not another crypto checkout.&lt;/p&gt;

&lt;p&gt;Not another wallet dashboard.&lt;/p&gt;

&lt;p&gt;A tool that connects payment events, invoices, orders, customers, transactions, support records, reports, and exceptions into one operational system.&lt;/p&gt;

&lt;p&gt;In this article, I will use &lt;strong&gt;OxaPay&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The business opportunity
&lt;/h2&gt;

&lt;p&gt;Merchants do not usually wake up thinking they need a reconciliation tool.&lt;/p&gt;

&lt;p&gt;They discover the need when payment volume creates operational friction.&lt;/p&gt;

&lt;p&gt;At low volume, a merchant can manually check payments. At higher volume, manual checking starts to break down.&lt;/p&gt;

&lt;p&gt;A customer says they paid, but the order is still pending.&lt;/p&gt;

&lt;p&gt;An invoice expires, but the blockchain transaction arrives late.&lt;/p&gt;

&lt;p&gt;A customer pays less than expected.&lt;/p&gt;

&lt;p&gt;A support agent sees an order ID, but the payment provider stores a track ID.&lt;/p&gt;

&lt;p&gt;The finance team exports payment history, but the ecommerce platform has a different number of paid orders.&lt;/p&gt;

&lt;p&gt;The developer who solves this problem is not selling “API integration.”&lt;/p&gt;

&lt;p&gt;They are selling operational confidence.&lt;/p&gt;

&lt;p&gt;A reconciliation product helps merchants answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which orders are fully paid?&lt;/li&gt;
&lt;li&gt;Which paid orders were not fulfilled?&lt;/li&gt;
&lt;li&gt;Which payments are unresolved?&lt;/li&gt;
&lt;li&gt;Which invoices are expired but have transaction activity?&lt;/li&gt;
&lt;li&gt;Which payments are underpaid or manually accepted?&lt;/li&gt;
&lt;li&gt;Which customers need support follow-up?&lt;/li&gt;
&lt;li&gt;Which payment records should be exported for finance?&lt;/li&gt;
&lt;li&gt;Which webhook events were received, ignored, duplicated, or failed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much more valuable than simply adding a payment button.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who would pay for this?
&lt;/h2&gt;

&lt;p&gt;A crypto payment reconciliation tool is not for every merchant.&lt;/p&gt;

&lt;p&gt;A small seller with five payments per month probably does not need it.&lt;/p&gt;

&lt;p&gt;The best customers are merchants where payment confusion has a real cost.&lt;/p&gt;

&lt;p&gt;Good targets include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Digital product stores&lt;/li&gt;
&lt;li&gt;SaaS platforms&lt;/li&gt;
&lt;li&gt;Hosting companies&lt;/li&gt;
&lt;li&gt;VPN and reseller panels&lt;/li&gt;
&lt;li&gt;Online course platforms&lt;/li&gt;
&lt;li&gt;Telegram or Discord paid communities&lt;/li&gt;
&lt;li&gt;Software license sellers&lt;/li&gt;
&lt;li&gt;Cross-border service businesses&lt;/li&gt;
&lt;li&gt;Agencies that manage multiple merchant clients&lt;/li&gt;
&lt;li&gt;Marketplaces or creator platforms that receive many payments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common pattern is simple: they receive payments, deliver something after payment, and need proof that payment state and business state match.&lt;/p&gt;

&lt;p&gt;This product becomes more valuable when the merchant has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;many orders&lt;/li&gt;
&lt;li&gt;multiple payment methods&lt;/li&gt;
&lt;li&gt;manual support tickets&lt;/li&gt;
&lt;li&gt;paid but undelivered orders&lt;/li&gt;
&lt;li&gt;expired invoice disputes&lt;/li&gt;
&lt;li&gt;multiple currencies or networks&lt;/li&gt;
&lt;li&gt;finance reporting needs&lt;/li&gt;
&lt;li&gt;operational staff who are not developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you are building
&lt;/h2&gt;

&lt;p&gt;A crypto payment reconciliation tool sits between the merchant’s business system and the payment provider.&lt;/p&gt;

&lt;p&gt;It receives payment events, stores normalized payment records, compares those records with merchant orders, detects mismatches, and gives humans a clear queue of exceptions.&lt;/p&gt;

&lt;p&gt;The architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The product has four main responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingest payment events&lt;/strong&gt; from webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill and verify payment records&lt;/strong&gt; using payment information and payment history APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match payments to merchant orders&lt;/strong&gt; using identifiers such as &lt;code&gt;order_id&lt;/code&gt;, &lt;code&gt;track_id&lt;/code&gt;, amount, currency, email, address, and time window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface exceptions&lt;/strong&gt; that require action.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most important idea is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reconciliation is not just storing payment status. Reconciliation is proving that payment state, order state, fulfillment state, and finance state agree.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why OxaPay has the right primitives for this
&lt;/h2&gt;

&lt;p&gt;A reconciliation tool needs more than a checkout URL.&lt;/p&gt;

&lt;p&gt;It needs persistent identifiers, payment status, callbacks, transaction metadata, history endpoints, and the ability to query records again when the webhook stream is incomplete.&lt;/p&gt;

&lt;p&gt;OxaPay provides several useful primitives for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate Invoice
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Generate Invoice&lt;/strong&gt; 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 &lt;code&gt;order_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;order_id&lt;/code&gt; is important. It lets the developer connect the payment session to the merchant’s internal order record from the beginning.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook
&lt;/h3&gt;

&lt;p&gt;OxaPay webhooks send JSON notifications to a merchant-defined &lt;code&gt;callback_url&lt;/code&gt; when payment status changes. The docs explain that the merchant should wait for the final &lt;code&gt;paid&lt;/code&gt; state rather than treating the earlier &lt;code&gt;paying&lt;/code&gt; status as final.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;ok&lt;/code&gt;, and retries webhook delivery up to five times with increasing delays.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment Information
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Payment Information&lt;/strong&gt; endpoint lets a developer retrieve details for a specific payment using its &lt;code&gt;track_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is useful for support investigation, webhook recovery, and periodic verification.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment History
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Payment History&lt;/strong&gt; endpoint returns account payment records and supports filtering by criteria such as &lt;code&gt;track_id&lt;/code&gt;, type, status, currency, network, address, date window, amount range, sorting, page, and size.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment Status Table
&lt;/h3&gt;

&lt;p&gt;OxaPay documents payment statuses such as &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;paying&lt;/code&gt;, &lt;code&gt;paid&lt;/code&gt;, &lt;code&gt;manual_accept&lt;/code&gt;, &lt;code&gt;underpaid&lt;/code&gt;, &lt;code&gt;refunding&lt;/code&gt;, &lt;code&gt;refunded&lt;/code&gt;, and &lt;code&gt;expired&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A reconciliation tool should map these provider statuses to internal operational states.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-status-table" rel="noopener noreferrer"&gt;OxaPay Payment Status Table&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Static Address
&lt;/h3&gt;

&lt;p&gt;For some merchants, invoice-based payments are not the only model. OxaPay also supports static addresses linked to a unique &lt;code&gt;track_id&lt;/code&gt;, with callback support for payments made to that address.&lt;/p&gt;

&lt;p&gt;This can matter for recurring deposits, account top-ups, wallets, or customer-specific payment addresses.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Generate Static Address&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The core product: from payment records to reconciliation cases
&lt;/h2&gt;

&lt;p&gt;The tool should not show merchants a raw table of transactions and call it a day.&lt;/p&gt;

&lt;p&gt;The product should turn payment data into &lt;strong&gt;cases&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A case is a business question that needs resolution.&lt;/p&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the product becomes valuable.&lt;/p&gt;

&lt;p&gt;A raw payment dashboard is easy to ignore.&lt;/p&gt;

&lt;p&gt;A prioritized exception queue is useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  MVP scope
&lt;/h2&gt;

&lt;p&gt;Do not start by building a full finance platform.&lt;/p&gt;

&lt;p&gt;Start with a focused MVP.&lt;/p&gt;

&lt;p&gt;A strong first version could include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merchant account connection&lt;/li&gt;
&lt;li&gt;Invoice creation wrapper&lt;/li&gt;
&lt;li&gt;Webhook receiver&lt;/li&gt;
&lt;li&gt;HMAC validation&lt;/li&gt;
&lt;li&gt;Payment event log&lt;/li&gt;
&lt;li&gt;Order-to-payment matching&lt;/li&gt;
&lt;li&gt;Reconciliation status engine&lt;/li&gt;
&lt;li&gt;Exception queue&lt;/li&gt;
&lt;li&gt;Manual resolution notes&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;li&gt;Daily summary email or Telegram alert&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The MVP should answer five questions well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which orders are paid?&lt;/li&gt;
&lt;li&gt;Which paid orders are not fulfilled?&lt;/li&gt;
&lt;li&gt;Which payments need review?&lt;/li&gt;
&lt;li&gt;Which webhook events were missed or duplicated?&lt;/li&gt;
&lt;li&gt;What should support or finance do next?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is enough to sell to an early merchant.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data model
&lt;/h2&gt;

&lt;p&gt;A reconciliation tool needs its own normalized database.&lt;/p&gt;

&lt;p&gt;Do not rely only on the ecommerce platform database.&lt;/p&gt;

&lt;p&gt;Do not rely only on the payment provider dashboard.&lt;/p&gt;

&lt;p&gt;You need a middle layer that can compare both sides.&lt;/p&gt;

&lt;p&gt;Here is a practical schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_merchant_api_key_encrypted&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;webhook_secret_hint&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchant_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;external_order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expected_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expected_currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;fulfillment_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'not_fulfilled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchant_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'oxapay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;requested_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;requested_currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;internal_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;invoice_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_provider_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_transactions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_session_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payment_sessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;tx_hash&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tx_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confirmations&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_tx_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'oxapay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hmac_valid&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload_hash&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reconciliation_cases&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchant_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payment_session_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payment_sessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;case_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;recommended_action&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;assigned_to&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resolved_by&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resolution_note&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;resolved_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema is intentionally operational.&lt;/p&gt;

&lt;p&gt;It is not only for storing payments.&lt;/p&gt;

&lt;p&gt;It is for investigating mismatches.&lt;/p&gt;




&lt;h2&gt;
  
  
  Internal payment state machine
&lt;/h2&gt;

&lt;p&gt;Provider statuses are not always the same as business statuses.&lt;/p&gt;

&lt;p&gt;A merchant does not only care whether a payment is &lt;code&gt;paid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They care whether the order is safe to deliver, whether support should intervene, and whether finance should include it in reporting.&lt;/p&gt;

&lt;p&gt;You can map OxaPay statuses into internal states.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not trigger fulfillment on &lt;code&gt;paying&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In OxaPay’s webhook documentation, &lt;code&gt;paying&lt;/code&gt; is an earlier status and &lt;code&gt;paid&lt;/code&gt; is the status to wait for before treating the payment as confirmed.&lt;/p&gt;

&lt;p&gt;The reconciliation tool should make that distinction very visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Webhook receiver with HMAC validation
&lt;/h2&gt;

&lt;p&gt;Webhook ingestion is the first technical building block.&lt;/p&gt;

&lt;p&gt;This example uses Node.js and Express.&lt;/p&gt;

&lt;p&gt;The important part is that HMAC validation must use the &lt;strong&gt;raw request body&lt;/strong&gt;, not a parsed and re-serialized JSON object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Keep raw body for HMAC verification.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyOxaPayHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calculated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;calculated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// In a multi-merchant product, resolve the merchant carefully.&lt;/span&gt;
  &lt;span class="c1"&gt;// You might map by callback URL token, track_id lookup, or tenant route.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findMerchantFromWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown merchant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmacValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyOxaPayHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oxapayMerchantApiKey&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payloadText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payloadHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payloadText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payloadText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveWebhookEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;providerTrackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;eventStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;hmacValid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hmacValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Store the event for audit, but do not process it.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enqueuePaymentEventProcessing&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;providerTrackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payloadHash&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// OxaPay expects HTTP 200 with body "ok" for successful webhook delivery.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A production implementation should process the webhook asynchronously.&lt;/p&gt;

&lt;p&gt;The endpoint should validate, store, enqueue, and return quickly.&lt;/p&gt;

&lt;p&gt;Do not run long reconciliation jobs inside the webhook request.&lt;/p&gt;




&lt;h2&gt;
  
  
  Idempotency and duplicate callbacks
&lt;/h2&gt;

&lt;p&gt;Webhook retries are normal.&lt;/p&gt;

&lt;p&gt;Network failures happen.&lt;/p&gt;

&lt;p&gt;Your endpoint may receive the same event more than once.&lt;/p&gt;

&lt;p&gt;Your reconciliation tool must be idempotent.&lt;/p&gt;

&lt;p&gt;Use a unique constraint on a stable event hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveWebhookEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isUniqueViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;merchantId_payloadHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;payloadHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payloadHash&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also make payment state transitions idempotent.&lt;/p&gt;

&lt;p&gt;If the local session is already &lt;code&gt;paid_confirmed&lt;/code&gt;, another &lt;code&gt;paid&lt;/code&gt; webhook should not trigger fulfillment twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyPaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextInternalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapProviderStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;nextInternalStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid_confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;providerStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;internalStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextInternalStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextInternalStatus&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not just a code quality issue.&lt;/p&gt;

&lt;p&gt;It protects the merchant from duplicate fulfillment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backfill with Payment History
&lt;/h2&gt;

&lt;p&gt;A serious reconciliation tool should not trust webhooks as the only source of truth.&lt;/p&gt;

&lt;p&gt;Webhooks can fail.&lt;/p&gt;

&lt;p&gt;Servers can be down.&lt;/p&gt;

&lt;p&gt;Firewalls can block callbacks.&lt;/p&gt;

&lt;p&gt;A bug can process an event incorrectly.&lt;/p&gt;

&lt;p&gt;That is why you also need periodic provider-side backfill.&lt;/p&gt;

&lt;p&gt;OxaPay’s Payment History endpoint supports date filtering, status filtering, payment type filtering, pagination, sorting, and amount filtering.&lt;/p&gt;

&lt;p&gt;A scheduled job can pull recent records and compare them with local state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchOxaPayPaymentHistory&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;to_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;sort_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pay_date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sort_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.oxapay.com/v1/payment?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OxaPay history request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple backfill strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every 15 minutes:
  Pull payments from the last 2 hours.
  Upsert payment sessions by track_id.
  Compare provider status with local status.
  Create case if provider is paid but local order is unpaid.

Every 24 hours:
  Pull payments from the previous day.
  Compare total paid amount with merchant order totals.
  Generate daily reconciliation report.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where your tool becomes more robust than a basic webhook integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Payment Information lookup for support
&lt;/h2&gt;

&lt;p&gt;The Payment Information endpoint is useful when support needs to investigate one specific payment.&lt;/p&gt;

&lt;p&gt;For example, a customer writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I paid, but my order is still pending.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The support agent can search by order ID, customer email, track ID, or transaction hash.&lt;/p&gt;

&lt;p&gt;If the tool has the OxaPay &lt;code&gt;track_id&lt;/code&gt;, it can fetch the latest payment details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchPaymentInformation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.oxapay.com/v1/payment/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Payment lookup failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result can update the support timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order created
Invoice generated
Customer selected currency
Payment status changed to paying
Transaction confirmation detected
Payment status changed to paid
Fulfillment job failed
Support case opened
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of view support teams actually need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Matching logic
&lt;/h2&gt;

&lt;p&gt;Reconciliation is mostly matching.&lt;/p&gt;

&lt;p&gt;You are matching provider-side records to merchant-side records.&lt;/p&gt;

&lt;p&gt;The strongest matching key is &lt;code&gt;order_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When creating invoices, always send the merchant’s internal order ID as &lt;code&gt;order_id&lt;/code&gt;. This gives your reconciliation engine a clean join key.&lt;/p&gt;

&lt;p&gt;But real systems need fallback logic.&lt;/p&gt;

&lt;p&gt;Recommended matching priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Exact provider track_id already linked to order
2. Exact order_id from provider payload
3. Exact external_order_id stored in local metadata
4. Customer email + amount + narrow time window
5. Static address track_id assigned to customer account
6. Transaction hash manually attached by support
7. Manual review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not over-automate weak matches.&lt;/p&gt;

&lt;p&gt;If a match is not strong enough, create a reconciliation case instead of silently marking an order paid.&lt;/p&gt;

&lt;p&gt;Example matching function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findMatchingOrder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchantOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;merchantId_externalOrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;externalOrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchantOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;expectedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;expectedCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email_amount_time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_safe_match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Weak matching should require human approval.&lt;/p&gt;

&lt;p&gt;That is a product feature, not a limitation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciliation cases
&lt;/h2&gt;

&lt;p&gt;The product should create clear, actionable cases.&lt;/p&gt;

&lt;p&gt;Here are useful case types.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Paid but not fulfilled
&lt;/h3&gt;

&lt;p&gt;Provider says payment is paid, but merchant order has not been fulfilled.&lt;/p&gt;

&lt;p&gt;Recommended action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check fulfillment job logs. If payment is confirmed and order is valid, trigger fulfillment manually or re-run automation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Fulfilled but not paid
&lt;/h3&gt;

&lt;p&gt;Merchant system says the order was fulfilled, but provider status is not paid.&lt;/p&gt;

&lt;p&gt;Recommended action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review automation logs. Possible duplicate fulfillment, manual override, or incorrect order state.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Underpaid payment
&lt;/h3&gt;

&lt;p&gt;Provider status indicates underpayment or the received amount is below expected amount.&lt;/p&gt;

&lt;p&gt;Recommended action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ask customer to complete remaining payment, manually accept if policy allows, or cancel order.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Expired invoice with customer claim
&lt;/h3&gt;

&lt;p&gt;Invoice expired, but the customer says they paid.&lt;/p&gt;

&lt;p&gt;Recommended action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Look up payment information by track_id, address, tx_hash, amount, and time window.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Unknown payment
&lt;/h3&gt;

&lt;p&gt;A payment exists in provider history, but no local order matches it.&lt;/p&gt;

&lt;p&gt;Recommended action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review order_id, email, amount, static address assignment, and support tickets.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Duplicate webhook
&lt;/h3&gt;

&lt;p&gt;Same event was received multiple times.&lt;/p&gt;

&lt;p&gt;Recommended action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No merchant action needed if idempotency prevented duplicate fulfillment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Invalid webhook signature
&lt;/h3&gt;

&lt;p&gt;Webhook failed HMAC validation.&lt;/p&gt;

&lt;p&gt;Recommended action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not process payment state. Investigate source, endpoint exposure, and secret configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Status mismatch
&lt;/h3&gt;

&lt;p&gt;Provider and local database disagree.&lt;/p&gt;

&lt;p&gt;Recommended action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refresh payment information by track_id and update local state only if provider response is verified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These cases are the product.&lt;/p&gt;

&lt;p&gt;They turn raw payment data into operational work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciliation engine example
&lt;/h2&gt;

&lt;p&gt;Here is a simplified reconciliation worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcilePayment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerTrackId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;merchantId_provider_providerTrackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;providerTrackId&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_session_missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No local payment session found for track_id &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;providerTrackId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recommendedAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fetch payment information from provider and attempt safe matching.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid_confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid_payment_without_order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment is confirmed but no local order is linked.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recommendedAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Review order_id, customer email, amount, and time window.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid_confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fulfillmentStatus&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid_not_fulfilled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;externalOrderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is paid but not fulfilled.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recommendedAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Re-run fulfillment job or review fulfillment logs.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;needs_review_underpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;underpaid_payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment appears underpaid and needs merchant review.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recommendedAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Apply merchant policy: request remaining amount, manually accept, or cancel.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;fulfillmentStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalStatus&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid_confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;paymentSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;caseType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fulfilled_not_paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;externalOrderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; was fulfilled before confirmed payment.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;recommendedAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Review automation logic and prevent future fulfillment before paid status.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, each case type should have deduplication rules.&lt;/p&gt;

&lt;p&gt;You do not want to create the same case every time a job runs.&lt;/p&gt;

&lt;p&gt;Use a unique case key such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;merchant_id + case_type + order_id + payment_session_id + open_status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The dashboard
&lt;/h2&gt;

&lt;p&gt;The dashboard should be built for operations, not developers.&lt;/p&gt;

&lt;p&gt;A merchant support agent should understand the screen in seconds.&lt;/p&gt;

&lt;p&gt;Useful dashboard sections:&lt;/p&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total paid today
Orders paid but not fulfilled
Open reconciliation cases
Underpaid payments
Expired invoices with activity
Webhook failures
Daily payment total
Provider/local mismatch count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Exception queue
&lt;/h3&gt;

&lt;p&gt;Columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Severity
Case type
Order ID
Track ID
Customer
Amount
Currency
Provider status
Order status
Fulfillment status
Age
Assigned agent
Recommended action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Payment timeline
&lt;/h3&gt;

&lt;p&gt;For each order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order created
Invoice generated
Webhook received: waiting
Webhook received: paying
Provider lookup: paid
Fulfillment job started
Fulfillment completed
Support note added
Case resolved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Finance export
&lt;/h3&gt;

&lt;p&gt;Export fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Date
Order ID
Track ID
Payment type
Payment status
Order status
Fulfillment status
Requested amount
Paid amount
Currency
Network
Transaction hash
Customer email
Resolution status
Support note
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Webhook health
&lt;/h3&gt;

&lt;p&gt;Show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;received events
valid HMAC count
invalid HMAC count
duplicate events
processing failures
last webhook timestamp
retry-sensitive endpoints
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially valuable because many merchants only notice webhook problems when customers complain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Alerts
&lt;/h2&gt;

&lt;p&gt;Do not send alerts for everything.&lt;/p&gt;

&lt;p&gt;Alert fatigue kills operational tools.&lt;/p&gt;

&lt;p&gt;Good alerts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Paid order not fulfilled for more than 5 minutes
Invalid webhook signature received
Provider shows paid but local order is unpaid
High number of expired invoices in last hour
Backfill found paid payment missing locally
Underpaid payment above threshold
Webhook processing queue delayed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bad alerts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every invoice created
Every waiting payment
Every duplicate webhook
Every normal paid payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reconciliation tool should reduce noise, not create more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Static address reconciliation
&lt;/h2&gt;

&lt;p&gt;Static addresses create a different reconciliation problem.&lt;/p&gt;

&lt;p&gt;With invoice payments, each payment session usually has a specific expected amount and order ID.&lt;/p&gt;

&lt;p&gt;With static addresses, the same address may be associated with a customer, account, or deposit flow. Payments can arrive at different times and amounts.&lt;/p&gt;

&lt;p&gt;OxaPay’s static address endpoint links an address to a &lt;code&gt;track_id&lt;/code&gt; and supports callback notifications when payments are made to that address.&lt;/p&gt;

&lt;p&gt;For reconciliation, static address flows need extra rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assign static address to customer or account
Store track_id and address mapping
Ingest payment callback
Match payment to account balance or deposit record
Detect unknown amount
Detect duplicate tx_hash
Detect stale address assignment
Handle revoked/inactive addresses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static address reconciliation is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;account top-ups&lt;/li&gt;
&lt;li&gt;wallet-like merchant balances&lt;/li&gt;
&lt;li&gt;customer deposit accounts&lt;/li&gt;
&lt;li&gt;recurring customer deposits&lt;/li&gt;
&lt;li&gt;B2B clients with repeated payments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it needs stricter tracking.&lt;/p&gt;

&lt;p&gt;Do not use static addresses as a shortcut if the merchant really needs order-level invoice reconciliation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revenue model for developers
&lt;/h2&gt;

&lt;p&gt;This can be a real business, but the monetization depends on positioning.&lt;/p&gt;

&lt;p&gt;Do not sell it as “I built a crypto dashboard.”&lt;/p&gt;

&lt;p&gt;Sell it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I reduce payment support problems, fulfillment mistakes, and finance reporting gaps for merchants accepting crypto payments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Possible revenue models:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Setup fee
&lt;/h3&gt;

&lt;p&gt;A one-time fee to connect the merchant’s store, OxaPay account, webhook endpoint, order database, and dashboard.&lt;/p&gt;

&lt;p&gt;Best for freelancers and agencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Monthly monitoring retainer
&lt;/h3&gt;

&lt;p&gt;The developer monitors webhook health, open cases, daily mismatch reports, and operational alerts.&lt;/p&gt;

&lt;p&gt;Best for service businesses.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SaaS subscription
&lt;/h3&gt;

&lt;p&gt;The developer sells the tool as a self-serve or semi-managed product.&lt;/p&gt;

&lt;p&gt;Best if the product is focused on a niche, such as WooCommerce digital stores, hosting providers, or paid communities.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Finance/reporting add-on
&lt;/h3&gt;

&lt;p&gt;The developer charges extra for CSV exports, daily reports, monthly summaries, and custom accounting formats.&lt;/p&gt;

&lt;p&gt;Best for merchants with operations or finance teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Support operations package
&lt;/h3&gt;

&lt;p&gt;The developer sells support workflows, case queues, customer response templates, and payment investigation tools.&lt;/p&gt;

&lt;p&gt;Best for merchants with many customer tickets.&lt;/p&gt;

&lt;p&gt;Revenue should be framed carefully.&lt;/p&gt;

&lt;p&gt;There is no guaranteed income.&lt;/p&gt;

&lt;p&gt;But the value can be strong because the product touches real merchant pain: lost orders, confused customers, delayed fulfillment, support load, and finance mismatch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing examples
&lt;/h2&gt;

&lt;p&gt;These are not promises. They are product packaging examples.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starter package:
- One merchant integration
- Webhook receiver
- Basic payment/order matching
- Daily CSV export
- Manual support
Possible pricing: setup fee + small monthly retainer

Operations package:
- Everything in Starter
- Exception queue
- Payment timeline
- Backfill jobs
- Alerts
- Support notes
Possible pricing: higher setup fee + monthly monitoring

Advanced package:
- Multi-store support
- Static address reconciliation
- Custom fulfillment checks
- Finance exports
- Role-based dashboard
- SLA monitoring
Possible pricing: premium setup + recurring subscription/retainer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are a solo developer, start with a service package before building full SaaS.&lt;/p&gt;

&lt;p&gt;You will learn the real edge cases faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical risks
&lt;/h2&gt;

&lt;p&gt;A reconciliation product deals with financial operations.&lt;/p&gt;

&lt;p&gt;Be careful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not process unsigned callbacks
&lt;/h3&gt;

&lt;p&gt;Store invalid callbacks for audit, but do not update payment state from them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not trigger fulfillment twice
&lt;/h3&gt;

&lt;p&gt;Every fulfillment action needs idempotency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not treat &lt;code&gt;paying&lt;/code&gt; as final
&lt;/h3&gt;

&lt;p&gt;Wait for confirmed paid state before fulfillment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not hide underpayments
&lt;/h3&gt;

&lt;p&gt;Underpayments should be visible to support or finance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not rely only on webhooks
&lt;/h3&gt;

&lt;p&gt;Use scheduled backfill from provider history.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not overwrite manual decisions silently
&lt;/h3&gt;

&lt;p&gt;If a merchant manually accepts or resolves a case, preserve the audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not expose API keys
&lt;/h3&gt;

&lt;p&gt;Encrypt stored API keys. Use strict access control. Avoid logging secrets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not become the merchant of record accidentally
&lt;/h3&gt;

&lt;p&gt;If you are building software for merchants, make it clear that the merchant owns the payment relationship, customer relationship, tax obligations, refund policy, and compliance responsibilities.&lt;/p&gt;

&lt;p&gt;This is especially important if your tool touches payouts, refunds, account balances, or multi-party flows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security checklist
&lt;/h2&gt;

&lt;p&gt;A production-grade reconciliation tool should include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] HMAC validation for every webhook
[ ] Raw body preservation for signature verification
[ ] Timing-safe signature comparison
[ ] Encrypted API key storage
[ ] No secrets in logs
[ ] Idempotent webhook processing
[ ] Unique transaction hash constraints
[ ] Role-based dashboard access
[ ] Audit trail for manual resolution
[ ] Provider/local state mismatch alerts
[ ] Backfill jobs for missed webhooks
[ ] Queue-based processing
[ ] Dead-letter queue for failed jobs
[ ] Secure admin actions
[ ] Export access logging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checklist can become part of your paid offering.&lt;/p&gt;

&lt;p&gt;Merchants do not only pay for code. They pay for operational safety.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build plan: 21 days
&lt;/h2&gt;

&lt;p&gt;Here is a realistic build plan for a first usable version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Days 1-3: Merchant and order model
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merchant tenant model&lt;/li&gt;
&lt;li&gt;encrypted API key storage&lt;/li&gt;
&lt;li&gt;local order import&lt;/li&gt;
&lt;li&gt;external order ID mapping&lt;/li&gt;
&lt;li&gt;basic dashboard shell&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 4-6: Invoice and payment session layer
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice creation wrapper&lt;/li&gt;
&lt;li&gt;storage of track ID&lt;/li&gt;
&lt;li&gt;order-to-payment link&lt;/li&gt;
&lt;li&gt;payment session table&lt;/li&gt;
&lt;li&gt;basic payment status page&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 7-9: Webhook ingestion
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhook endpoint&lt;/li&gt;
&lt;li&gt;raw body capture&lt;/li&gt;
&lt;li&gt;HMAC validation&lt;/li&gt;
&lt;li&gt;webhook event log&lt;/li&gt;
&lt;li&gt;async processing queue&lt;/li&gt;
&lt;li&gt;idempotency logic&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 10-12: Reconciliation engine
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider status mapping&lt;/li&gt;
&lt;li&gt;paid-not-fulfilled detection&lt;/li&gt;
&lt;li&gt;fulfilled-not-paid detection&lt;/li&gt;
&lt;li&gt;underpaid case detection&lt;/li&gt;
&lt;li&gt;expired invoice detection&lt;/li&gt;
&lt;li&gt;duplicate webhook detection&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 13-15: Backfill jobs
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment History pull&lt;/li&gt;
&lt;li&gt;recent window sync&lt;/li&gt;
&lt;li&gt;daily reconciliation report&lt;/li&gt;
&lt;li&gt;provider/local mismatch detection&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 16-18: Dashboard and case queue
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exception queue&lt;/li&gt;
&lt;li&gt;severity labels&lt;/li&gt;
&lt;li&gt;case assignment&lt;/li&gt;
&lt;li&gt;resolution notes&lt;/li&gt;
&lt;li&gt;payment timeline&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 19-21: Alerts and production hardening
&lt;/h3&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email/Telegram/Slack alerts&lt;/li&gt;
&lt;li&gt;retry handling&lt;/li&gt;
&lt;li&gt;dead-letter queue&lt;/li&gt;
&lt;li&gt;admin audit log&lt;/li&gt;
&lt;li&gt;security review&lt;/li&gt;
&lt;li&gt;onboarding checklist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end of this sprint, you do not have a perfect SaaS.&lt;/p&gt;

&lt;p&gt;You have a productized tool that can solve a real merchant problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The best niche to start with
&lt;/h2&gt;

&lt;p&gt;Do not start too broad.&lt;/p&gt;

&lt;p&gt;A reconciliation tool for “all crypto merchants” is hard to sell.&lt;/p&gt;

&lt;p&gt;Start with one niche.&lt;/p&gt;

&lt;p&gt;Good first niches:&lt;/p&gt;

&lt;h3&gt;
  
  
  Digital product stores
&lt;/h3&gt;

&lt;p&gt;They need automatic delivery and support investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosting or VPN sellers
&lt;/h3&gt;

&lt;p&gt;They need plan activation, renewal checks, and payment status clarity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Course platforms
&lt;/h3&gt;

&lt;p&gt;They need access control and finance reporting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Telegram paid communities
&lt;/h3&gt;

&lt;p&gt;They need membership state, expiry, and support lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agencies managing merchant clients
&lt;/h3&gt;

&lt;p&gt;They need one dashboard across multiple stores.&lt;/p&gt;

&lt;p&gt;The narrower the niche, the easier it is to write copy, design workflows, and price the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes this more than a dashboard?
&lt;/h2&gt;

&lt;p&gt;A dashboard shows data.&lt;/p&gt;

&lt;p&gt;A reconciliation product closes operational gaps.&lt;/p&gt;

&lt;p&gt;The difference is action.&lt;/p&gt;

&lt;p&gt;Weak product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here are your payments.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strong product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;These 7 paid orders were not fulfilled.
These 3 invoices are underpaid.
These 2 payments exist in provider history but not in your store.
This webhook endpoint failed 4 times.
Here is the recommended action for each case.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is what merchants will pay for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Advanced features
&lt;/h2&gt;

&lt;p&gt;Once the MVP works, useful advanced features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-merchant agency dashboard&lt;/li&gt;
&lt;li&gt;per-niche reconciliation rules&lt;/li&gt;
&lt;li&gt;customer-facing payment status page&lt;/li&gt;
&lt;li&gt;Slack/Telegram alerting&lt;/li&gt;
&lt;li&gt;accounting exports&lt;/li&gt;
&lt;li&gt;static address deposit reconciliation&lt;/li&gt;
&lt;li&gt;payout reconciliation&lt;/li&gt;
&lt;li&gt;automatic support response drafts&lt;/li&gt;
&lt;li&gt;anomaly detection&lt;/li&gt;
&lt;li&gt;monthly finance summary&lt;/li&gt;
&lt;li&gt;webhook health score&lt;/li&gt;
&lt;li&gt;API integration with helpdesk tools&lt;/li&gt;
&lt;li&gt;role-based approval for manual resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not build these first.&lt;/p&gt;

&lt;p&gt;Use them to upsell after the core reconciliation workflow proves value.&lt;/p&gt;




&lt;h2&gt;
  
  
  How this becomes a developer business
&lt;/h2&gt;

&lt;p&gt;This is the important part.&lt;/p&gt;

&lt;p&gt;A developer does not need to compete with payment gateways.&lt;/p&gt;

&lt;p&gt;The developer builds the layer around the gateway.&lt;/p&gt;

&lt;p&gt;OxaPay handles the crypto payment infrastructure.&lt;/p&gt;

&lt;p&gt;The developer builds the merchant-specific operational system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;order matching&lt;/li&gt;
&lt;li&gt;case management&lt;/li&gt;
&lt;li&gt;support visibility&lt;/li&gt;
&lt;li&gt;fulfillment checks&lt;/li&gt;
&lt;li&gt;reporting&lt;/li&gt;
&lt;li&gt;alerts&lt;/li&gt;
&lt;li&gt;audit trail&lt;/li&gt;
&lt;li&gt;niche-specific workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where customization lives.&lt;/p&gt;

&lt;p&gt;That is where merchants need help.&lt;/p&gt;

&lt;p&gt;That is where a developer can charge.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;Crypto payment reconciliation is one of the strongest product opportunities for developers building around payment infrastructure.&lt;/p&gt;

&lt;p&gt;It is not the flashiest idea.&lt;/p&gt;

&lt;p&gt;It is not as obvious as building a checkout page.&lt;/p&gt;

&lt;p&gt;But it solves a real operational problem.&lt;/p&gt;

&lt;p&gt;When payment volume grows, merchants need more than invoice links and callbacks. They need to know whether every payment, order, fulfillment action, support case, and finance report agrees.&lt;/p&gt;

&lt;p&gt;That is the product.&lt;/p&gt;

&lt;p&gt;If you build it well, you are not just integrating OxaPay.&lt;/p&gt;

&lt;p&gt;You are building a merchant payment operations layer on top of it.&lt;/p&gt;

&lt;p&gt;And that is a much stronger business than selling one-off API setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-status-table" rel="noopener noreferrer"&gt;OxaPay Payment Status Table&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Generate Static Address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;OxaPay PHP SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.make.com/oxapay-crypto-pay-gtw" rel="noopener noreferrer"&gt;OxaPay Make Integration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>crypto</category>
      <category>backend</category>
    </item>
    <item>
      <title>Build a Crypto Revenue Split and Payout System for Merchants</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Wed, 15 Jul 2026 10:36:34 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-crypto-revenue-split-and-payout-system-for-merchants-365a</link>
      <guid>https://dev.to/kevins1988/build-a-crypto-revenue-split-and-payout-system-for-merchants-365a</guid>
      <description>&lt;p&gt;Most payment tutorials stop too early.&lt;/p&gt;

&lt;p&gt;They show you how to create an invoice, redirect the buyer, receive a webhook, and mark an order as paid.&lt;/p&gt;

&lt;p&gt;That is useful, but it is not where many real merchant problems end.&lt;/p&gt;

&lt;p&gt;For marketplaces, creator platforms, affiliate programs, course platforms, agencies, reseller networks, and service communities, the real operational question often comes after the customer pays:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who should receive the money, how much should each party receive, when should payouts happen, and how do we prove what happened later?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the business opportunity.&lt;/p&gt;

&lt;p&gt;A developer can build a crypto revenue split and payout system for merchants who already have revenue coming in, but do not want to manage spreadsheets, manual wallet transfers, partner balances, payout approvals, and payment disputes by hand.&lt;/p&gt;

&lt;p&gt;In this article, I will use OxaPay as the example payment infrastructure because its documentation exposes the payment and payout primitives required for this kind of system: invoice generation, callback URLs, payment webhooks, payment history, payout creation, payout information, payout history, payout status tracking, SDKs, and automation integrations.&lt;/p&gt;

&lt;p&gt;This is not an article about creating a new crypto payment gateway.&lt;/p&gt;

&lt;p&gt;It is about building a merchant-facing operational product on top of existing crypto payment infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;A crypto revenue split and payout system helps a merchant collect payment from customers and later distribute revenue to the right people.&lt;/p&gt;

&lt;p&gt;The developer does not simply build a checkout button.&lt;/p&gt;

&lt;p&gt;The developer builds the layer between incoming payments and outgoing payouts.&lt;/p&gt;

&lt;p&gt;That layer usually includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment intake&lt;/li&gt;
&lt;li&gt;order matching&lt;/li&gt;
&lt;li&gt;partner or seller records&lt;/li&gt;
&lt;li&gt;revenue split rules&lt;/li&gt;
&lt;li&gt;internal ledger entries&lt;/li&gt;
&lt;li&gt;partner balances&lt;/li&gt;
&lt;li&gt;payout thresholds&lt;/li&gt;
&lt;li&gt;payout approval flows&lt;/li&gt;
&lt;li&gt;payout execution&lt;/li&gt;
&lt;li&gt;payout status tracking&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;li&gt;finance exports&lt;/li&gt;
&lt;li&gt;support visibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The merchant buys the system because manual revenue sharing becomes painful as soon as transaction volume grows.&lt;/p&gt;

&lt;p&gt;A simple example:&lt;/p&gt;

&lt;p&gt;A course platform sells a $100 course.&lt;/p&gt;

&lt;p&gt;The platform keeps 20%.&lt;/p&gt;

&lt;p&gt;The instructor receives 70%.&lt;/p&gt;

&lt;p&gt;An affiliate receives 10%.&lt;/p&gt;

&lt;p&gt;The payment arrives in crypto.&lt;/p&gt;

&lt;p&gt;Without a system, someone has to calculate every share, update spreadsheets, check whether the payment was fully confirmed, copy wallet addresses, send payouts, record transaction IDs, answer partner questions, and fix mistakes.&lt;/p&gt;

&lt;p&gt;With a proper system, the flow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer pays invoice
        ↓
Payment webhook confirms paid status
        ↓
System records payment in ledger
        ↓
Split engine calculates shares
        ↓
Partner balances are updated
        ↓
Payout queue is created
        ↓
Admin approves payout batch
        ↓
Payout API sends funds
        ↓
Payout webhook updates final status
        ↓
Partner dashboard and finance export are updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a real product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why merchants would pay for this
&lt;/h2&gt;

&lt;p&gt;Merchants do not pay for code because it is interesting.&lt;/p&gt;

&lt;p&gt;They pay when a system removes a recurring operational problem.&lt;/p&gt;

&lt;p&gt;Revenue split and payout workflows create several recurring problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;partners ask when they will be paid&lt;/li&gt;
&lt;li&gt;sellers dispute their balances&lt;/li&gt;
&lt;li&gt;affiliates want transparent reporting&lt;/li&gt;
&lt;li&gt;finance teams need exportable records&lt;/li&gt;
&lt;li&gt;payouts get delayed by manual review&lt;/li&gt;
&lt;li&gt;wallet addresses change&lt;/li&gt;
&lt;li&gt;a wrong payout can be expensive&lt;/li&gt;
&lt;li&gt;support teams need to understand payment history&lt;/li&gt;
&lt;li&gt;one payment can belong to multiple parties&lt;/li&gt;
&lt;li&gt;one partner can earn from many payments&lt;/li&gt;
&lt;li&gt;one payout batch can contain many partner payments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The value is not only automation.&lt;/p&gt;

&lt;p&gt;The deeper value is control.&lt;/p&gt;

&lt;p&gt;A merchant wants to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which payment created this payable?&lt;/li&gt;
&lt;li&gt;which rule created this partner share?&lt;/li&gt;
&lt;li&gt;has this payout been approved?&lt;/li&gt;
&lt;li&gt;was the payout sent?&lt;/li&gt;
&lt;li&gt;did the blockchain transaction confirm?&lt;/li&gt;
&lt;li&gt;was the payout rejected?&lt;/li&gt;
&lt;li&gt;who changed the partner wallet address?&lt;/li&gt;
&lt;li&gt;why did the balance change?&lt;/li&gt;
&lt;li&gt;can finance export this month’s payout report?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why this can become a paid product or service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who can use this kind of product?
&lt;/h2&gt;

&lt;p&gt;This model works best when a business receives money from customers and must distribute money to other people later.&lt;/p&gt;

&lt;p&gt;Good target users include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Business type&lt;/th&gt;
&lt;th&gt;Revenue split problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Marketplaces&lt;/td&gt;
&lt;td&gt;Split revenue between platform and sellers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creator platforms&lt;/td&gt;
&lt;td&gt;Pay creators after customer purchases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Course platforms&lt;/td&gt;
&lt;td&gt;Pay instructors and affiliates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agencies&lt;/td&gt;
&lt;td&gt;Pay contractors, referrers, and partners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affiliate programs&lt;/td&gt;
&lt;td&gt;Track commission and batch payouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freelancer networks&lt;/td&gt;
&lt;td&gt;Pay service providers after customer payment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gaming communities&lt;/td&gt;
&lt;td&gt;Share revenue with server owners, teams, or partners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Donation platforms&lt;/td&gt;
&lt;td&gt;Route donations to campaigns, creators, or beneficiaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reseller programs&lt;/td&gt;
&lt;td&gt;Calculate reseller commission and payout balances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS partner programs&lt;/td&gt;
&lt;td&gt;Pay referral or integration partners&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The best customers are not tiny merchants with one transaction per week.&lt;/p&gt;

&lt;p&gt;The best customers are merchants with recurring transaction flow, multiple partners, and enough operational complexity that spreadsheets start breaking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The important distinction: split logic is yours, payment infrastructure is OxaPay
&lt;/h2&gt;

&lt;p&gt;This is the first design rule.&lt;/p&gt;

&lt;p&gt;OxaPay can help you create invoices, receive webhooks, retrieve payment records, and generate payout requests.&lt;/p&gt;

&lt;p&gt;Your application must own the revenue split logic.&lt;/p&gt;

&lt;p&gt;That means your system decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who receives a share&lt;/li&gt;
&lt;li&gt;how much each partner receives&lt;/li&gt;
&lt;li&gt;which payment created the balance&lt;/li&gt;
&lt;li&gt;whether funds should be held&lt;/li&gt;
&lt;li&gt;whether the payout requires approval&lt;/li&gt;
&lt;li&gt;whether there is a minimum payout threshold&lt;/li&gt;
&lt;li&gt;whether a partner is allowed to receive payouts&lt;/li&gt;
&lt;li&gt;whether a payout should be delayed, blocked, or retried&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;p&gt;Do not build a system that receives a payment webhook and immediately sends payouts in the same request.&lt;/p&gt;

&lt;p&gt;That is fragile.&lt;/p&gt;

&lt;p&gt;A production-grade system should use this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment confirmation → ledger entry → split calculation → payout queue → approval → payout execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ledger is the source of truth.&lt;/p&gt;

&lt;p&gt;The payout API is the execution layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  OxaPay primitives used in this system
&lt;/h2&gt;

&lt;p&gt;For this article, we need several OxaPay primitives.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Generate Invoice
&lt;/h3&gt;

&lt;p&gt;OxaPay's Generate Invoice endpoint lets a merchant create a new invoice and obtain a payment URL. The request supports fields such as &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;lifetime&lt;/code&gt;, &lt;code&gt;fee_paid_by_payer&lt;/code&gt;, &lt;code&gt;under_paid_coverage&lt;/code&gt;, &lt;code&gt;callback_url&lt;/code&gt;, &lt;code&gt;return_url&lt;/code&gt;, and &lt;code&gt;order_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payment/generate-invoice&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a revenue split system, the invoice represents the customer-facing payment request.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;order_id&lt;/code&gt; should reference your internal order, booking, sale, or transaction record.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;callback_url&lt;/code&gt; should point to your webhook endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Webhook
&lt;/h3&gt;

&lt;p&gt;OxaPay sends payment status updates to the &lt;code&gt;callback_url&lt;/code&gt;. According to the webhook documentation, callbacks are sent as JSON over HTTP POST. The system expects your endpoint to return HTTP 200 with &lt;code&gt;ok&lt;/code&gt;, and OxaPay retries webhook delivery up to five times if delivery fails.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;https://docs.oxapay.com/webhook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is important because revenue split logic should only start after the payment reaches the correct confirmed business state.&lt;/p&gt;

&lt;p&gt;In most systems, you should not create partner balances from early or incomplete payment states.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. HMAC validation
&lt;/h3&gt;

&lt;p&gt;OxaPay webhooks include an &lt;code&gt;HMAC&lt;/code&gt; header. The docs state that merchants should validate callbacks using the merchant API key for payment webhooks, and the payout API key for payout webhooks. The signature is generated over the raw POST body using HMAC SHA-512.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;https://docs.oxapay.com/webhook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not optional in a payout-related system.&lt;/p&gt;

&lt;p&gt;If your application updates balances or payout status from webhooks, signature validation is a core security requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Payment History
&lt;/h3&gt;

&lt;p&gt;The Payment History endpoint retrieves payments associated with a merchant account. It supports filters such as &lt;code&gt;track_id&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;pay_currency&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;network&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, date range, amount range, sorting, and pagination.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payment/payment-history&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is useful for reconciliation jobs and admin dashboards.&lt;/p&gt;

&lt;p&gt;If a webhook is delayed or your processing job fails, your system can use payment history or payment information to reconcile its internal state.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Generate Payout
&lt;/h3&gt;

&lt;p&gt;OxaPay's Generate Payout endpoint creates a cryptocurrency payout request to a specified recipient address. The request uses a &lt;code&gt;payout_api_key&lt;/code&gt; and fields such as &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;network&lt;/code&gt;, &lt;code&gt;callback_url&lt;/code&gt;, &lt;code&gt;memo&lt;/code&gt;, and &lt;code&gt;description&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://docs.oxapay.com/api-reference/payout/generate-payout" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payout/generate-payout&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the execution step after your own system has calculated and approved a payout.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Payout Information and Payout History
&lt;/h3&gt;

&lt;p&gt;Payout Information retrieves the details of a specific payout using its &lt;code&gt;track_id&lt;/code&gt;. Payout History retrieves payout transactions associated with the account and supports filtering by status, type, currency, network, amount range, date range, sorting, and pagination.&lt;/p&gt;

&lt;p&gt;Docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-information" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payout/payout-information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-history" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payout/payout-history&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These endpoints are useful for payout dashboards, reconciliation, and support tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Payout Status Table
&lt;/h3&gt;

&lt;p&gt;The payout status table lists states such as &lt;code&gt;processing&lt;/code&gt;, &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;confirming&lt;/code&gt;, &lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;canceled&lt;/code&gt;, and &lt;code&gt;rejected&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://docs.oxapay.com/api-reference/payout/payout-status-table" rel="noopener noreferrer"&gt;https://docs.oxapay.com/api-reference/payout/payout-status-table&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your system should map these external statuses to internal payout states.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. SDKs and automation tools
&lt;/h3&gt;

&lt;p&gt;OxaPay also provides SDKs and automation integrations. The PHP, Python, and Laravel SDK pages include payment methods and webhook handling notes. The Make integration lists modules such as Generate Invoice, Generate Payout, Get Payment Information, Get Payout Information, Search Payments, Search Payouts, Watch Payment Webhook, and Watch Payout Webhook.&lt;/p&gt;

&lt;p&gt;Docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.make.com/oxapay-crypto-pay-gtw" rel="noopener noreferrer"&gt;https://apps.make.com/oxapay-crypto-pay-gtw&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives developers multiple implementation paths: code-first, SDK-based, or workflow-assisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually building
&lt;/h2&gt;

&lt;p&gt;A real revenue split and payout product has five layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Payment intake
&lt;/h3&gt;

&lt;p&gt;This layer creates invoices and receives payment webhooks.&lt;/p&gt;

&lt;p&gt;It should handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;order creation&lt;/li&gt;
&lt;li&gt;invoice generation&lt;/li&gt;
&lt;li&gt;customer redirect&lt;/li&gt;
&lt;li&gt;payment callback&lt;/li&gt;
&lt;li&gt;payment status updates&lt;/li&gt;
&lt;li&gt;duplicate webhook events&lt;/li&gt;
&lt;li&gt;late payment events&lt;/li&gt;
&lt;li&gt;failed or incomplete payment states&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 2: Ledger
&lt;/h3&gt;

&lt;p&gt;This is the most important layer.&lt;/p&gt;

&lt;p&gt;The ledger records financial movements inside your application.&lt;/p&gt;

&lt;p&gt;A common mistake is to only store balances as a single number.&lt;/p&gt;

&lt;p&gt;That is not enough.&lt;/p&gt;

&lt;p&gt;You need immutable ledger entries.&lt;/p&gt;

&lt;p&gt;A partner balance should be calculated from ledger entries, not manually edited as a random number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Split engine
&lt;/h3&gt;

&lt;p&gt;The split engine decides how revenue is allocated.&lt;/p&gt;

&lt;p&gt;It may support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixed percentage splits&lt;/li&gt;
&lt;li&gt;product-owner splits&lt;/li&gt;
&lt;li&gt;affiliate commission&lt;/li&gt;
&lt;li&gt;platform fees&lt;/li&gt;
&lt;li&gt;instructor revenue share&lt;/li&gt;
&lt;li&gt;creator revenue share&lt;/li&gt;
&lt;li&gt;multi-party splits&lt;/li&gt;
&lt;li&gt;reserves or holdbacks&lt;/li&gt;
&lt;li&gt;minimum payout thresholds&lt;/li&gt;
&lt;li&gt;tiered commission rules&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 4: Payout queue
&lt;/h3&gt;

&lt;p&gt;Do not execute payouts immediately.&lt;/p&gt;

&lt;p&gt;Create payout queue items first.&lt;/p&gt;

&lt;p&gt;A payout queue allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;review before sending&lt;/li&gt;
&lt;li&gt;minimum threshold checks&lt;/li&gt;
&lt;li&gt;partner address verification&lt;/li&gt;
&lt;li&gt;compliance review where needed&lt;/li&gt;
&lt;li&gt;balance checks&lt;/li&gt;
&lt;li&gt;scheduled payouts&lt;/li&gt;
&lt;li&gt;batch approvals&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;status tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Layer 5: Payout execution and tracking
&lt;/h3&gt;

&lt;p&gt;After approval, the system sends payout requests through the payout API.&lt;/p&gt;

&lt;p&gt;Then it listens for payout webhooks or checks payout information/history to update payout state.&lt;/p&gt;

&lt;p&gt;The goal is not only to send funds.&lt;/p&gt;

&lt;p&gt;The goal is to make payouts traceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference architecture
&lt;/h2&gt;

&lt;p&gt;A practical architecture can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer checkout
      ↓
Create order in merchant app
      ↓
Create OxaPay invoice
      ↓
Customer pays
      ↓
OxaPay payment webhook
      ↓
Validate HMAC
      ↓
Store raw webhook event
      ↓
Process payment idempotently
      ↓
Create ledger entries
      ↓
Apply split rules
      ↓
Update partner balances
      ↓
Create payout queue items
      ↓
Admin reviews payout batch
      ↓
Call OxaPay Generate Payout
      ↓
OxaPay payout webhook
      ↓
Validate payout HMAC
      ↓
Update payout item status
      ↓
Update partner dashboard and finance exports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the architecture a merchant is paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database model
&lt;/h2&gt;

&lt;p&gt;You do not need an over-engineered database on day one.&lt;/p&gt;

&lt;p&gt;But you do need the right concepts.&lt;/p&gt;

&lt;p&gt;Here is a simplified schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;partners&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;partner_payout_methods&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;partner_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;memo&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending_verification'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;verified_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;external_order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount_decimal&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'oxapay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount_decimal&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;split_rules&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rule_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ledger_entries&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;partner_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;entry_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount_decimal&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'credit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'debit'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payout_batches&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_by&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;approved_by&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;approved_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payout_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;batch_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payout_batches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;partner_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payout_method_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;partner_payout_methods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;amount_decimal&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'oxapay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;sent_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confirmed_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;signature_valid&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;audit_logs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;actor_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema is not final.&lt;/p&gt;

&lt;p&gt;But it captures the key idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payments are not payouts&lt;/li&gt;
&lt;li&gt;balances come from ledger entries&lt;/li&gt;
&lt;li&gt;payout items are queued before execution&lt;/li&gt;
&lt;li&gt;webhook events are stored&lt;/li&gt;
&lt;li&gt;partner payout methods are separate records&lt;/li&gt;
&lt;li&gt;audit logs exist for sensitive actions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why a ledger matters
&lt;/h2&gt;

&lt;p&gt;Imagine this partner balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Partner A balance: 700 USDT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That number alone tells you almost nothing.&lt;/p&gt;

&lt;p&gt;A real system needs to explain why the balance is 700 USDT.&lt;/p&gt;

&lt;p&gt;A ledger can show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+70 USDT  Course sale #1001 instructor share
+70 USDT  Course sale #1002 instructor share
+70 USDT  Course sale #1003 instructor share
-100 USDT Payout batch #22
+20 USDT  Affiliate bonus correction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The balance is the result of events.&lt;/p&gt;

&lt;p&gt;This gives your merchant trust.&lt;/p&gt;

&lt;p&gt;It also gives partners visibility.&lt;/p&gt;

&lt;p&gt;Without a ledger, every balance dispute becomes a manual investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split rule examples
&lt;/h2&gt;

&lt;p&gt;A revenue split system should support simple rules first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixed percentage split
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform_fee_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partner_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use case:&lt;/p&gt;

&lt;p&gt;A marketplace keeps 20%, seller receives 80%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instructor plus affiliate split
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform_fee_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instructor_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"affiliate_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use case:&lt;/p&gt;

&lt;p&gt;An online course platform pays the instructor and affiliate after a sale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Product owner split
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform_fee_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product_owner_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use case:&lt;/p&gt;

&lt;p&gt;A digital product marketplace pays the product creator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Holdback or reserve
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform_fee_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partner_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reserve_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reserve_release_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use case:&lt;/p&gt;

&lt;p&gt;A merchant wants to delay part of partner earnings for support issues, refunds, delivery disputes, or risk management.&lt;/p&gt;

&lt;p&gt;Even when crypto payments are operationally different from card payments, merchants may still need internal holds for customer support, refund policies, service delivery windows, or partner disputes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: create an OxaPay invoice
&lt;/h2&gt;

&lt;p&gt;Here is a simplified Node.js example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createInvoiceForOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1/payment/invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://yourapp.com/webhooks/oxapay/payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://yourapp.com/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/thank-you`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OxaPay invoice request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your database, store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal order ID&lt;/li&gt;
&lt;li&gt;OxaPay track ID if returned&lt;/li&gt;
&lt;li&gt;payment URL if returned&lt;/li&gt;
&lt;li&gt;requested amount&lt;/li&gt;
&lt;li&gt;requested currency&lt;/li&gt;
&lt;li&gt;invoice status&lt;/li&gt;
&lt;li&gt;creation time&lt;/li&gt;
&lt;li&gt;expiration time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not rely only on the frontend redirect.&lt;/p&gt;

&lt;p&gt;The webhook should be the source of payment confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: validate and store a payment webhook
&lt;/h2&gt;

&lt;p&gt;Webhook handling should be fast, verifiable, and idempotent.&lt;/p&gt;

&lt;p&gt;Do not perform heavy split calculations before responding.&lt;/p&gt;

&lt;p&gt;Store the event, return &lt;code&gt;ok&lt;/code&gt;, and process it asynchronously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Important: use raw body for HMAC validation.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing hmac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Store the raw event first.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;signature_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Return exactly what the provider expects.&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Process asynchronously.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process_oxapay_payment_webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;webhook_event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OxaPay documentation says payment callbacks should be validated using the merchant API key, and payout callbacks should be validated using the payout API key.&lt;/p&gt;

&lt;p&gt;So use different secrets for different webhook routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing the payment webhook idempotently
&lt;/h2&gt;

&lt;p&gt;A webhook can be retried.&lt;/p&gt;

&lt;p&gt;Your system must survive duplicates.&lt;/p&gt;

&lt;p&gt;This is the minimum logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processPaymentWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingPayment&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsertByProviderTrackId&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeOxaPayPaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByProviderPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_without_order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entity_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider_track_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applySplitRules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeOxaPayPaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact payload fields should be mapped from your tested webhook payloads and the Payment Information response.&lt;/p&gt;

&lt;p&gt;The important part is the process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify signature&lt;/li&gt;
&lt;li&gt;store raw event&lt;/li&gt;
&lt;li&gt;deduplicate by provider track ID&lt;/li&gt;
&lt;li&gt;process inside a database transaction&lt;/li&gt;
&lt;li&gt;only apply split rules once&lt;/li&gt;
&lt;li&gt;mark the webhook as processed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Applying split rules
&lt;/h2&gt;

&lt;p&gt;For financial calculations, do not use floating point arithmetic.&lt;/p&gt;

&lt;p&gt;Use integer minor units where possible or a decimal library with explicit rounding rules.&lt;/p&gt;

&lt;p&gt;For crypto amounts, use a decimal type that can preserve precision.&lt;/p&gt;

&lt;p&gt;Here is a simplified split example using decimal logic conceptually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Decimal&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decimal.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applySplitRules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split_rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActiveForOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platformFee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform_fee_percent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instructorShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instructor_percent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;affiliateShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;affiliate_percent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;div&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Optional: keep rounding dust in the platform account.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allocated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;platformFee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instructorShare&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;affiliateShare&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roundingRemainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allocated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ledger_entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertMany&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;partner_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entry_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;platform_fee&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;credit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;platformFee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roundingRemainder&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Platform fee for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external_order_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;partner_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instructor_partner_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entry_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;partner_share&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;credit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;instructorShare&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Instructor share for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external_order_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;affiliateShare&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;affiliate_partner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ledger_entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;partner_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;affiliate_partner_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entry_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;affiliate_commission&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;credit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;affiliateShare&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Affiliate commission for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external_order_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, you need more controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rule versioning&lt;/li&gt;
&lt;li&gt;effective dates&lt;/li&gt;
&lt;li&gt;product-specific rules&lt;/li&gt;
&lt;li&gt;partner-specific overrides&lt;/li&gt;
&lt;li&gt;minimum payout thresholds&lt;/li&gt;
&lt;li&gt;maximum payout limits&lt;/li&gt;
&lt;li&gt;rounding policy&lt;/li&gt;
&lt;li&gt;reserve policy&lt;/li&gt;
&lt;li&gt;manual adjustments&lt;/li&gt;
&lt;li&gt;approval workflow for corrections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never silently edit past ledger entries.&lt;/p&gt;

&lt;p&gt;If you need to fix something, create a reversing entry or adjustment entry.&lt;/p&gt;

&lt;p&gt;That is how you preserve auditability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the payout queue
&lt;/h2&gt;

&lt;p&gt;A payout queue is where earned balances become payable items.&lt;/p&gt;

&lt;p&gt;A daily or weekly job can calculate partner balances and create payout items.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createPayoutBatchForMerchant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payout_batches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActiveWithBalances&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;min_balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;25.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partner&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;partner_payout_methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findVerifiedDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payout_skipped_no_verified_method&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;entity_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;partner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;partner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;partner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payout_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;partner_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;partner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;payout_method_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;partner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This job should not send funds.&lt;/p&gt;

&lt;p&gt;It should create a draft batch.&lt;/p&gt;

&lt;p&gt;A merchant admin should review it first, especially in early versions of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approval workflow
&lt;/h2&gt;

&lt;p&gt;Payouts are sensitive.&lt;/p&gt;

&lt;p&gt;Your first version should include manual approval.&lt;/p&gt;

&lt;p&gt;At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payout batch created as &lt;code&gt;draft&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;admin reviews partner, amount, currency, network, address&lt;/li&gt;
&lt;li&gt;admin approves batch&lt;/li&gt;
&lt;li&gt;system records &lt;code&gt;approved_by&lt;/code&gt; and &lt;code&gt;approved_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;execution worker sends payout requests&lt;/li&gt;
&lt;li&gt;payout status is tracked asynchronously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For larger merchants, add stronger controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two-person approval&lt;/li&gt;
&lt;li&gt;payout limits per day&lt;/li&gt;
&lt;li&gt;payout limits per partner&lt;/li&gt;
&lt;li&gt;address change cooldown&lt;/li&gt;
&lt;li&gt;require re-approval after address changes&lt;/li&gt;
&lt;li&gt;role-based permissions&lt;/li&gt;
&lt;li&gt;audit logs for every approval&lt;/li&gt;
&lt;li&gt;blocked partner status&lt;/li&gt;
&lt;li&gt;suspicious payout review queue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where your product becomes serious.&lt;/p&gt;

&lt;p&gt;Many developers can call a payout API.&lt;/p&gt;

&lt;p&gt;Fewer developers build a safe payout approval system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: generate an OxaPay payout
&lt;/h2&gt;

&lt;p&gt;After approval, call the payout API from a backend worker.&lt;/p&gt;

&lt;p&gt;Never call payout execution from the frontend.&lt;/p&gt;

&lt;p&gt;Never expose the payout API key to client-side code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_PAYOUT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_PAYOUT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendOxaPayPayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1/payout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payout_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_PAYOUT_API_KEY&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memo&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://yourapp.com/webhooks/oxapay/payout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Payout item &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OxaPay payout failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After sending the request, store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payout item ID&lt;/li&gt;
&lt;li&gt;provider track ID&lt;/li&gt;
&lt;li&gt;request payload hash&lt;/li&gt;
&lt;li&gt;requested amount&lt;/li&gt;
&lt;li&gt;requested currency&lt;/li&gt;
&lt;li&gt;network&lt;/li&gt;
&lt;li&gt;payout address reference&lt;/li&gt;
&lt;li&gt;provider status&lt;/li&gt;
&lt;li&gt;sent timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not delete the payout item if the provider returns an error.&lt;/p&gt;

&lt;p&gt;Record the failure and require review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payout state machine
&lt;/h2&gt;

&lt;p&gt;Your internal payout state machine should not blindly mirror provider statuses.&lt;/p&gt;

&lt;p&gt;Use your own states and map provider statuses into them.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Internal status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Possible OxaPay mapping&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queued&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Created but not approved&lt;/td&gt;
&lt;td&gt;Internal only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approved&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Approved by merchant admin&lt;/td&gt;
&lt;td&gt;Internal only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Worker is calling payout API&lt;/td&gt;
&lt;td&gt;Internal only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;provider_processing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provider accepted request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;processing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;provider_pending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Request is in provider queue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pending&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blockchain_confirming&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transaction created and awaiting confirmation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;confirming&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;completed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payout completed successfully&lt;/td&gt;
&lt;td&gt;&lt;code&gt;confirmed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payout failed or rejected&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rejected&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;canceled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payout was canceled&lt;/td&gt;
&lt;td&gt;&lt;code&gt;canceled&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;needs_review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human review needed&lt;/td&gt;
&lt;td&gt;Internal only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This separation makes your system easier to evolve.&lt;/p&gt;

&lt;p&gt;If OxaPay changes wording or returns additional states later, you only update the mapping layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: payout webhook handler
&lt;/h2&gt;

&lt;p&gt;Use a separate route and validate with the payout API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/payout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing hmac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_PAYOUT_API_KEY&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;signature_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;process_oxapay_payout_webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;webhook_event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then process it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processPayoutWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payout_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByProviderTrackId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown_payout_webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entity_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webhook_event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapOxaPayPayoutStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payout_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;provider_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;confirmed_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmed_at&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ledger_entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;partner_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;partner_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entry_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payout_debit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount_decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Payout completed for item &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;payout_item_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider_track_id&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;needs_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextStatus&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit_logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payout_requires_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entity_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payout_item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payoutItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mapOxaPayPayoutStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blockchain_confirming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;needs_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real implementation, make sure the debit ledger entry is also idempotent.&lt;/p&gt;

&lt;p&gt;A duplicate payout webhook should not create two payout debits.&lt;/p&gt;

&lt;p&gt;Use a unique constraint such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;unique_payout_debit_entry&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ledger_entries&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'payout_item_id'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;entry_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'payout_debit'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Idempotency is not optional
&lt;/h2&gt;

&lt;p&gt;Payment and payout systems are distributed systems.&lt;/p&gt;

&lt;p&gt;Requests fail.&lt;/p&gt;

&lt;p&gt;Webhooks retry.&lt;/p&gt;

&lt;p&gt;Workers crash.&lt;/p&gt;

&lt;p&gt;Admins double-click buttons.&lt;/p&gt;

&lt;p&gt;Networks timeout.&lt;/p&gt;

&lt;p&gt;If your payout system is not idempotent, it can duplicate financial actions.&lt;/p&gt;

&lt;p&gt;Use idempotency at several levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unique internal order IDs&lt;/li&gt;
&lt;li&gt;unique payment provider track IDs&lt;/li&gt;
&lt;li&gt;unique webhook event records&lt;/li&gt;
&lt;li&gt;unique payout item IDs&lt;/li&gt;
&lt;li&gt;unique provider payout track IDs&lt;/li&gt;
&lt;li&gt;unique ledger debit per payout item&lt;/li&gt;
&lt;li&gt;database transactions around state transitions&lt;/li&gt;
&lt;li&gt;worker locks when executing payout batches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stripe's public API docs explain the general idea well: an idempotency key lets a server recognize retries of the same operation and return the original result instead of creating a duplicate operation.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;https://docs.stripe.com/api/idempotent_requests&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even if OxaPay's specific endpoints do not expose a public idempotency-key parameter in the examples, your own application should still implement internal idempotency around all payment and payout state transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security model
&lt;/h2&gt;

&lt;p&gt;A payout system has a larger blast radius than a checkout integration.&lt;/p&gt;

&lt;p&gt;A checkout bug may fail to activate an order.&lt;/p&gt;

&lt;p&gt;A payout bug may send funds to the wrong address.&lt;/p&gt;

&lt;p&gt;That changes the engineering standard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protect API keys
&lt;/h3&gt;

&lt;p&gt;Use separate secrets for merchant payment flows and payout flows.&lt;/p&gt;

&lt;p&gt;Store them in a secret manager or encrypted environment configuration.&lt;/p&gt;

&lt;p&gt;Never expose them to browser code.&lt;/p&gt;

&lt;p&gt;Limit who can access payout configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify payout addresses
&lt;/h3&gt;

&lt;p&gt;Partner payout addresses should not go from form input to payout execution immediately.&lt;/p&gt;

&lt;p&gt;Use a verification workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;partner submits address&lt;/li&gt;
&lt;li&gt;system marks it &lt;code&gt;pending_verification&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;merchant admin reviews it&lt;/li&gt;
&lt;li&gt;optional confirmation email is sent&lt;/li&gt;
&lt;li&gt;address becomes &lt;code&gt;verified&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;address changes trigger cooldown or re-approval&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use role-based access control
&lt;/h3&gt;

&lt;p&gt;A partner should only see their own earnings.&lt;/p&gt;

&lt;p&gt;An admin should only manage their own merchant account.&lt;/p&gt;

&lt;p&gt;A support agent should not be able to approve payouts.&lt;/p&gt;

&lt;p&gt;OWASP lists Broken Object Level Authorization as a major API security risk. In a payout product, this is especially important because object IDs often represent sensitive resources such as partner balances, payout methods, payout items, or merchant records.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/" rel="noopener noreferrer"&gt;https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add payout limits
&lt;/h3&gt;

&lt;p&gt;Start with conservative limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;max payout per item&lt;/li&gt;
&lt;li&gt;max payout per partner per day&lt;/li&gt;
&lt;li&gt;max payout per merchant per day&lt;/li&gt;
&lt;li&gt;max payout batch size&lt;/li&gt;
&lt;li&gt;minimum payout threshold&lt;/li&gt;
&lt;li&gt;cooldown after payout address change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These controls protect both the merchant and your product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep audit logs
&lt;/h3&gt;

&lt;p&gt;Every sensitive action should produce an audit log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;split rule created&lt;/li&gt;
&lt;li&gt;split rule changed&lt;/li&gt;
&lt;li&gt;partner payout address added&lt;/li&gt;
&lt;li&gt;payout address verified&lt;/li&gt;
&lt;li&gt;payout batch created&lt;/li&gt;
&lt;li&gt;payout batch approved&lt;/li&gt;
&lt;li&gt;payout sent&lt;/li&gt;
&lt;li&gt;payout canceled&lt;/li&gt;
&lt;li&gt;manual adjustment created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When money moves, logs are not optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation jobs
&lt;/h2&gt;

&lt;p&gt;Webhooks are useful, but a production payout product should also reconcile.&lt;/p&gt;

&lt;p&gt;Schedule jobs that compare your internal records against provider records.&lt;/p&gt;

&lt;p&gt;Example jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every 15 minutes:
- find payout items in provider_processing, provider_pending, blockchain_confirming
- call Payout Information by provider_track_id
- update stale statuses

Every day:
- retrieve Payout History for the previous day
- compare provider payouts with internal payout items
- flag missing or unknown payouts

Every day:
- retrieve Payment History for the previous day
- compare paid provider payments with internal paid orders
- flag mismatches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because webhooks can fail, infrastructure can be down, and humans can make mistakes.&lt;/p&gt;

&lt;p&gt;OxaPay's Payment History and Payout History endpoints are useful for this kind of reconciliation because they allow filtered and paginated retrieval of payment and payout records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling edge cases
&lt;/h2&gt;

&lt;p&gt;A good payout product is built around edge cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate payment webhook
&lt;/h3&gt;

&lt;p&gt;Process the payment once.&lt;/p&gt;

&lt;p&gt;Use provider track ID and internal payment status checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payment is paying but not paid
&lt;/h3&gt;

&lt;p&gt;Do not create final partner balances yet.&lt;/p&gt;

&lt;p&gt;Wait for the paid state required by your business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invoice expired but customer claims payment
&lt;/h3&gt;

&lt;p&gt;Use Payment Information or Payment History to investigate.&lt;/p&gt;

&lt;p&gt;Give support a payment timeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Split percentages do not add to 100%
&lt;/h3&gt;

&lt;p&gt;Reject the rule or assign the remainder to a defined platform rounding account.&lt;/p&gt;

&lt;p&gt;Never let the system silently lose money.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partner address changed before payout
&lt;/h3&gt;

&lt;p&gt;Require re-approval.&lt;/p&gt;

&lt;p&gt;Consider a cooldown window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payout rejected
&lt;/h3&gt;

&lt;p&gt;Move the payout item to &lt;code&gt;needs_review&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not automatically retry forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Currency mismatch
&lt;/h3&gt;

&lt;p&gt;Define whether each partner is paid in the same currency as the incoming payment, a preferred payout currency, or a merchant-selected settlement currency.&lt;/p&gt;

&lt;p&gt;Document the rule clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Insufficient balance
&lt;/h3&gt;

&lt;p&gt;Keep payout item queued or failed with a clear reason.&lt;/p&gt;

&lt;p&gt;Notify the merchant admin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rounding dust
&lt;/h3&gt;

&lt;p&gt;Use a consistent policy.&lt;/p&gt;

&lt;p&gt;Usually the safest option is to allocate rounding remainder to the platform account or a specific rounding ledger account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual adjustment
&lt;/h3&gt;

&lt;p&gt;Never edit the original ledger entry.&lt;/p&gt;

&lt;p&gt;Create a new adjustment entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  MVP version
&lt;/h2&gt;

&lt;p&gt;The MVP should be intentionally narrow.&lt;/p&gt;

&lt;p&gt;Do not start with every marketplace feature.&lt;/p&gt;

&lt;p&gt;Build this first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one merchant account&lt;/li&gt;
&lt;li&gt;one incoming payment flow&lt;/li&gt;
&lt;li&gt;one currency&lt;/li&gt;
&lt;li&gt;one network&lt;/li&gt;
&lt;li&gt;fixed percentage split rules&lt;/li&gt;
&lt;li&gt;partner records&lt;/li&gt;
&lt;li&gt;verified payout addresses&lt;/li&gt;
&lt;li&gt;immutable ledger entries&lt;/li&gt;
&lt;li&gt;manual payout batch creation&lt;/li&gt;
&lt;li&gt;manual payout approval&lt;/li&gt;
&lt;li&gt;OxaPay payout execution&lt;/li&gt;
&lt;li&gt;payout webhook handling&lt;/li&gt;
&lt;li&gt;partner balance dashboard&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;li&gt;audit logs for payout actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is enough to sell to a small marketplace, course platform, agency, or creator platform.&lt;/p&gt;

&lt;p&gt;The MVP should prove that merchants trust the numbers.&lt;/p&gt;

&lt;p&gt;Not that your UI has every feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production version
&lt;/h2&gt;

&lt;p&gt;A production version can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-merchant support&lt;/li&gt;
&lt;li&gt;multi-currency support&lt;/li&gt;
&lt;li&gt;multi-network payout methods&lt;/li&gt;
&lt;li&gt;advanced split rules&lt;/li&gt;
&lt;li&gt;rule versioning&lt;/li&gt;
&lt;li&gt;reserves and release schedules&lt;/li&gt;
&lt;li&gt;partner self-service portal&lt;/li&gt;
&lt;li&gt;payout batch scheduling&lt;/li&gt;
&lt;li&gt;two-person approval&lt;/li&gt;
&lt;li&gt;balance reconciliation&lt;/li&gt;
&lt;li&gt;accounting exports&lt;/li&gt;
&lt;li&gt;webhook replay tools&lt;/li&gt;
&lt;li&gt;API for merchant platforms&lt;/li&gt;
&lt;li&gt;white-label dashboard for agencies&lt;/li&gt;
&lt;li&gt;notification system&lt;/li&gt;
&lt;li&gt;dispute notes&lt;/li&gt;
&lt;li&gt;operational alerts&lt;/li&gt;
&lt;li&gt;role-based permissions&lt;/li&gt;
&lt;li&gt;address change cooldown&lt;/li&gt;
&lt;li&gt;payout risk scoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the service becomes more than a dev project.&lt;/p&gt;

&lt;p&gt;It becomes financial operations software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revenue model for developers
&lt;/h2&gt;

&lt;p&gt;There are several ways to monetize this product.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Setup fee
&lt;/h3&gt;

&lt;p&gt;Charge for implementation, configuration, and merchant onboarding.&lt;/p&gt;

&lt;p&gt;This works well for agencies, course platforms, and marketplaces that need custom integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Monthly platform fee
&lt;/h3&gt;

&lt;p&gt;Charge a recurring fee for hosting, dashboard access, webhook monitoring, payout queue management, and reporting.&lt;/p&gt;

&lt;p&gt;This is better than one-off integration work because the merchant depends on the system every month.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Managed payout operations
&lt;/h3&gt;

&lt;p&gt;Some merchants may want you to operate the payout workflow for them.&lt;/p&gt;

&lt;p&gt;That can include reviewing payout batches, generating reports, monitoring failed payouts, and coordinating support.&lt;/p&gt;

&lt;p&gt;Be careful with responsibility boundaries here.&lt;/p&gt;

&lt;p&gt;You should define exactly what you do and what the merchant approves.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Usage-based fee
&lt;/h3&gt;

&lt;p&gt;Charge based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;number of partners&lt;/li&gt;
&lt;li&gt;number of payout items&lt;/li&gt;
&lt;li&gt;number of payout batches&lt;/li&gt;
&lt;li&gt;number of monthly paid orders&lt;/li&gt;
&lt;li&gt;number of supported merchant accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. White-label license
&lt;/h3&gt;

&lt;p&gt;Sell the system to agencies or platforms that want to offer crypto revenue split tooling under their own brand.&lt;/p&gt;

&lt;p&gt;This is harder to build, but potentially stronger if you can reach agencies with existing merchant relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing examples
&lt;/h2&gt;

&lt;p&gt;These are not guaranteed income numbers.&lt;/p&gt;

&lt;p&gt;They are pricing structures a developer could test.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;What it includes&lt;/th&gt;
&lt;th&gt;Possible pricing model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starter setup&lt;/td&gt;
&lt;td&gt;One merchant, one split rule, manual payout queue&lt;/td&gt;
&lt;td&gt;one-time setup fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Growth setup&lt;/td&gt;
&lt;td&gt;Multiple partners, dashboard, CSV export, payout tracking&lt;/td&gt;
&lt;td&gt;higher setup fee + monthly support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed ops&lt;/td&gt;
&lt;td&gt;Monitoring, reconciliation, payout issue review&lt;/td&gt;
&lt;td&gt;monthly retainer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS dashboard&lt;/td&gt;
&lt;td&gt;Hosted partner balances and payout queue&lt;/td&gt;
&lt;td&gt;monthly subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency license&lt;/td&gt;
&lt;td&gt;White-label version for agency clients&lt;/td&gt;
&lt;td&gt;license + support fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise custom&lt;/td&gt;
&lt;td&gt;Custom rules, approval workflows, reporting, integrations&lt;/td&gt;
&lt;td&gt;project fee + recurring maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are starting alone, the easiest first offer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I build a crypto revenue split and payout dashboard for your marketplace, course platform, affiliate program, or creator business so you can calculate partner balances and send payouts without spreadsheets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is more concrete than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I integrate crypto payouts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What to sell first
&lt;/h2&gt;

&lt;p&gt;Do not sell the full platform first.&lt;/p&gt;

&lt;p&gt;Sell a narrow outcome.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;h3&gt;
  
  
  For a course platform
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Automatically split every crypto course sale between the platform, instructor, and affiliate, then generate weekly payout batches with partner balance reports.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  For an agency
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Track revenue from client payments, calculate contractor shares, and create monthly crypto payout batches with approval logs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  For a creator marketplace
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Let creators see their earned balance and receive approved crypto payouts after customer payments are confirmed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  For an affiliate program
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Calculate affiliate commission from paid crypto invoices and create payout queues after a minimum threshold is reached.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The more specific the outcome, the easier it is to sell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical roadmap: 21-day build plan
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Days 1-3: Define the niche
&lt;/h3&gt;

&lt;p&gt;Pick one use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;course platform&lt;/li&gt;
&lt;li&gt;affiliate program&lt;/li&gt;
&lt;li&gt;small marketplace&lt;/li&gt;
&lt;li&gt;agency contractor payouts&lt;/li&gt;
&lt;li&gt;creator platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Define one split rule.&lt;/p&gt;

&lt;p&gt;Do not build every split model yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Days 4-6: Build payment intake
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;create merchant/order model&lt;/li&gt;
&lt;li&gt;create OxaPay invoice&lt;/li&gt;
&lt;li&gt;store invoice response&lt;/li&gt;
&lt;li&gt;add payment webhook endpoint&lt;/li&gt;
&lt;li&gt;validate HMAC&lt;/li&gt;
&lt;li&gt;store raw webhook events&lt;/li&gt;
&lt;li&gt;process paid events idempotently&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 7-9: Build ledger and split engine
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;create ledger schema&lt;/li&gt;
&lt;li&gt;create one split rule type&lt;/li&gt;
&lt;li&gt;apply split after paid payment&lt;/li&gt;
&lt;li&gt;calculate partner balances from ledger entries&lt;/li&gt;
&lt;li&gt;add tests for duplicate webhooks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 10-12: Build partner and payout method management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;add partner records&lt;/li&gt;
&lt;li&gt;add payout address records&lt;/li&gt;
&lt;li&gt;add admin verification flow&lt;/li&gt;
&lt;li&gt;add audit logs for address changes&lt;/li&gt;
&lt;li&gt;show partner balances&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 13-15: Build payout queue
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;create payout batches&lt;/li&gt;
&lt;li&gt;add minimum payout threshold&lt;/li&gt;
&lt;li&gt;add manual approval&lt;/li&gt;
&lt;li&gt;create payout items&lt;/li&gt;
&lt;li&gt;show admin review screen&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 16-18: Execute payouts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;call OxaPay Generate Payout&lt;/li&gt;
&lt;li&gt;store provider track ID&lt;/li&gt;
&lt;li&gt;add payout webhook endpoint&lt;/li&gt;
&lt;li&gt;validate payout HMAC&lt;/li&gt;
&lt;li&gt;update payout status&lt;/li&gt;
&lt;li&gt;create payout debit ledger entries idempotently&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 19-21: Make it sellable
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;add CSV export&lt;/li&gt;
&lt;li&gt;add partner dashboard&lt;/li&gt;
&lt;li&gt;add merchant dashboard&lt;/li&gt;
&lt;li&gt;add failed payout review screen&lt;/li&gt;
&lt;li&gt;add audit log view&lt;/li&gt;
&lt;li&gt;add a demo dataset&lt;/li&gt;
&lt;li&gt;create a landing page&lt;/li&gt;
&lt;li&gt;record a demo video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end of 21 days, you should not have a perfect product.&lt;/p&gt;

&lt;p&gt;You should have a sellable prototype.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing checklist
&lt;/h2&gt;

&lt;p&gt;Before showing this to a merchant, test these cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice created successfully&lt;/li&gt;
&lt;li&gt;payment webhook validates HMAC&lt;/li&gt;
&lt;li&gt;invalid webhook signature is rejected&lt;/li&gt;
&lt;li&gt;duplicate payment webhook does not duplicate ledger entries&lt;/li&gt;
&lt;li&gt;payment status before paid does not create partner balances&lt;/li&gt;
&lt;li&gt;paid payment creates correct ledger entries&lt;/li&gt;
&lt;li&gt;split rule percentages are validated&lt;/li&gt;
&lt;li&gt;rounding is handled consistently&lt;/li&gt;
&lt;li&gt;partner balance is calculated from ledger entries&lt;/li&gt;
&lt;li&gt;payout address must be verified before use&lt;/li&gt;
&lt;li&gt;payout batch requires approval&lt;/li&gt;
&lt;li&gt;payout API key is never exposed to frontend&lt;/li&gt;
&lt;li&gt;payout webhook validates with payout API key&lt;/li&gt;
&lt;li&gt;duplicate payout webhook does not duplicate debit entries&lt;/li&gt;
&lt;li&gt;rejected payout moves to review state&lt;/li&gt;
&lt;li&gt;canceled payout moves to review state&lt;/li&gt;
&lt;li&gt;payout history reconciliation detects missing records&lt;/li&gt;
&lt;li&gt;admin actions create audit logs&lt;/li&gt;
&lt;li&gt;partner cannot access another partner's balance&lt;/li&gt;
&lt;li&gt;support can trace payment → split → payout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This checklist is part of the product.&lt;/p&gt;

&lt;p&gt;A merchant buying payout infrastructure wants reliability, not just features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes to avoid
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Sending payout directly from the payment webhook
&lt;/h3&gt;

&lt;p&gt;This is the biggest mistake.&lt;/p&gt;

&lt;p&gt;Payment confirmation and payout execution should be separated by a ledger and payout queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: No audit trail
&lt;/h3&gt;

&lt;p&gt;If you cannot explain why a partner balance changed, the merchant will not trust the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Editable balances
&lt;/h3&gt;

&lt;p&gt;Balances should be derived from ledger entries.&lt;/p&gt;

&lt;p&gt;Manual correction should be an adjustment entry, not a direct balance edit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: No approval workflow
&lt;/h3&gt;

&lt;p&gt;Automated payouts sound nice, but most merchants need review controls first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Weak authorization
&lt;/h3&gt;

&lt;p&gt;A partner viewing another partner's balance is a serious security issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 6: Ignoring payout address changes
&lt;/h3&gt;

&lt;p&gt;Changing a payout address should be treated as a sensitive event.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 7: No reconciliation
&lt;/h3&gt;

&lt;p&gt;Webhooks are not a full accounting system.&lt;/p&gt;

&lt;p&gt;Use provider history endpoints to detect mismatches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 8: Overbuilding before selling
&lt;/h3&gt;

&lt;p&gt;Start with one niche and one split model.&lt;/p&gt;

&lt;p&gt;Do not build a general payout platform before validating demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Legal, tax, and compliance boundaries
&lt;/h2&gt;

&lt;p&gt;This article is a technical and product-building guide, not legal, tax, or financial advice.&lt;/p&gt;

&lt;p&gt;Revenue split and payout systems can touch sensitive obligations depending on jurisdiction, business type, custody model, merchant-of-record structure, tax reporting, sanctions rules, partner onboarding, and the assets involved.&lt;/p&gt;

&lt;p&gt;A developer should not casually promise that the software solves compliance.&lt;/p&gt;

&lt;p&gt;A safer positioning is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This product helps merchants calculate, queue, approve, execute, and track crypto payouts using their own payment infrastructure and business rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Do not position yourself as the legal owner of funds unless you are prepared for that responsibility.&lt;/p&gt;

&lt;p&gt;For many developers, the safer business model is implementation, software, dashboard, automation, and operational tooling for the merchant, where the merchant owns the commercial relationship and payout policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Landing page positioning
&lt;/h2&gt;

&lt;p&gt;A weak landing page headline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Crypto payout API integration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A stronger headline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Split crypto revenue between sellers, creators, affiliates, or contractors with a ledger, payout queue, approval flow, and payout tracking.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A simple landing page should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;target niche&lt;/li&gt;
&lt;li&gt;revenue split problem&lt;/li&gt;
&lt;li&gt;payment-to-payout flow diagram&lt;/li&gt;
&lt;li&gt;dashboard screenshots&lt;/li&gt;
&lt;li&gt;example split rules&lt;/li&gt;
&lt;li&gt;payout approval workflow&lt;/li&gt;
&lt;li&gt;audit log example&lt;/li&gt;
&lt;li&gt;security controls&lt;/li&gt;
&lt;li&gt;OxaPay infrastructure used&lt;/li&gt;
&lt;li&gt;pricing packages&lt;/li&gt;
&lt;li&gt;demo video&lt;/li&gt;
&lt;li&gt;implementation timeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The visitor should think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This developer understands payout operations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This developer can call an API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A crypto revenue split and payout system is one of the stronger developer business ideas because the merchant problem is painful, recurring, and operational.&lt;/p&gt;

&lt;p&gt;The opportunity is not simply sending crypto payouts.&lt;/p&gt;

&lt;p&gt;The opportunity is building the layer that turns customer payments into trusted partner balances and controlled payout workflows.&lt;/p&gt;

&lt;p&gt;OxaPay provides the useful infrastructure primitives: invoice creation, callback URLs, webhooks, payment history, payout creation, payout information, payout history, payout status tracking, SDKs, and automation modules.&lt;/p&gt;

&lt;p&gt;But your product must provide the business logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;split rules&lt;/li&gt;
&lt;li&gt;ledger entries&lt;/li&gt;
&lt;li&gt;payout queues&lt;/li&gt;
&lt;li&gt;approval workflows&lt;/li&gt;
&lt;li&gt;partner dashboards&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;li&gt;security controls&lt;/li&gt;
&lt;li&gt;reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what merchants pay for.&lt;/p&gt;

&lt;p&gt;Start with one niche.&lt;/p&gt;

&lt;p&gt;Pick one split rule.&lt;/p&gt;

&lt;p&gt;Build the ledger first.&lt;/p&gt;

&lt;p&gt;Queue payouts before sending them.&lt;/p&gt;

&lt;p&gt;Validate every webhook.&lt;/p&gt;

&lt;p&gt;Keep audit logs.&lt;/p&gt;

&lt;p&gt;Make partner balances explainable.&lt;/p&gt;

&lt;p&gt;Then turn the pattern into a productized service.&lt;/p&gt;

&lt;p&gt;That is how a simple payout integration becomes a real developer business.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout" rel="noopener noreferrer"&gt;OxaPay Payout Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/generate-payout" rel="noopener noreferrer"&gt;OxaPay Generate Payout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-information" rel="noopener noreferrer"&gt;OxaPay Payout Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-history" rel="noopener noreferrer"&gt;OxaPay Payout History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-status-table" rel="noopener noreferrer"&gt;OxaPay Payout Status Table&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;OxaPay PHP SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;OxaPay Laravel SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.make.com/oxapay-crypto-pay-gtw" rel="noopener noreferrer"&gt;OxaPay Make Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.stripe.com/connect/separate-charges-and-transfers" rel="noopener noreferrer"&gt;Stripe Connect: Separate Charges and Transfers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe API: Idempotent Requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/" rel="noopener noreferrer"&gt;OWASP API1:2023 — Broken Object Level Authorization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>crypto</category>
      <category>backend</category>
    </item>
    <item>
      <title>Build a Telegram Paid Access System with Crypto Payments</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Tue, 14 Jul 2026 05:24:23 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-telegram-paid-access-system-with-crypto-payments-60l</link>
      <guid>https://dev.to/kevins1988/build-a-telegram-paid-access-system-with-crypto-payments-60l</guid>
      <description>&lt;p&gt;Telegram is full of communities that already sell access informally.&lt;/p&gt;

&lt;p&gt;Some sell private trading groups. Some sell language classes. Some sell paid newsletters, templates, software access, private research, coaching groups, digital files, signals, courses, or creator communities.&lt;/p&gt;

&lt;p&gt;Many of them already monetize.&lt;/p&gt;

&lt;p&gt;But their operations are often fragile.&lt;/p&gt;

&lt;p&gt;A user sends money. An admin checks a screenshot. Someone manually adds the user to a private channel. Another admin tracks renewal dates in a spreadsheet. Expired users stay inside the group for too long. Paid users sometimes wait hours for access. Support messages pile up because nobody can quickly answer whether a payment was received, expired, underpaid, or still confirming.&lt;/p&gt;

&lt;p&gt;That is the business opportunity.&lt;/p&gt;

&lt;p&gt;Not simply a Telegram bot.&lt;/p&gt;

&lt;p&gt;Not simply a crypto payment link.&lt;/p&gt;

&lt;p&gt;The real opportunity is to build a &lt;strong&gt;paid access system&lt;/strong&gt; that connects payment, membership, renewal, expiration, access control, and support visibility.&lt;/p&gt;

&lt;p&gt;In this article, we will use &lt;a href="https://oxapay.com/" rel="noopener noreferrer"&gt;OxaPay&lt;/a&gt; as the example crypto payment infrastructure because its documentation includes the primitives a developer needs for this kind of product: hosted invoices, callback URLs, webhooks, payment information, payment history, SDKs, n8n automation, Make automation, and Telegram bot workflow examples.&lt;/p&gt;

&lt;p&gt;Telegram provides the community surface. OxaPay provides the crypto payment layer. Your product sits between them and turns payment events into membership events.&lt;/p&gt;

&lt;p&gt;That is where a developer can build a real merchant-facing business.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Telegram paid access system&lt;/strong&gt; lets a community owner charge users for access to a private Telegram channel, group, course, file drop, or VIP community.&lt;/p&gt;

&lt;p&gt;The system should handle the full lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;user starts the bot&lt;/li&gt;
&lt;li&gt;user selects a plan&lt;/li&gt;
&lt;li&gt;backend creates a crypto invoice&lt;/li&gt;
&lt;li&gt;user pays the invoice&lt;/li&gt;
&lt;li&gt;OxaPay sends a webhook update&lt;/li&gt;
&lt;li&gt;backend validates the webhook&lt;/li&gt;
&lt;li&gt;subscription becomes active&lt;/li&gt;
&lt;li&gt;Telegram access is granted&lt;/li&gt;
&lt;li&gt;renewal reminders are sent&lt;/li&gt;
&lt;li&gt;expired members are removed or restricted&lt;/li&gt;
&lt;li&gt;admins can search payment and membership status&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The developer is not selling:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can create a Telegram bot that sends a payment link.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The developer is selling:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can build a paid access system that sells membership, confirms crypto payments, grants Telegram access, manages expiry, reduces manual admin work, and gives you a searchable payment trail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;A payment link is a feature. Access management is a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a real developer business
&lt;/h2&gt;

&lt;p&gt;Most paid Telegram communities are run by creators, educators, operators, marketers, traders, coaches, or small digital businesses. They often have an audience, but they do not have a payment engineering team.&lt;/p&gt;

&lt;p&gt;Their problem is not only “how do I accept payment?”&lt;/p&gt;

&lt;p&gt;Their real problems are operational:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do I know who paid?&lt;/li&gt;
&lt;li&gt;How do I prevent unpaid users from joining?&lt;/li&gt;
&lt;li&gt;How do I remove expired members?&lt;/li&gt;
&lt;li&gt;How do I stop invite links from being shared?&lt;/li&gt;
&lt;li&gt;How do I handle failed, expired, or underpaid payments?&lt;/li&gt;
&lt;li&gt;How do I remind users before access expires?&lt;/li&gt;
&lt;li&gt;How do I give admins a clean view of who is active?&lt;/li&gt;
&lt;li&gt;How do I avoid manually checking wallets and screenshots?&lt;/li&gt;
&lt;li&gt;How do I sell weekly, monthly, quarterly, and lifetime plans?&lt;/li&gt;
&lt;li&gt;How do I run the business when payment volume grows?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly where a developer can productize the solution.&lt;/p&gt;

&lt;p&gt;Telegram gives developers a mature Bot API. The Bot API lets bots create invite links, receive chat member updates, approve join requests, and manage members when the bot has the required administrator permissions. For example, Telegram documents &lt;code&gt;createChatInviteLink&lt;/code&gt;, including parameters such as &lt;code&gt;expire_date&lt;/code&gt; and &lt;code&gt;member_limit&lt;/code&gt;, and states that the bot must be an administrator with the appropriate rights. Telegram also documents &lt;code&gt;revokeChatInviteLink&lt;/code&gt;, &lt;code&gt;approveChatJoinRequest&lt;/code&gt;, and chat member update objects. See the official &lt;a href="https://core.telegram.org/bots/api" rel="noopener noreferrer"&gt;Telegram Bot API&lt;/a&gt; reference.&lt;/p&gt;

&lt;p&gt;OxaPay gives developers the payment side. Its &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;Generate Invoice&lt;/a&gt; endpoint creates a payment URL and supports fields such as amount, currency, lifetime, callback URL, return URL, order ID, description, and sandbox mode. Its &lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;Webhook&lt;/a&gt; documentation explains how to receive payment callbacks and validate HMAC signatures. Its &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;Payment Information&lt;/a&gt; and &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;Payment History&lt;/a&gt; endpoints help you build status pages, admin tools, and reconciliation screens.&lt;/p&gt;

&lt;p&gt;OxaPay also documents no-code and low-code Telegram automation examples through &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/n8n-automation/telegram-bot" rel="noopener noreferrer"&gt;n8n Telegram Bot&lt;/a&gt; and &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation/telegram-bot" rel="noopener noreferrer"&gt;Make Telegram Bot&lt;/a&gt; workflows. Those are useful signals: this workflow is not theoretical. Telegram payment automation is a documented use case.&lt;/p&gt;

&lt;p&gt;The business is not “accept crypto in Telegram.”&lt;/p&gt;

&lt;p&gt;The business is “run paid Telegram access without manual payment operations.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Who would pay for this?
&lt;/h2&gt;

&lt;p&gt;The best buyers are communities where access itself is the product.&lt;/p&gt;

&lt;p&gt;Good targets include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Buyer&lt;/th&gt;
&lt;th&gt;What they sell&lt;/th&gt;
&lt;th&gt;Why they need this&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trading communities&lt;/td&gt;
&lt;td&gt;VIP channel, signals, market commentary&lt;/td&gt;
&lt;td&gt;They need fast activation, renewal tracking, and expired member removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Course creators&lt;/td&gt;
&lt;td&gt;Private class group, study cohort, premium lessons&lt;/td&gt;
&lt;td&gt;They need paid onboarding and access control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newsletter operators&lt;/td&gt;
&lt;td&gt;Private Telegram delivery channel&lt;/td&gt;
&lt;td&gt;They need subscription plans and renewals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coaches and consultants&lt;/td&gt;
&lt;td&gt;Paid group calls, private Q&amp;amp;A, weekly reports&lt;/td&gt;
&lt;td&gt;They need simple checkout and member visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Software sellers&lt;/td&gt;
&lt;td&gt;License keys, bot access, private support group&lt;/td&gt;
&lt;td&gt;They need payment-to-access automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital product sellers&lt;/td&gt;
&lt;td&gt;Templates, files, PDFs, source code, research drops&lt;/td&gt;
&lt;td&gt;They need automatic delivery after payment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creator communities&lt;/td&gt;
&lt;td&gt;Premium chat, private updates, early access&lt;/td&gt;
&lt;td&gt;They need recurring community monetization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agencies&lt;/td&gt;
&lt;td&gt;Paid client communities or customer support channels&lt;/td&gt;
&lt;td&gt;They need a reusable launch package for multiple clients&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key qualification is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does the merchant lose time or revenue because payment and access are handled manually?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If yes, a paid access system can be sold as a practical business tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can sell
&lt;/h2&gt;

&lt;p&gt;A developer can package this product in several ways.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offer&lt;/th&gt;
&lt;th&gt;What the merchant gets&lt;/th&gt;
&lt;th&gt;Revenue model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Telegram paid access setup&lt;/td&gt;
&lt;td&gt;Bot, invoice flow, webhook handling, and invite link automation&lt;/td&gt;
&lt;td&gt;One-time setup fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed community billing&lt;/td&gt;
&lt;td&gt;Ongoing payment monitoring, expiry checks, renewal reminders, and admin support&lt;/td&gt;
&lt;td&gt;Monthly retainer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted paid access SaaS&lt;/td&gt;
&lt;td&gt;A reusable dashboard for multiple Telegram communities&lt;/td&gt;
&lt;td&gt;Monthly subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White-label agency kit&lt;/td&gt;
&lt;td&gt;A reusable bot/dashboard package agencies can sell to clients&lt;/td&gt;
&lt;td&gt;License + support fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom automation package&lt;/td&gt;
&lt;td&gt;Payment connected to CRM, Sheets, email, Notion, Discord, or internal tools&lt;/td&gt;
&lt;td&gt;Setup + maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium support dashboard&lt;/td&gt;
&lt;td&gt;Payment search, user search, membership timeline, issue categories&lt;/td&gt;
&lt;td&gt;Per-community or per-admin pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The simplest path is not to build a full SaaS on day one.&lt;/p&gt;

&lt;p&gt;The practical path is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;build one custom implementation for one community,&lt;/li&gt;
&lt;li&gt;identify repeated requirements,&lt;/li&gt;
&lt;li&gt;turn the repeated parts into reusable modules,&lt;/li&gt;
&lt;li&gt;sell the same system to similar communities,&lt;/li&gt;
&lt;li&gt;add a dashboard only when operations become repetitive enough.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A common mistake is building a generic bot first.&lt;/p&gt;

&lt;p&gt;A better approach is selling a productized service first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical architecture
&lt;/h2&gt;

&lt;p&gt;A production-grade Telegram paid access system has five layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Telegram user
    |
    | /start, select plan, request access
    v
Telegram bot
    |
    | creates checkout session
    v
Paid access backend
    |
    | creates OxaPay invoice with callback_url + order_id
    v
OxaPay invoice
    |
    | user pays crypto invoice
    v
OxaPay webhook
    |
    | validate HMAC, update payment state
    v
Membership engine
    |
    | activate subscription, create invite link, send access
    v
Telegram private group / channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical design principle is that &lt;strong&gt;payment state and membership state must be separate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A payment can be created, paid, failed, expired, or disputed internally.&lt;/p&gt;

&lt;p&gt;A membership can be pending, active, grace, expired, revoked, banned, or manually extended.&lt;/p&gt;

&lt;p&gt;Do not collapse those into one boolean field like &lt;code&gt;is_paid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That shortcut breaks quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OxaPay primitives used
&lt;/h2&gt;

&lt;p&gt;The minimal OxaPay setup uses hosted invoices and webhooks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OxaPay primitive&lt;/th&gt;
&lt;th&gt;Role in this product&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;Generate Invoice&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Create a payment link for the selected Telegram access plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;callback_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tell OxaPay where to send payment status updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;order_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Store your internal subscription or checkout ID in the payment request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lifetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Control how long the payment link should remain valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;Webhook&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Receive payment updates and trigger membership activation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMAC validation&lt;/td&gt;
&lt;td&gt;Verify that webhook callbacks are authentic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;Payment Information&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Fetch a specific payment by &lt;code&gt;track_id&lt;/code&gt; for support and recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;Payment History&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Build admin dashboards, payment search, and reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/n8n-automation/telegram-bot" rel="noopener noreferrer"&gt;n8n Telegram Bot integration&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Useful for prototypes or low-code workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation/telegram-bot" rel="noopener noreferrer"&gt;Make Telegram Bot integration&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Useful for non-code automation and merchant demos&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the first version, hosted invoices are usually enough.&lt;/p&gt;

&lt;p&gt;White-label payment is useful later if you want to keep the full payment experience inside your own interface or mini app. OxaPay's &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;Generate White Label&lt;/a&gt; endpoint returns payment details such as address, currency, amount, network, and expiration information, allowing you to manage the payment process inside your own UI.&lt;/p&gt;

&lt;p&gt;Static addresses are usually not the first choice for paid access subscriptions, because plan-based access normally needs a clear invoice per membership period. But static addresses can be useful for account balance top-ups or wallet-like community credits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telegram access control model
&lt;/h2&gt;

&lt;p&gt;There are two practical access models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model 1: one-time invite links
&lt;/h3&gt;

&lt;p&gt;After payment is confirmed, your backend creates a Telegram invite link with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;short expiration time&lt;/li&gt;
&lt;li&gt;&lt;code&gt;member_limit = 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;clear internal link name&lt;/li&gt;
&lt;li&gt;mapping to the payment ID and Telegram user ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the bot sends the invite link to the user.&lt;/p&gt;

&lt;p&gt;This is simple and works well for many private groups and channels.&lt;/p&gt;

&lt;p&gt;Risks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the user can forward the link before joining&lt;/li&gt;
&lt;li&gt;you need to detect whether the expected user actually joined&lt;/li&gt;
&lt;li&gt;invite link abuse must be monitored&lt;/li&gt;
&lt;li&gt;expired links need cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Telegram's &lt;code&gt;createChatInviteLink&lt;/code&gt; supports expiration and member limits. The bot must be an administrator with the needed permissions. See &lt;a href="https://core.telegram.org/bots/api#createchatinvitelink" rel="noopener noreferrer"&gt;Telegram Bot API: createChatInviteLink&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model 2: join request approval
&lt;/h3&gt;

&lt;p&gt;Instead of sending a simple invite link, the user requests access and the bot approves the join request only if the subscription is active.&lt;/p&gt;

&lt;p&gt;This is stricter.&lt;/p&gt;

&lt;p&gt;The bot can receive &lt;code&gt;chat_join_request&lt;/code&gt; updates, and Telegram documents methods such as &lt;code&gt;approveChatJoinRequest&lt;/code&gt; and &lt;code&gt;declineChatJoinRequest&lt;/code&gt;. This requires the bot to have the correct admin rights.&lt;/p&gt;

&lt;p&gt;This model is better for higher-value communities because you can match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegram user ID&lt;/li&gt;
&lt;li&gt;subscription ID&lt;/li&gt;
&lt;li&gt;payment ID&lt;/li&gt;
&lt;li&gt;requested channel or group&lt;/li&gt;
&lt;li&gt;plan validity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A developer product can support both.&lt;/p&gt;

&lt;p&gt;Start with one-time invite links for the MVP. Add join request approval for serious merchants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suggested database schema
&lt;/h2&gt;

&lt;p&gt;Here is a simple relational schema for the MVP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;telegram_chat_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_merchant_api_key_encrypted&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_webhook_secret_encrypted&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;plans&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price_currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;duration_days&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;telegram_users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;telegram_user_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;telegram_user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;plan_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;telegram_user_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;telegram_users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- pending, active, grace, expired, revoked&lt;/span&gt;
  &lt;span class="n"&gt;starts_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_payment_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;subscription_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- created, paying, paid, expired, failed, ignored&lt;/span&gt;
  &lt;span class="n"&gt;invoice_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_provider_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;invite_links&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;subscription_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;telegram_invite_link&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expected_telegram_user_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- created, sent, used, expired, revoked&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;audit_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;entity_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema is intentionally operational.&lt;/p&gt;

&lt;p&gt;It gives you enough structure to answer support questions later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which plan did the user buy?&lt;/li&gt;
&lt;li&gt;Which invoice did they pay?&lt;/li&gt;
&lt;li&gt;Which Telegram account received the invite?&lt;/li&gt;
&lt;li&gt;When does access expire?&lt;/li&gt;
&lt;li&gt;Was the invite used?&lt;/li&gt;
&lt;li&gt;Was the user removed manually or by the expiry job?&lt;/li&gt;
&lt;li&gt;What webhook payload changed the state?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions are the difference between a demo bot and a paid product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payment and membership state machine
&lt;/h2&gt;

&lt;p&gt;Use explicit state transitions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checkout created
    -&amp;gt; invoice created
    -&amp;gt; waiting for payment
    -&amp;gt; payment callback received
    -&amp;gt; payment verified
    -&amp;gt; subscription activated
    -&amp;gt; invite link generated
    -&amp;gt; invite sent
    -&amp;gt; user joined
    -&amp;gt; renewal reminder sent
    -&amp;gt; grace period
    -&amp;gt; expired
    -&amp;gt; access revoked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separate payment state from membership state.&lt;/p&gt;

&lt;p&gt;A payment can be &lt;code&gt;paid&lt;/code&gt; while invite delivery fails.&lt;/p&gt;

&lt;p&gt;A membership can be &lt;code&gt;active&lt;/code&gt; while a renewal payment is pending.&lt;/p&gt;

&lt;p&gt;A user can be removed even if payment history remains valid.&lt;/p&gt;

&lt;p&gt;This separation prevents messy edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  MVP user flow
&lt;/h2&gt;

&lt;p&gt;Here is a practical MVP flow.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User opens the bot and sends &lt;code&gt;/start&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Bot shows available plans.&lt;/li&gt;
&lt;li&gt;User selects a plan.&lt;/li&gt;
&lt;li&gt;Backend creates a pending subscription.&lt;/li&gt;
&lt;li&gt;Backend creates an OxaPay invoice.&lt;/li&gt;
&lt;li&gt;Bot sends the payment URL.&lt;/li&gt;
&lt;li&gt;User pays.&lt;/li&gt;
&lt;li&gt;OxaPay sends a webhook to your backend.&lt;/li&gt;
&lt;li&gt;Backend validates the HMAC signature.&lt;/li&gt;
&lt;li&gt;Backend checks payment status and &lt;code&gt;order_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Backend activates the subscription.&lt;/li&gt;
&lt;li&gt;Backend creates a one-time Telegram invite link.&lt;/li&gt;
&lt;li&gt;Bot sends the invite link to the user.&lt;/li&gt;
&lt;li&gt;A scheduled job checks expiry dates daily or hourly.&lt;/li&gt;
&lt;li&gt;Expired users are removed, restricted, or marked for admin review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This MVP is already sellable if it works reliably.&lt;/p&gt;

&lt;p&gt;Do not start with a complex dashboard.&lt;/p&gt;

&lt;p&gt;Start with reliable access control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an OxaPay invoice
&lt;/h2&gt;

&lt;p&gt;This example uses Node.js and &lt;code&gt;fetch&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOxaPayInvoice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;callbackUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;returnUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1/payment/invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callbackUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;returnUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OxaPay invoice error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;order_id&lt;/code&gt; should be a stable internal ID.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;telegram_access:merchant_123:subscription_456:payment_789
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not depend only on the amount or Telegram username.&lt;/p&gt;

&lt;p&gt;Usernames can change. Amounts can collide. Screenshots can lie.&lt;/p&gt;

&lt;p&gt;Use internal IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending the invoice in Telegram
&lt;/h2&gt;

&lt;p&gt;Here is a simplified bot handler using Telegraf.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Telegraf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Markup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;telegraf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Telegraf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;upsertTelegramUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Choose your access plan:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;Markup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inlineKeyboard&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Markup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;7 days - $15&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plan:weekly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Markup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;30 days - $39&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plan:monthly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Markup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;90 days - $99&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plan:quarterly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^plan:&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;planCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;telegramUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;upsertTelegramUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findPlanByCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;planCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPendingSubscription&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;planId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;telegramUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;telegramUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`tg_access:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPaymentRecord&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createOxaPayInvoice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;merchantApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;callbackUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;returnUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment/success`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Telegram access`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updatePaymentWithInvoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;oxapayTrackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;invoiceUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`Pay here to activate your &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; access:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\nYour access will be sent automatically after payment confirmation.`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;answerCbQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact response field names should be checked against the current OxaPay response format in your environment.&lt;/p&gt;

&lt;p&gt;The important architecture is the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create internal payment record first&lt;/li&gt;
&lt;li&gt;create OxaPay invoice second&lt;/li&gt;
&lt;li&gt;persist &lt;code&gt;track_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;send invoice URL to the Telegram user&lt;/li&gt;
&lt;li&gt;activate access only after webhook verification&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Validating OxaPay webhooks
&lt;/h2&gt;

&lt;p&gt;For paid access, webhook security is not optional.&lt;/p&gt;

&lt;p&gt;If someone can fake a webhook, they can fake a payment and enter a paid group.&lt;/p&gt;

&lt;p&gt;OxaPay's webhook documentation says you should validate the HMAC signature. Its Python SDK documentation states that OxaPay sends an &lt;code&gt;HMAC&lt;/code&gt; header using sha512 over the raw request body.&lt;/p&gt;

&lt;p&gt;Here is a simplified Express example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedSignature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedSignature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_API_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;receivedSignature&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;receivedSignature&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expectedSignature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleOxaPayPaymentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful with middleware.&lt;/p&gt;

&lt;p&gt;If you parse JSON before validating the raw request body, the HMAC check may fail or become unsafe.&lt;/p&gt;

&lt;p&gt;For webhook processing, store the raw payload in an audit table.&lt;/p&gt;

&lt;p&gt;That helps with disputes, support, and debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotent webhook handling
&lt;/h2&gt;

&lt;p&gt;Webhooks can be retried.&lt;/p&gt;

&lt;p&gt;Networks can fail.&lt;/p&gt;

&lt;p&gt;Your server can process the same event twice.&lt;/p&gt;

&lt;p&gt;Your handler must be idempotent.&lt;/p&gt;

&lt;p&gt;Do not write logic like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;activateSubscription&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;sendInviteLink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can create duplicate invite links, duplicate access extensions, or duplicate messages.&lt;/p&gt;

&lt;p&gt;Instead, use a transaction and state checks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleOxaPayPaymentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByOrderId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auditEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown_payment_webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeOxaPayStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;raw_provider_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// already processed&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isPaidStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateNewExpiry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;starts_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;starts_at&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;last_payment_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grant_telegram_access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use an outbox table for side effects.&lt;/p&gt;

&lt;p&gt;Do not call Telegram inside the same database transaction if you can avoid it.&lt;/p&gt;

&lt;p&gt;A better pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;webhook updates payment and subscription state&lt;/li&gt;
&lt;li&gt;webhook creates an outbox job&lt;/li&gt;
&lt;li&gt;worker creates the Telegram invite link&lt;/li&gt;
&lt;li&gt;worker sends the invite to the user&lt;/li&gt;
&lt;li&gt;worker records success or failure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That makes retries safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a one-time Telegram invite link
&lt;/h2&gt;

&lt;p&gt;Telegram lets bots create additional invite links when they have the right admin rights.&lt;/p&gt;

&lt;p&gt;Here is a simplified call using the Telegram Bot API directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOneTimeInviteLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;botToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;expireAtUnix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.telegram.org/bot&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;botToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/createChatInviteLink`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;expire_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expireAtUnix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;member_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Telegram invite error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invite_link&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a paid webhook, create the link and send it to the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;grantTelegramAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getTelegramUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telegram_user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMerchant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inviteLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createOneTimeInviteLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;botToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELEGRAM_BOT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telegram_chat_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`sub_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;expireAtUnix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveInviteLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paymentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expectedTelegramUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telegram_user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;telegramInviteLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inviteLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telegram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telegram_user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`Payment confirmed. Use this one-time invite link to join:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inviteLink&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\nThis link expires in 15 minutes.`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important Telegram constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the user must have started the bot before the bot can message them privately&lt;/li&gt;
&lt;li&gt;the bot must be an admin in the target group or channel&lt;/li&gt;
&lt;li&gt;the bot needs permission to invite users&lt;/li&gt;
&lt;li&gt;for removing users, the bot needs the relevant restriction/ban permissions&lt;/li&gt;
&lt;li&gt;private channel/group IDs should be stored carefully&lt;/li&gt;
&lt;li&gt;one-time links reduce sharing, but do not eliminate all abuse&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tracking joins
&lt;/h2&gt;

&lt;p&gt;You should track whether the paid user actually joined.&lt;/p&gt;

&lt;p&gt;Telegram can send &lt;code&gt;chat_member&lt;/code&gt; updates when a chat member status changes, but the bot must be an administrator and must request the relevant allowed updates.&lt;/p&gt;

&lt;p&gt;Store join events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat_member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat_member&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;new_chat_member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inviteLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invite_link&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;invite_link&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;new_chat_member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;inviteLink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markInviteUsed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;telegramInviteLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inviteLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;telegramUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;joinedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets your admin dashboard answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice paid?&lt;/li&gt;
&lt;li&gt;invite sent?&lt;/li&gt;
&lt;li&gt;invite used?&lt;/li&gt;
&lt;li&gt;expected user joined?&lt;/li&gt;
&lt;li&gt;wrong user joined?&lt;/li&gt;
&lt;li&gt;access active?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the kind of visibility merchants pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expiry and renewal logic
&lt;/h2&gt;

&lt;p&gt;Paid access systems fail when expiry is manual.&lt;/p&gt;

&lt;p&gt;You need scheduled jobs.&lt;/p&gt;

&lt;p&gt;A simple renewal lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;active subscription
    -&amp;gt; 3 days before expiry: send reminder
    -&amp;gt; 1 day before expiry: send final reminder
    -&amp;gt; expiry date: move to grace
    -&amp;gt; grace ended: remove access
    -&amp;gt; payment received later: reactivate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example scheduled worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processExpiringSubscriptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;soon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findSubscriptionsExpiringInDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;soon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reminder_3d_sent_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendRenewalReminder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your access expires in 3 days.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markReminderSent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;3d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findSubscriptionsPastExpiry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;moveToGraceOrRevoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To remove a user, you can use Telegram's member management methods, depending on the chat type and permissions. A common approach is to ban or kick the member from the private group/channel when the subscription is no longer valid, then record the access revocation in your audit log. Always test this in your exact Telegram chat configuration before promising fully automatic removal to merchants.&lt;/p&gt;

&lt;p&gt;The safer product promise is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The system can automate expiry detection and access revocation when the bot has the required Telegram admin permissions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This will remove every expired user in every Telegram setup without edge cases.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference protects your credibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Admin dashboard: the feature merchants will actually love
&lt;/h2&gt;

&lt;p&gt;Developers often overbuild the bot and underbuild the admin view.&lt;/p&gt;

&lt;p&gt;But merchants need visibility.&lt;/p&gt;

&lt;p&gt;A useful dashboard should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;active members&lt;/li&gt;
&lt;li&gt;expired members&lt;/li&gt;
&lt;li&gt;pending payments&lt;/li&gt;
&lt;li&gt;paid invoices&lt;/li&gt;
&lt;li&gt;failed or expired invoices&lt;/li&gt;
&lt;li&gt;invite links sent&lt;/li&gt;
&lt;li&gt;invite links unused&lt;/li&gt;
&lt;li&gt;renewal dates&lt;/li&gt;
&lt;li&gt;users in grace period&lt;/li&gt;
&lt;li&gt;support notes&lt;/li&gt;
&lt;li&gt;webhook event history&lt;/li&gt;
&lt;li&gt;manual extension actions&lt;/li&gt;
&lt;li&gt;revenue by plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The support search is especially important.&lt;/p&gt;

&lt;p&gt;A good support search lets an admin search by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegram username&lt;/li&gt;
&lt;li&gt;Telegram user ID&lt;/li&gt;
&lt;li&gt;OxaPay &lt;code&gt;track_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;internal order ID&lt;/li&gt;
&lt;li&gt;invoice URL&lt;/li&gt;
&lt;li&gt;plan&lt;/li&gt;
&lt;li&gt;payment status&lt;/li&gt;
&lt;li&gt;subscription status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where your product becomes more than a bot.&lt;/p&gt;

&lt;p&gt;It becomes a small payment operations system for Telegram businesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customer status page
&lt;/h2&gt;

&lt;p&gt;Do not force users to ask admins for every payment question.&lt;/p&gt;

&lt;p&gt;A simple status page can reduce support load.&lt;/p&gt;

&lt;p&gt;Example URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yourapp.com/status/tg_access_abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selected plan&lt;/li&gt;
&lt;li&gt;invoice status&lt;/li&gt;
&lt;li&gt;payment instructions&lt;/li&gt;
&lt;li&gt;invoice expiration time&lt;/li&gt;
&lt;li&gt;access status&lt;/li&gt;
&lt;li&gt;invite link delivery status&lt;/li&gt;
&lt;li&gt;next renewal date&lt;/li&gt;
&lt;li&gt;support contact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should not expose sensitive internal data.&lt;/p&gt;

&lt;p&gt;Do not display raw API keys, internal webhook payloads, private chat IDs, or unrelated user data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Low-code version with n8n or Make
&lt;/h2&gt;

&lt;p&gt;Not every developer needs to start with a custom backend.&lt;/p&gt;

&lt;p&gt;OxaPay documents Telegram bot workflows using both n8n and Make.&lt;/p&gt;

&lt;p&gt;The n8n Telegram integration shows a workflow where Telegram triggers can generate payment requests, send invoice links, receive real-time payment status updates, and notify users or admins when a transaction is completed. It also mentions storing payment records in n8n Data Tables, Google Sheets, Airtable, Notion, or a custom database.&lt;/p&gt;

&lt;p&gt;The Make Telegram integration describes scenarios such as sending Telegram messages when payment is completed, generating invoices and delivering them to customers, delivering digital products after successful payment, and alerting users about payment expiration or failure.&lt;/p&gt;

&lt;p&gt;This gives developers two product paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path A: Low-code productized setup
&lt;/h3&gt;

&lt;p&gt;Use n8n or Make for the first merchant.&lt;/p&gt;

&lt;p&gt;Sell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup&lt;/li&gt;
&lt;li&gt;configuration&lt;/li&gt;
&lt;li&gt;workflow customization&lt;/li&gt;
&lt;li&gt;admin training&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is good for small communities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path B: Custom SaaS backend
&lt;/h3&gt;

&lt;p&gt;Build your own backend when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-merchant support&lt;/li&gt;
&lt;li&gt;clean admin dashboard&lt;/li&gt;
&lt;li&gt;stricter invite controls&lt;/li&gt;
&lt;li&gt;more reliable expiry jobs&lt;/li&gt;
&lt;li&gt;better audit logs&lt;/li&gt;
&lt;li&gt;custom pricing plans&lt;/li&gt;
&lt;li&gt;webhook idempotency&lt;/li&gt;
&lt;li&gt;private database&lt;/li&gt;
&lt;li&gt;white-label branding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The low-code path can validate demand. The custom backend can scale the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revenue model
&lt;/h2&gt;

&lt;p&gt;Do not position this as passive income.&lt;/p&gt;

&lt;p&gt;A Telegram paid access system has real support, edge cases, onboarding, and merchant education.&lt;/p&gt;

&lt;p&gt;But it can be monetized in practical ways.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup fee&lt;/td&gt;
&lt;td&gt;Build and configure the bot, OxaPay flow, plans, and admin access&lt;/td&gt;
&lt;td&gt;First client or custom deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly retainer&lt;/td&gt;
&lt;td&gt;Monitor payments, fix issues, adjust plans, manage workflow changes&lt;/td&gt;
&lt;td&gt;Communities with recurring sales&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS subscription&lt;/td&gt;
&lt;td&gt;Merchant pays monthly for hosted dashboard and bot infrastructure&lt;/td&gt;
&lt;td&gt;Productized multi-merchant version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-community pricing&lt;/td&gt;
&lt;td&gt;One price per Telegram group/channel&lt;/td&gt;
&lt;td&gt;Agencies and creators with multiple communities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium support tier&lt;/td&gt;
&lt;td&gt;Faster support, custom reports, advanced workflows&lt;/td&gt;
&lt;td&gt;Higher-volume merchants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue share&lt;/td&gt;
&lt;td&gt;Small percentage of sales&lt;/td&gt;
&lt;td&gt;Early-stage creators with low setup budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A realistic pricing ladder might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Example scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;One Telegram community, one OxaPay invoice flow, one plan, basic access delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Growth&lt;/td&gt;
&lt;td&gt;Multiple plans, renewal reminders, admin dashboard, payment search, expiry automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;Multiple communities, custom branding, support dashboard, analytics, manual override tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency&lt;/td&gt;
&lt;td&gt;White-label deployment, reusable templates, client onboarding docs, support playbook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Avoid promising specific revenue.&lt;/p&gt;

&lt;p&gt;Instead, explain that revenue depends on merchant volume, niche urgency, support quality, trust, and distribution.&lt;/p&gt;

&lt;p&gt;The product is easier to sell when the merchant already has an audience and already charges for access manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  MVP scope
&lt;/h2&gt;

&lt;p&gt;A strong MVP should include only the features needed to replace manual access management.&lt;/p&gt;

&lt;p&gt;Build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegram bot &lt;code&gt;/start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;plan selection&lt;/li&gt;
&lt;li&gt;OxaPay invoice creation&lt;/li&gt;
&lt;li&gt;webhook receiver with HMAC validation&lt;/li&gt;
&lt;li&gt;payment record storage&lt;/li&gt;
&lt;li&gt;subscription activation&lt;/li&gt;
&lt;li&gt;one-time invite link generation&lt;/li&gt;
&lt;li&gt;renewal reminder job&lt;/li&gt;
&lt;li&gt;expiry job&lt;/li&gt;
&lt;li&gt;admin notification channel&lt;/li&gt;
&lt;li&gt;simple admin search page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not build yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;complex analytics&lt;/li&gt;
&lt;li&gt;affiliate tracking&lt;/li&gt;
&lt;li&gt;multi-currency treasury tools&lt;/li&gt;
&lt;li&gt;public marketplace&lt;/li&gt;
&lt;li&gt;advanced CRM&lt;/li&gt;
&lt;li&gt;mobile app&lt;/li&gt;
&lt;li&gt;AI support bot&lt;/li&gt;
&lt;li&gt;complex coupon logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can add those after merchants use the core product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production version
&lt;/h2&gt;

&lt;p&gt;A production version should add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-merchant support&lt;/li&gt;
&lt;li&gt;encrypted OxaPay credentials&lt;/li&gt;
&lt;li&gt;merchant-specific webhook URLs&lt;/li&gt;
&lt;li&gt;idempotency keys&lt;/li&gt;
&lt;li&gt;webhook retry handling&lt;/li&gt;
&lt;li&gt;outbox jobs for Telegram actions&lt;/li&gt;
&lt;li&gt;full audit log&lt;/li&gt;
&lt;li&gt;admin roles&lt;/li&gt;
&lt;li&gt;manual subscription extension&lt;/li&gt;
&lt;li&gt;manual access revocation&lt;/li&gt;
&lt;li&gt;failed invite recovery&lt;/li&gt;
&lt;li&gt;customer status page&lt;/li&gt;
&lt;li&gt;payment history sync&lt;/li&gt;
&lt;li&gt;daily revenue report&lt;/li&gt;
&lt;li&gt;renewal reminder templates&lt;/li&gt;
&lt;li&gt;grace period settings&lt;/li&gt;
&lt;li&gt;backup admin alerts&lt;/li&gt;
&lt;li&gt;abuse monitoring&lt;/li&gt;
&lt;li&gt;basic incident logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest upgrade is not UI.&lt;/p&gt;

&lt;p&gt;It is operational reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and operational risks
&lt;/h2&gt;

&lt;p&gt;This business touches payments and access, so you need to design carefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Fake payment callbacks
&lt;/h3&gt;

&lt;p&gt;Always verify webhook signatures.&lt;/p&gt;

&lt;p&gt;Never activate access from an unauthenticated request.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Duplicate webhook events
&lt;/h3&gt;

&lt;p&gt;Make all payment processing idempotent.&lt;/p&gt;

&lt;p&gt;A duplicate callback must not create duplicate access extensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Invite link sharing
&lt;/h3&gt;

&lt;p&gt;Use short-lived invite links with &lt;code&gt;member_limit = 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For high-value groups, add join request approval and compare Telegram user ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Bot permission issues
&lt;/h3&gt;

&lt;p&gt;Before onboarding a merchant, check that the bot has the needed admin rights.&lt;/p&gt;

&lt;p&gt;Create a setup checker.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Username changes
&lt;/h3&gt;

&lt;p&gt;Do not identify users only by username.&lt;/p&gt;

&lt;p&gt;Store Telegram user ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Expired users not removed
&lt;/h3&gt;

&lt;p&gt;Run expiry jobs regularly.&lt;/p&gt;

&lt;p&gt;Log removal failures.&lt;/p&gt;

&lt;p&gt;Notify admins if automatic removal fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Support disputes
&lt;/h3&gt;

&lt;p&gt;Store payment timeline, webhook payload, plan, subscription status, and admin actions.&lt;/p&gt;

&lt;p&gt;A support system without an audit trail becomes painful quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Refund and manual override policy
&lt;/h3&gt;

&lt;p&gt;Decide what happens when a merchant wants to manually extend, revoke, or refund access.&lt;/p&gt;

&lt;p&gt;Even if refunds are handled outside the product, your admin panel should record manual decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Payment status ambiguity
&lt;/h3&gt;

&lt;p&gt;Do not treat every status as final.&lt;/p&gt;

&lt;p&gt;Build a state machine.&lt;/p&gt;

&lt;p&gt;Only grant access when your business rule considers the payment accepted.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Compliance and terms
&lt;/h3&gt;

&lt;p&gt;Developers should avoid promising that crypto payments remove business, tax, platform, or legal obligations.&lt;/p&gt;

&lt;p&gt;Your product should help merchants manage access and payment operations, not bypass rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes this product defensible?
&lt;/h2&gt;

&lt;p&gt;A basic Telegram bot is easy to copy.&lt;/p&gt;

&lt;p&gt;A full paid access system is harder because the value sits in operational details.&lt;/p&gt;

&lt;p&gt;Defensibility comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;niche-specific onboarding&lt;/li&gt;
&lt;li&gt;admin dashboard quality&lt;/li&gt;
&lt;li&gt;reliable webhook handling&lt;/li&gt;
&lt;li&gt;renewal and expiry logic&lt;/li&gt;
&lt;li&gt;support tooling&lt;/li&gt;
&lt;li&gt;clean merchant documentation&lt;/li&gt;
&lt;li&gt;good error handling&lt;/li&gt;
&lt;li&gt;proven templates for specific communities&lt;/li&gt;
&lt;li&gt;trusted implementation&lt;/li&gt;
&lt;li&gt;migration from manual systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The merchant is not buying your code alone.&lt;/p&gt;

&lt;p&gt;They are buying confidence that paid members get access and expired members do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example niche positioning
&lt;/h2&gt;

&lt;p&gt;Here are stronger ways to position the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weak positioning
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;I build Telegram bots with crypto payments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Better positioning
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;I build paid Telegram access systems that connect crypto payments to membership activation, renewal reminders, and expired user removal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Niche-specific positioning
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Crypto-paid Telegram memberships for course creators: sell weekly or monthly access, confirm payments automatically, send one-time invite links, remind users before expiry, and keep a searchable admin dashboard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  High-value community positioning
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Paid access infrastructure for private Telegram communities: invoice creation, webhook verification, access approval, membership expiry, renewal reminders, payment search, and admin audit logs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The more specific you are, the easier it is for a merchant to understand why they should pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to pitch the first client
&lt;/h2&gt;

&lt;p&gt;Do not start by explaining APIs.&lt;/p&gt;

&lt;p&gt;Start by asking operational questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do users currently pay?&lt;/li&gt;
&lt;li&gt;How do you know who paid?&lt;/li&gt;
&lt;li&gt;How do you add users to the private group?&lt;/li&gt;
&lt;li&gt;How do you track expiry dates?&lt;/li&gt;
&lt;li&gt;How many users expire each week?&lt;/li&gt;
&lt;li&gt;How many support messages are about payment or access?&lt;/li&gt;
&lt;li&gt;Do users send payment screenshots?&lt;/li&gt;
&lt;li&gt;Do admins manually remove expired members?&lt;/li&gt;
&lt;li&gt;Do you sell one plan or multiple plans?&lt;/li&gt;
&lt;li&gt;Do you want crypto-only or crypto as one payment option?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then show the before/after.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment screenshot -&amp;gt; manual check -&amp;gt; admin adds user -&amp;gt; spreadsheet expiry -&amp;gt; manual reminders -&amp;gt; manual removal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan selected -&amp;gt; OxaPay invoice -&amp;gt; verified webhook -&amp;gt; invite link -&amp;gt; active subscription -&amp;gt; reminders -&amp;gt; expiry automation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is what sells.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer build checklist
&lt;/h2&gt;

&lt;p&gt;Before launching your first paid Telegram access product, prepare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one demo Telegram bot&lt;/li&gt;
&lt;li&gt;one private demo channel&lt;/li&gt;
&lt;li&gt;one test OxaPay invoice flow&lt;/li&gt;
&lt;li&gt;one webhook endpoint&lt;/li&gt;
&lt;li&gt;one admin dashboard page&lt;/li&gt;
&lt;li&gt;one subscription expiry job&lt;/li&gt;
&lt;li&gt;one invite link recovery flow&lt;/li&gt;
&lt;li&gt;one payment search tool&lt;/li&gt;
&lt;li&gt;one merchant onboarding checklist&lt;/li&gt;
&lt;li&gt;one bot permission checklist&lt;/li&gt;
&lt;li&gt;one support SOP&lt;/li&gt;
&lt;li&gt;one pricing page&lt;/li&gt;
&lt;li&gt;one short demo video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your demo should not say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is a crypto payment link in Telegram.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is a full paid access workflow: user chooses a plan, pays, receives access, gets renewal reminders, and is removed when access expires.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Example onboarding checklist for merchants
&lt;/h2&gt;

&lt;p&gt;Use a checklist like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create or provide a Telegram bot token via BotFather.&lt;/li&gt;
&lt;li&gt;Add the bot as admin to the private group or channel.&lt;/li&gt;
&lt;li&gt;Grant the bot invite permissions.&lt;/li&gt;
&lt;li&gt;Grant restriction permissions if automatic removal is required.&lt;/li&gt;
&lt;li&gt;Create OxaPay merchant API credentials.&lt;/li&gt;
&lt;li&gt;Configure webhook callback URL.&lt;/li&gt;
&lt;li&gt;Define access plans and durations.&lt;/li&gt;
&lt;li&gt;Choose grace period rules.&lt;/li&gt;
&lt;li&gt;Define renewal reminder timing.&lt;/li&gt;
&lt;li&gt;Test invoice creation in sandbox mode.&lt;/li&gt;
&lt;li&gt;Test webhook receipt and HMAC validation.&lt;/li&gt;
&lt;li&gt;Test invite delivery.&lt;/li&gt;
&lt;li&gt;Test expiry and revocation.&lt;/li&gt;
&lt;li&gt;Train admins on payment search and manual override.&lt;/li&gt;
&lt;li&gt;Go live with one plan first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This checklist gives your service structure.&lt;/p&gt;

&lt;p&gt;It also helps merchants trust you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical pricing examples
&lt;/h2&gt;

&lt;p&gt;Pricing depends on the niche, support level, merchant volume, and how much custom logic you own.&lt;/p&gt;

&lt;p&gt;Do not present the numbers below as guaranteed income. Treat them as packaging examples you can adjust after talking to real merchants.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offer&lt;/th&gt;
&lt;th&gt;Possible price shape&lt;/th&gt;
&lt;th&gt;When it makes sense&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic paid access setup&lt;/td&gt;
&lt;td&gt;One-time setup fee for one bot, one group, one plan, and invoice delivery&lt;/td&gt;
&lt;td&gt;A creator already selling access manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation setup + support&lt;/td&gt;
&lt;td&gt;Setup fee plus monthly maintenance&lt;/td&gt;
&lt;td&gt;Merchant wants you to monitor webhook issues, plan changes, and expiry problems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted dashboard&lt;/td&gt;
&lt;td&gt;Monthly SaaS subscription per community&lt;/td&gt;
&lt;td&gt;You have several similar merchants and a reusable backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-plan community billing&lt;/td&gt;
&lt;td&gt;Higher setup fee plus monthly support&lt;/td&gt;
&lt;td&gt;Merchant sells weekly, monthly, quarterly, and lifetime access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency white-label kit&lt;/td&gt;
&lt;td&gt;License fee plus per-client setup support&lt;/td&gt;
&lt;td&gt;Agencies want to resell Telegram monetization to multiple clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-value private community system&lt;/td&gt;
&lt;td&gt;Premium setup plus support SLA&lt;/td&gt;
&lt;td&gt;The community has meaningful revenue and cannot tolerate access failures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful way to think about pricing is not developer hours.&lt;/p&gt;

&lt;p&gt;Price against the operational value.&lt;/p&gt;

&lt;p&gt;If the system saves admins hours of manual checking every week, reduces delayed access, prevents unpaid users from staying inside the group, and gives the owner a cleaner revenue workflow, it is worth more than a simple bot script.&lt;/p&gt;

&lt;p&gt;For the first client, a developer might sell a smaller implementation to prove the workflow. For later clients, the same backend, onboarding checklist, and dashboard can become a repeatable productized service.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to build first
&lt;/h2&gt;

&lt;p&gt;A common failure mode is building a large SaaS before proving that one niche will pay.&lt;/p&gt;

&lt;p&gt;Avoid starting with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a public marketplace of Telegram communities&lt;/li&gt;
&lt;li&gt;a complex affiliate system&lt;/li&gt;
&lt;li&gt;a full CRM&lt;/li&gt;
&lt;li&gt;a mobile app&lt;/li&gt;
&lt;li&gt;multi-language onboarding&lt;/li&gt;
&lt;li&gt;advanced analytics&lt;/li&gt;
&lt;li&gt;custom AI support&lt;/li&gt;
&lt;li&gt;ten payment plans&lt;/li&gt;
&lt;li&gt;too many admin roles&lt;/li&gt;
&lt;li&gt;a generic tool for every type of creator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those may become useful later, but they are not the wedge.&lt;/p&gt;

&lt;p&gt;The wedge is much simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A paid Telegram community can stop checking payments manually and start granting, renewing, and revoking access automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Build that first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 14-day build plan
&lt;/h2&gt;

&lt;p&gt;A focused developer can prototype the first version quickly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Build focus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Define one niche, one merchant profile, and three access plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Create the Telegram bot and private test group/channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Build &lt;code&gt;/start&lt;/code&gt;, plan selection, and user capture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Create OxaPay invoice flow and store &lt;code&gt;order_id&lt;/code&gt; / &lt;code&gt;track_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Build webhook endpoint and HMAC validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Implement idempotent payment state updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Generate one-time invite links after payment confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Track invite delivery and join events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Build renewal reminder job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Build expiry and access revocation job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Add admin search by user, order ID, and payment ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Add audit log and manual override actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Test edge cases: duplicate webhook, expired invoice, failed Telegram send, wrong user join&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Record demo video and prepare the first merchant pitch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This plan does not produce a perfect SaaS.&lt;/p&gt;

&lt;p&gt;It produces a sellable demo.&lt;/p&gt;

&lt;p&gt;That is enough to validate the business.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first version of your landing page
&lt;/h2&gt;

&lt;p&gt;Your landing page should not be abstract.&lt;/p&gt;

&lt;p&gt;A weak headline is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Crypto payments for Telegram.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A stronger headline is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sell paid Telegram access with crypto payments, automatic invite links, renewal reminders, and expired member removal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A simple landing page should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who it is for&lt;/li&gt;
&lt;li&gt;the pain it solves&lt;/li&gt;
&lt;li&gt;a visual flow from payment to access&lt;/li&gt;
&lt;li&gt;screenshots of the bot&lt;/li&gt;
&lt;li&gt;screenshot of the admin dashboard&lt;/li&gt;
&lt;li&gt;supported plan types&lt;/li&gt;
&lt;li&gt;setup requirements&lt;/li&gt;
&lt;li&gt;security notes&lt;/li&gt;
&lt;li&gt;pricing packages&lt;/li&gt;
&lt;li&gt;a short demo video&lt;/li&gt;
&lt;li&gt;a call to book setup or request a demo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your landing page should make the merchant think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This person understands my access problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This person knows how to call a payment API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A Telegram paid access system is a strong developer business because the merchant problem is specific, painful, and operational.&lt;/p&gt;

&lt;p&gt;Creators and community operators do not only need to accept payment. They need to sell access, activate members, manage renewals, remove expired users, reduce support, and see what happened when something goes wrong.&lt;/p&gt;

&lt;p&gt;OxaPay provides useful crypto payment primitives for this workflow: invoice creation, callback URLs, webhooks, payment information, payment history, SDKs, and Telegram automation examples through n8n and Make.&lt;/p&gt;

&lt;p&gt;Telegram provides the community layer and Bot API methods for invite links, member updates, and access control when the bot has the right permissions.&lt;/p&gt;

&lt;p&gt;The developer's opportunity is to connect those pieces into a product merchants can buy.&lt;/p&gt;

&lt;p&gt;Start with one niche.&lt;/p&gt;

&lt;p&gt;Build the full access lifecycle.&lt;/p&gt;

&lt;p&gt;Make webhook processing reliable.&lt;/p&gt;

&lt;p&gt;Add expiry automation.&lt;/p&gt;

&lt;p&gt;Give admins visibility.&lt;/p&gt;

&lt;p&gt;Then productize the pattern.&lt;/p&gt;

&lt;p&gt;That is how a simple Telegram bot becomes a real paid access business.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;OxaPay Generate White Label&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/n8n-automation/telegram-bot" rel="noopener noreferrer"&gt;OxaPay n8n Telegram Bot Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation/telegram-bot" rel="noopener noreferrer"&gt;OxaPay Make Telegram Bot Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation" rel="noopener noreferrer"&gt;OxaPay Make Automation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://core.telegram.org/bots/api" rel="noopener noreferrer"&gt;Telegram Bot API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://core.telegram.org/bots/api#createchatinvitelink" rel="noopener noreferrer"&gt;Telegram Bot API: createChatInviteLink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://core.telegram.org/bots/api#approvechatjoinrequest" rel="noopener noreferrer"&gt;Telegram Bot API: approveChatJoinRequest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://core.telegram.org/bots/api#revokechatinvitelink" rel="noopener noreferrer"&gt;Telegram Bot API: revokeChatInviteLink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>crypto</category>
      <category>backend</category>
    </item>
    <item>
      <title>Build a Vertical Crypto Checkout for a Specific Niche</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Tue, 14 Jul 2026 05:24:12 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-vertical-crypto-checkout-for-a-specific-niche-i3i</link>
      <guid>https://dev.to/kevins1988/build-a-vertical-crypto-checkout-for-a-specific-niche-i3i</guid>
      <description>&lt;h1&gt;
  
  
  Build a Vertical Crypto Checkout for a Specific Niche
&lt;/h1&gt;

&lt;p&gt;Most developers treat checkout as a generic feature.&lt;/p&gt;

&lt;p&gt;A customer selects a product. The backend creates a payment request. The user pays. A webhook arrives. The order becomes paid.&lt;/p&gt;

&lt;p&gt;That works as a technical integration, but it is not always a strong business.&lt;/p&gt;

&lt;p&gt;A generic checkout has too many competitors and too little differentiation. The merchant usually compares it against existing plugins, hosted payment pages, or one-time freelance integration work. The developer becomes a payment installer.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;vertical checkout&lt;/strong&gt; is different.&lt;/p&gt;

&lt;p&gt;It is not simply a crypto payment button. It is a checkout system designed around the operational workflow of one specific niche.&lt;/p&gt;

&lt;p&gt;For example, a hosting company does not only need to collect payment. It needs to provision a server, extend a renewal date, handle invoice expiry, update a billing panel, notify support, and keep a payment trail. A course seller does not only need payment. It needs to unlock lessons, send onboarding emails, prevent access after expiry, and support students who paid from the wrong network. A gaming community does not only need a transaction. It needs to assign roles, deliver credits, apply package rules, and prevent duplicate fulfillment.&lt;/p&gt;

&lt;p&gt;That is the business opportunity.&lt;/p&gt;

&lt;p&gt;In this article, we will use OxaPay as the example crypto payment infrastructure because its documentation exposes useful payment primitives for this kind of product: hosted invoices, white-label payment requests, static addresses, payment information, payment history, payment statuses, webhooks, SDKs, and plugins.&lt;/p&gt;

&lt;p&gt;This is not an article about launching a generic payment gateway. It is a blueprint for developers who want to build a niche checkout product that merchants can actually understand, buy, and use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;vertical crypto checkout&lt;/strong&gt; is a payment workflow built for a specific merchant category.&lt;/p&gt;

&lt;p&gt;The developer does not sell:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can add crypto payments to your site.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The developer sells:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can build a checkout flow that matches how your business sells, fulfills, renews, supports, and reports orders.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference changes the value of the product.&lt;/p&gt;

&lt;p&gt;A general payment integration is a technical task. A vertical checkout is a merchant-facing product.&lt;/p&gt;

&lt;p&gt;It can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a niche-specific checkout page&lt;/li&gt;
&lt;li&gt;a payment method selector&lt;/li&gt;
&lt;li&gt;a branded crypto payment screen&lt;/li&gt;
&lt;li&gt;OxaPay invoice or white-label payment creation&lt;/li&gt;
&lt;li&gt;webhook validation&lt;/li&gt;
&lt;li&gt;order state management&lt;/li&gt;
&lt;li&gt;automatic fulfillment&lt;/li&gt;
&lt;li&gt;customer instructions&lt;/li&gt;
&lt;li&gt;admin status visibility&lt;/li&gt;
&lt;li&gt;support tooling&lt;/li&gt;
&lt;li&gt;renewal or expiry logic&lt;/li&gt;
&lt;li&gt;reporting and reconciliation basics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is that the checkout understands the merchant's business process, not just the payment API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why vertical beats generic
&lt;/h2&gt;

&lt;p&gt;Generic checkout software tries to serve everyone.&lt;/p&gt;

&lt;p&gt;That sounds scalable, but it often makes the product harder to sell at the beginning. A generic checkout must compete on features, price, brand trust, and integrations. A niche checkout can compete on relevance.&lt;/p&gt;

&lt;p&gt;A hosting provider does not want to read a long list of payment features. They want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can this mark an invoice as paid in my billing flow?&lt;/li&gt;
&lt;li&gt;Can this extend the service automatically?&lt;/li&gt;
&lt;li&gt;Can this handle renewals?&lt;/li&gt;
&lt;li&gt;Can support see the payment status?&lt;/li&gt;
&lt;li&gt;Can the customer pay with the right network and coin?&lt;/li&gt;
&lt;li&gt;Can the system avoid provisioning before the payment is confirmed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A course seller wants different answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can this unlock the course after payment?&lt;/li&gt;
&lt;li&gt;Can this send the student a receipt or onboarding email?&lt;/li&gt;
&lt;li&gt;Can this prevent manual checking?&lt;/li&gt;
&lt;li&gt;Can this recover expired checkouts?&lt;/li&gt;
&lt;li&gt;Can this show clear payment instructions to non-technical buyers?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A gaming merchant wants another set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can this deliver credits after payment?&lt;/li&gt;
&lt;li&gt;Can this prevent duplicate crediting?&lt;/li&gt;
&lt;li&gt;Can this map each payment to a user ID?&lt;/li&gt;
&lt;li&gt;Can this handle small-value payments safely?&lt;/li&gt;
&lt;li&gt;Can support search by username, order ID, or transaction hash?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payment primitive may be similar across all three. The business workflow is not.&lt;/p&gt;

&lt;p&gt;That is why a vertical checkout can be a stronger developer business than a generic integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who would pay for this?
&lt;/h2&gt;

&lt;p&gt;The best buyers are not random merchants who are merely curious about crypto.&lt;/p&gt;

&lt;p&gt;The best buyers already have a business process where payment status controls revenue, fulfillment, access, renewal, or support.&lt;/p&gt;

&lt;p&gt;Good buyers include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hosting companies that need invoice payment to extend services&lt;/li&gt;
&lt;li&gt;SaaS founders that need plan activation after payment&lt;/li&gt;
&lt;li&gt;software sellers that need license delivery after confirmation&lt;/li&gt;
&lt;li&gt;course sellers that need paid access unlock&lt;/li&gt;
&lt;li&gt;gaming communities that need balance top-ups or role delivery&lt;/li&gt;
&lt;li&gt;agencies that want branded crypto invoice portals for international clients&lt;/li&gt;
&lt;li&gt;digital product stores that want automated delivery and fewer manual checks&lt;/li&gt;
&lt;li&gt;merchants already accepting crypto manually and struggling with support or reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They pay because the checkout removes a business bottleneck.&lt;/p&gt;

&lt;p&gt;They are not paying for an API call.&lt;/p&gt;

&lt;p&gt;They are paying for a system that answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what did the customer buy?&lt;/li&gt;
&lt;li&gt;did the payment arrive?&lt;/li&gt;
&lt;li&gt;is the payment confirmed?&lt;/li&gt;
&lt;li&gt;what should happen now?&lt;/li&gt;
&lt;li&gt;did fulfillment succeed?&lt;/li&gt;
&lt;li&gt;can support explain the payment state?&lt;/li&gt;
&lt;li&gt;can the business export records later?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why vertical checkout can become a productized service, not just a one-time integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can sell
&lt;/h2&gt;

&lt;p&gt;A developer can package this in several ways.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offer&lt;/th&gt;
&lt;th&gt;What the merchant gets&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vertical checkout setup&lt;/td&gt;
&lt;td&gt;OxaPay integration, checkout flow, webhook handling, and one fulfillment action&lt;/td&gt;
&lt;td&gt;First client, custom implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted checkout portal&lt;/td&gt;
&lt;td&gt;A reusable checkout layer hosted by the developer&lt;/td&gt;
&lt;td&gt;Multiple merchants in the same niche&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White-label checkout package&lt;/td&gt;
&lt;td&gt;Branded checkout UI using OxaPay white-label payment details&lt;/td&gt;
&lt;td&gt;Agencies, SaaS tools, niche platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment + fulfillment automation&lt;/td&gt;
&lt;td&gt;Payment event connected to access, delivery, provisioning, or renewal&lt;/td&gt;
&lt;td&gt;Digital products, courses, hosting, gaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support and recovery dashboard&lt;/td&gt;
&lt;td&gt;Payment timeline, status search, expired checkout recovery, and admin actions&lt;/td&gt;
&lt;td&gt;Merchants with support volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance retainer&lt;/td&gt;
&lt;td&gt;Monitoring, updates, webhook troubleshooting, small improvements&lt;/td&gt;
&lt;td&gt;Any merchant that depends on the flow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the practical path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;sell the first version as a service,&lt;/li&gt;
&lt;li&gt;reuse the architecture for similar merchants,&lt;/li&gt;
&lt;li&gt;turn repeated requirements into software,&lt;/li&gt;
&lt;li&gt;productize only after the niche proves demand.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The OxaPay primitives you can build on
&lt;/h2&gt;

&lt;p&gt;OxaPay exposes several primitives that can support a vertical checkout product.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OxaPay primitive&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it matters for vertical checkout&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;Generate Invoice&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creates a hosted invoice and returns a payment URL&lt;/td&gt;
&lt;td&gt;Good for fast MVPs, merchant dashboards, and simple hosted checkout flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;Generate White Label&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Returns payment details such as address, amount, currency, network, and expiry instead of only sending users to a hosted invoice URL&lt;/td&gt;
&lt;td&gt;Useful when the developer wants to own the checkout UI inside a niche product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;Generate Static Address&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creates a reusable address linked to a &lt;code&gt;track_id&lt;/code&gt; and can send callbacks for payments made to that address&lt;/td&gt;
&lt;td&gt;Useful for deposits, wallet top-ups, account balances, and recurring customer deposit flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;Payment Information&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Retrieves a specific payment by &lt;code&gt;track_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Useful for support tools, manual checks, retries, and payment recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;Payment History&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Lists payments with filters such as type, status, currency, network, date, amount, and pagination&lt;/td&gt;
&lt;td&gt;Useful for dashboards, admin views, reconciliation, and reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;Webhook&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sends payment status updates to a merchant callback URL&lt;/td&gt;
&lt;td&gt;The event layer that lets the checkout trigger fulfillment, access, alerts, and status changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/plugins" rel="noopener noreferrer"&gt;Plugins&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Existing integrations for platforms such as WooCommerce, WHMCS, WISECP, Blesta, PrestaShop, EDD, Paid Memberships Pro, Gravity Forms, OpenCart, Magento 2, and others&lt;/td&gt;
&lt;td&gt;Useful market signal: many merchant workflows already live inside niche platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;SDKs&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;SDK methods for invoice, white-label, static address, payment information, history, payouts, and account operations&lt;/td&gt;
&lt;td&gt;Useful for building faster in PHP, Laravel, Python, or other stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a vertical checkout, the most important distinction is between &lt;strong&gt;invoice&lt;/strong&gt; and &lt;strong&gt;white-label&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A hosted invoice is usually better for speed. It lets you create a payment URL and redirect the payer.&lt;/p&gt;

&lt;p&gt;A white-label payment is better when the checkout experience itself is part of your product. OxaPay's white-label endpoint is designed to return payment details instead of only an invoice URL, which allows you to manage the payment process in your own interface. It includes fields such as payment currency, amount, network, lifetime, fee payer behavior, underpayment coverage, callback URL, order ID, email, and description.&lt;/p&gt;

&lt;p&gt;That is important for vertical products because the UI can be tailored to the niche.&lt;/p&gt;

&lt;p&gt;For a hosting customer, the checkout can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your VPS will be extended after the payment is confirmed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a course buyer, it can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your course access will be unlocked automatically after confirmation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a gaming customer, it can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Credits are delivered to your account only after the transaction is confirmed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The payment details may come from the same infrastructure. The business promise is different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually building
&lt;/h2&gt;

&lt;p&gt;You are building a reusable checkout layer for one merchant category.&lt;/p&gt;

&lt;p&gt;A simple architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merchant product / store / billing panel
        |
        | creates checkout session
        v
Vertical checkout backend
        |
        | creates invoice or white-label payment
        v
OxaPay payment infrastructure
        |
        | payer sends payment
        v
OxaPay webhook
        |
        | validate HMAC + update state
        v
Vertical business action
- provision service
- unlock access
- deliver credits
- extend subscription
- notify support
- update CRM
- write reconciliation record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The product is not just the request to OxaPay.&lt;/p&gt;

&lt;p&gt;The product is the layer around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;niche checkout UI&lt;/li&gt;
&lt;li&gt;payment session database&lt;/li&gt;
&lt;li&gt;merchant configuration&lt;/li&gt;
&lt;li&gt;webhook processor&lt;/li&gt;
&lt;li&gt;fulfillment adapter&lt;/li&gt;
&lt;li&gt;customer-facing status page&lt;/li&gt;
&lt;li&gt;admin dashboard&lt;/li&gt;
&lt;li&gt;support search&lt;/li&gt;
&lt;li&gt;retry and recovery logic&lt;/li&gt;
&lt;li&gt;reporting export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where a developer can create value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the niche
&lt;/h2&gt;

&lt;p&gt;The niche matters more than the code.&lt;/p&gt;

&lt;p&gt;A weak niche makes the product hard to sell even if the implementation is good. A strong niche gives the checkout an obvious business case.&lt;/p&gt;

&lt;p&gt;A good niche has five traits.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The merchant already sells online
&lt;/h3&gt;

&lt;p&gt;Do not start with merchants who first need to be convinced to sell online. Start with businesses that already have a checkout, order system, billing system, bot, or membership flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Payment must trigger a digital action
&lt;/h3&gt;

&lt;p&gt;The strongest vertical checkout products connect payment to an automatic business action.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create hosting account&lt;/li&gt;
&lt;li&gt;renew subscription&lt;/li&gt;
&lt;li&gt;unlock course&lt;/li&gt;
&lt;li&gt;add user to a private community&lt;/li&gt;
&lt;li&gt;deliver license key&lt;/li&gt;
&lt;li&gt;credit game balance&lt;/li&gt;
&lt;li&gt;activate software plan&lt;/li&gt;
&lt;li&gt;generate booking confirmation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If payment does not trigger anything, the checkout is less valuable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Manual payment checking is painful
&lt;/h3&gt;

&lt;p&gt;The best merchants are already wasting time on screenshots, wallet checks, manual confirmations, support tickets, or spreadsheets.&lt;/p&gt;

&lt;p&gt;That pain creates willingness to pay.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. There is repeat usage
&lt;/h3&gt;

&lt;p&gt;A one-time sale can still work, but repeat usage is better.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;renewals&lt;/li&gt;
&lt;li&gt;deposits&lt;/li&gt;
&lt;li&gt;subscriptions&lt;/li&gt;
&lt;li&gt;top-ups&lt;/li&gt;
&lt;li&gt;recurring purchases&lt;/li&gt;
&lt;li&gt;ongoing digital services&lt;/li&gt;
&lt;li&gt;community access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeat usage creates more need for automation, reporting, and support tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The merchant understands the customer segment
&lt;/h3&gt;

&lt;p&gt;A vertical checkout should match how the merchant's customers behave.&lt;/p&gt;

&lt;p&gt;Crypto-native gaming users need different instructions from non-technical course buyers. Hosting customers may care about network fees, renewal timing, and provisioning. A creator community may care more about mobile-first payment and access control.&lt;/p&gt;

&lt;p&gt;The checkout should reflect those differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strong niches to consider
&lt;/h2&gt;

&lt;p&gt;You do not need to build for every industry.&lt;/p&gt;

&lt;p&gt;Pick one vertical and go deep.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosting and infrastructure providers
&lt;/h3&gt;

&lt;p&gt;This is one of the strongest niches.&lt;/p&gt;

&lt;p&gt;Why it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hosting merchants already use billing systems&lt;/li&gt;
&lt;li&gt;service activation is easy to define&lt;/li&gt;
&lt;li&gt;renewals are common&lt;/li&gt;
&lt;li&gt;support teams need payment visibility&lt;/li&gt;
&lt;li&gt;crypto payments are familiar in some hosting/VPN/infrastructure markets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;crypto checkout for hosting invoices&lt;/li&gt;
&lt;li&gt;service activation after &lt;code&gt;Paid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;renewal extension logic&lt;/li&gt;
&lt;li&gt;support timeline&lt;/li&gt;
&lt;li&gt;customer payment status page&lt;/li&gt;
&lt;li&gt;invoice recovery flow&lt;/li&gt;
&lt;li&gt;admin reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OxaPay has plugin coverage around hosting-related platforms such as WHMCS, WISECP, Clientexec, and Blesta. That does not mean every hosting business needs a custom product, but it shows that hosting and service-provider billing is a natural environment for payment workflows.&lt;/p&gt;

&lt;p&gt;Potential revenue model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup fee for custom billing integration&lt;/li&gt;
&lt;li&gt;monthly maintenance&lt;/li&gt;
&lt;li&gt;premium support for webhook monitoring&lt;/li&gt;
&lt;li&gt;white-label package for hosting agencies&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Digital products and software licenses
&lt;/h3&gt;

&lt;p&gt;This is another strong category because fulfillment can be automated.&lt;/p&gt;

&lt;p&gt;What to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment page for license purchases&lt;/li&gt;
&lt;li&gt;webhook-based license delivery&lt;/li&gt;
&lt;li&gt;fraud-resistant duplicate prevention&lt;/li&gt;
&lt;li&gt;failed payment recovery&lt;/li&gt;
&lt;li&gt;support search by email/order/track ID&lt;/li&gt;
&lt;li&gt;CSV export for accounting&lt;/li&gt;
&lt;li&gt;admin dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Merchants that sell files, templates, plugins, scripts, licenses, or paid resources often want simple fulfillment. They may not need a full payment operations platform, but they will pay for a reliable checkout that removes manual delivery.&lt;/p&gt;

&lt;p&gt;Potential revenue model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixed setup package&lt;/li&gt;
&lt;li&gt;monthly hosted checkout&lt;/li&gt;
&lt;li&gt;per-product storefront template&lt;/li&gt;
&lt;li&gt;paid support for plugin/custom store integration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Online courses and membership content
&lt;/h3&gt;

&lt;p&gt;This niche is valuable because the payment is connected to access control.&lt;/p&gt;

&lt;p&gt;What to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;course checkout&lt;/li&gt;
&lt;li&gt;paid access unlock&lt;/li&gt;
&lt;li&gt;renewal or expiry logic&lt;/li&gt;
&lt;li&gt;student onboarding email&lt;/li&gt;
&lt;li&gt;payment status page&lt;/li&gt;
&lt;li&gt;admin view for student payments&lt;/li&gt;
&lt;li&gt;failed/expired invoice recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OxaPay's plugin list includes membership and WordPress-oriented tools such as Paid Memberships Pro, Restrict Content Pro, Easy Digital Downloads, and Gravity Forms. A developer can use that as a signal that content, membership, and form-based payment flows are relevant merchant categories.&lt;/p&gt;

&lt;p&gt;Potential revenue model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup package for course sellers&lt;/li&gt;
&lt;li&gt;monthly access automation support&lt;/li&gt;
&lt;li&gt;paid templates for membership platforms&lt;/li&gt;
&lt;li&gt;managed payment support add-on&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gaming communities and digital credits
&lt;/h3&gt;

&lt;p&gt;This niche can be strong, but it requires careful state handling.&lt;/p&gt;

&lt;p&gt;What to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user balance top-up&lt;/li&gt;
&lt;li&gt;item purchase checkout&lt;/li&gt;
&lt;li&gt;Discord/Telegram role delivery&lt;/li&gt;
&lt;li&gt;idempotent crediting&lt;/li&gt;
&lt;li&gt;admin reversal workflow&lt;/li&gt;
&lt;li&gt;suspicious payment review queue&lt;/li&gt;
&lt;li&gt;player-facing payment status page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For top-ups, static addresses may be useful in some designs because the same user can deposit to a reusable address. However, you must design carefully around address ownership, account mapping, callback handling, revocation rules, and reconciliation.&lt;/p&gt;

&lt;p&gt;Potential revenue model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;monthly SaaS for small communities&lt;/li&gt;
&lt;li&gt;custom integration for game servers&lt;/li&gt;
&lt;li&gt;revenue share for community operators&lt;/li&gt;
&lt;li&gt;paid moderation/support tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  VPN, proxy, and digital access sellers
&lt;/h3&gt;

&lt;p&gt;This niche has clear payment-to-access logic.&lt;/p&gt;

&lt;p&gt;What to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plan checkout&lt;/li&gt;
&lt;li&gt;account activation&lt;/li&gt;
&lt;li&gt;renewal extension&lt;/li&gt;
&lt;li&gt;access expiry&lt;/li&gt;
&lt;li&gt;support timeline&lt;/li&gt;
&lt;li&gt;recurring reminder flow&lt;/li&gt;
&lt;li&gt;failed payment handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can be a strong vertical if you handle support and compliance boundaries carefully. The developer should sell operational automation, not make unrealistic claims about bypassing financial systems.&lt;/p&gt;

&lt;p&gt;Potential revenue model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup fee&lt;/li&gt;
&lt;li&gt;monthly support&lt;/li&gt;
&lt;li&gt;plan-based SaaS&lt;/li&gt;
&lt;li&gt;reseller panel integration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Agencies and remote service providers
&lt;/h3&gt;

&lt;p&gt;This is less about instant fulfillment and more about clean payment tracking.&lt;/p&gt;

&lt;p&gt;What to build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;branded invoice portal&lt;/li&gt;
&lt;li&gt;project payment checkout&lt;/li&gt;
&lt;li&gt;payment status tracking&lt;/li&gt;
&lt;li&gt;client email notifications&lt;/li&gt;
&lt;li&gt;support/admin timeline&lt;/li&gt;
&lt;li&gt;reporting export&lt;/li&gt;
&lt;li&gt;paid/unpaid project dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Potential revenue model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one-time setup&lt;/li&gt;
&lt;li&gt;hosted client payment portal&lt;/li&gt;
&lt;li&gt;monthly reporting and maintenance package&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hosted invoice or white-label checkout?
&lt;/h2&gt;

&lt;p&gt;The first important product decision is whether you should use a hosted invoice flow or build your own branded checkout UI with white-label payment details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosted invoice flow
&lt;/h3&gt;

&lt;p&gt;Use hosted invoices when you want speed and lower complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer clicks Pay
        ↓
Your backend creates OxaPay invoice
        ↓
Customer is redirected to payment_url
        ↓
OxaPay handles payment page
        ↓
Webhook updates your order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a strong MVP choice.&lt;/p&gt;

&lt;p&gt;It is useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the merchant does not need full checkout control&lt;/li&gt;
&lt;li&gt;speed matters more than UX customization&lt;/li&gt;
&lt;li&gt;the developer wants to validate demand quickly&lt;/li&gt;
&lt;li&gt;the niche workflow happens mostly after payment&lt;/li&gt;
&lt;li&gt;the merchant is comfortable redirecting users&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  White-label checkout flow
&lt;/h3&gt;

&lt;p&gt;Use white-label when the checkout experience itself is part of the product.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer opens your checkout page
        ↓
Your backend requests white-label payment details
        ↓
Your UI displays coin, network, amount, address, QR, timer, and instructions
        ↓
Customer pays from their wallet
        ↓
Webhook updates the checkout state
        ↓
Your UI and merchant system trigger fulfillment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more complex, but it gives you more product control.&lt;/p&gt;

&lt;p&gt;It is useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the merchant wants branded payment UX&lt;/li&gt;
&lt;li&gt;the niche requires special instructions&lt;/li&gt;
&lt;li&gt;you want to reduce support questions&lt;/li&gt;
&lt;li&gt;you need to show a custom payment status page&lt;/li&gt;
&lt;li&gt;the product is more than a simple redirect&lt;/li&gt;
&lt;li&gt;you want to package the checkout as software&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The OxaPay white-label endpoint is especially relevant here because it returns payment details that can be rendered inside your own interface rather than only returning a hosted payment URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example vertical: hosting checkout
&lt;/h2&gt;

&lt;p&gt;Let us make the idea concrete.&lt;/p&gt;

&lt;p&gt;Imagine you build a vertical crypto checkout for small hosting providers.&lt;/p&gt;

&lt;p&gt;The merchant problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customers want to pay for hosting with crypto&lt;/li&gt;
&lt;li&gt;the merchant uses a billing panel or custom admin system&lt;/li&gt;
&lt;li&gt;support manually checks payment status&lt;/li&gt;
&lt;li&gt;renewals and service extensions create confusion&lt;/li&gt;
&lt;li&gt;expired payments generate tickets&lt;/li&gt;
&lt;li&gt;the team wants a clean audit trail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a hosted checkout integration for hosting invoices&lt;/li&gt;
&lt;li&gt;optional white-label payment UI&lt;/li&gt;
&lt;li&gt;webhook-based confirmation&lt;/li&gt;
&lt;li&gt;service provisioning hook&lt;/li&gt;
&lt;li&gt;renewal extension logic&lt;/li&gt;
&lt;li&gt;support dashboard&lt;/li&gt;
&lt;li&gt;customer payment status page&lt;/li&gt;
&lt;li&gt;daily payment report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The merchant does not buy “crypto API integration.”&lt;/p&gt;

&lt;p&gt;They buy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Crypto checkout for hosting invoices with automatic renewal, service activation, payment status visibility, and support recovery.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is much easier to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical architecture for a vertical checkout
&lt;/h2&gt;

&lt;p&gt;A production-ready vertical checkout needs a few core components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend
- Checkout page
- Payment instructions
- Timer and status display
- Customer success/failure states

Backend
- Checkout session API
- OxaPay client
- Webhook receiver
- State machine
- Fulfillment adapter
- Admin API

Database
- merchants
- customers
- checkout_sessions
- oxapay_payments
- webhook_events
- fulfillment_jobs
- support_notes

Integrations
- Merchant store or billing panel
- Email/Telegram/Slack/Discord notifications
- CRM or support desk
- Accounting export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not start with all of this. But design the MVP so it can grow into this architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data model
&lt;/h2&gt;

&lt;p&gt;A simple database model might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;niche&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_merchant_key_encrypted&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;webhook_secret_reference&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;checkout_sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;customer_email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;external_order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;niche_object_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- hosting_invoice, course_access, game_credit, license&lt;/span&gt;
  &lt;span class="n"&gt;niche_object_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_method_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- invoice, white_label, static_address&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_payment_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hmac_valid&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fulfillment_jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;checkout_session_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;checkout_sessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;action_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- provision_service, unlock_course, deliver_license, credit_balance&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;completed_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important field is &lt;code&gt;niche_object_type&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is where the vertical logic starts.&lt;/p&gt;

&lt;p&gt;A generic checkout only knows “order.”&lt;/p&gt;

&lt;p&gt;A vertical checkout knows whether the payment is for a hosting renewal, course access, software license, community role, game credit, or deposit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payment state machine
&lt;/h2&gt;

&lt;p&gt;Do not let raw gateway statuses leak directly into merchant business logic.&lt;/p&gt;

&lt;p&gt;Create your own internal state machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created
  ↓
payment_created
  ↓
paying
  ↓
paid
  ↓
fulfillment_pending
  ↓
fulfilled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also handle failure paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created
  ↓
payment_created
  ↓
expired
  ↓
recovery_required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And manual paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;paid
  ↓
fulfillment_failed
  ↓
manual_review
  ↓
fulfilled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OxaPay webhook examples distinguish &lt;code&gt;Paying&lt;/code&gt; from &lt;code&gt;Paid&lt;/code&gt;. The documentation explains that the initial callback may indicate paying, and that the merchant should wait for a second callback where the status is paid before treating the payment as confirmed for fulfillment.&lt;/p&gt;

&lt;p&gt;That distinction matters for a vertical checkout.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Paying&lt;/code&gt; can update the UI to “payment detected”&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Paid&lt;/code&gt; can trigger fulfillment&lt;/li&gt;
&lt;li&gt;failed fulfillment should not reverse the payment state&lt;/li&gt;
&lt;li&gt;webhook retries should not duplicate delivery&lt;/li&gt;
&lt;li&gt;support should see both payment and fulfillment status&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creating a hosted invoice checkout
&lt;/h2&gt;

&lt;p&gt;Here is a minimal Node.js example for a hosted invoice flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createHostedCheckoutSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment/invoice`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oxapayMerchantKey&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Hosting invoice &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/checkout/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/return`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OxaPay invoice request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Always verify the exact response shape against the current API docs.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;external_order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;niche_object_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hosting_invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;niche_object_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payment_method_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;oxapay_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;oxapay_payment_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paymentUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is enough for an MVP if most of the value comes after payment.&lt;/p&gt;

&lt;p&gt;The merchant creates an order. Your service creates the invoice. The customer pays through the hosted payment page. Your webhook updates the internal order and triggers fulfillment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a white-label checkout
&lt;/h2&gt;

&lt;p&gt;A white-label checkout gives you more control over the payment experience.&lt;/p&gt;

&lt;p&gt;Instead of redirecting users to a payment URL, your product renders the payment screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createWhiteLabelCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedCoin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedNetwork&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment/white-label`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oxapayMerchantKey&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pay_currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedCoin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// example: USDT, BTC, LTC&lt;/span&gt;
      &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedNetwork&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// example: TRC20, Ethereum, Polygon, etc.&lt;/span&gt;
      &lt;span class="na"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Course access &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;courseId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fee_paid_by_payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;under_paid_coverage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OxaPay white-label request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;external_order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;niche_object_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;course_access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;niche_object_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;courseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payment_method_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;white_label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;oxapay_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your frontend can then display:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selected coin&lt;/li&gt;
&lt;li&gt;network&lt;/li&gt;
&lt;li&gt;exact amount&lt;/li&gt;
&lt;li&gt;payment address&lt;/li&gt;
&lt;li&gt;QR code if returned&lt;/li&gt;
&lt;li&gt;countdown timer&lt;/li&gt;
&lt;li&gt;warning about using the correct network&lt;/li&gt;
&lt;li&gt;status polling or WebSocket updates&lt;/li&gt;
&lt;li&gt;niche-specific fulfillment message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Send exactly the displayed amount on the selected network. Your course access will unlock after payment confirmation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is part of the product.&lt;/p&gt;

&lt;p&gt;Good vertical checkout products reduce support tickets before they happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhook handling
&lt;/h2&gt;

&lt;p&gt;Webhook handling is where the checkout becomes operational.&lt;/p&gt;

&lt;p&gt;The OxaPay webhook documentation says merchant callback URLs receive JSON payment updates and should return HTTP 200 with content such as &lt;code&gt;ok&lt;/code&gt; for successful delivery. It also describes retry behavior and HMAC validation using the raw request body and the HMAC header.&lt;/p&gt;

&lt;p&gt;A simplified Express-style webhook receiver might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmacHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hmac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid JSON&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;calculated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;calculated&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hmacHeader&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhook_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;oxapay_track_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;event_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hmac_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processPaymentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your processor should be idempotent.&lt;/p&gt;

&lt;p&gt;That means receiving the same webhook twice should not deliver the product twice, credit the account twice, or extend the service twice.&lt;/p&gt;

&lt;p&gt;A safe processor might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processPaymentEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByTrackId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unresolved_events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;raw_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Paying&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paying&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alreadyPaid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;alreadyPaid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enqueueFulfillmentJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a real system, you should also handle expired sessions, rejected events, manual review states, and merchant-specific rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fulfillment adapters
&lt;/h2&gt;

&lt;p&gt;A vertical checkout becomes valuable when it triggers the correct niche action.&lt;/p&gt;

&lt;p&gt;Do not hard-code every merchant process into one function.&lt;/p&gt;

&lt;p&gt;Use adapters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fulfillmentAdapters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;hosting_invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fulfillHostingInvoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;course_access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unlockCourseAccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;game_credit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;creditGameBalance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;software_license&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;deliverLicenseKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;community_role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;grantCommunityRole&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runFulfillmentJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout_session_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fulfillmentAdapters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;niche_object_type&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`No fulfillment adapter for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;niche_object_type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the product extensible.&lt;/p&gt;

&lt;p&gt;The same checkout core can support different verticals, but only one vertical should be your public positioning at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customer-facing checkout UX
&lt;/h2&gt;

&lt;p&gt;Crypto checkout UX is not just design. It is support prevention.&lt;/p&gt;

&lt;p&gt;A strong vertical checkout page should show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what the customer is buying&lt;/li&gt;
&lt;li&gt;order ID&lt;/li&gt;
&lt;li&gt;merchant name&lt;/li&gt;
&lt;li&gt;amount&lt;/li&gt;
&lt;li&gt;selected coin&lt;/li&gt;
&lt;li&gt;selected network&lt;/li&gt;
&lt;li&gt;exact payment address&lt;/li&gt;
&lt;li&gt;QR code&lt;/li&gt;
&lt;li&gt;payment expiry timer&lt;/li&gt;
&lt;li&gt;warning about wrong networks&lt;/li&gt;
&lt;li&gt;status: waiting, paying, paid, expired&lt;/li&gt;
&lt;li&gt;what happens after confirmation&lt;/li&gt;
&lt;li&gt;support link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a hosting checkout:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After the payment is confirmed, your hosting service will be extended automatically. Do not close this page until the payment is detected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a course checkout:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your course access will unlock after the payment is confirmed. If your payment is detected but still confirming, you do not need to pay again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a gaming checkout:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Credits are added only once per confirmed payment. Do not send another transaction unless this checkout expires.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These small messages are part of the business value.&lt;/p&gt;

&lt;p&gt;They reduce confusion.&lt;/p&gt;

&lt;p&gt;They reduce tickets.&lt;/p&gt;

&lt;p&gt;They make the checkout feel built for the niche.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merchant admin dashboard
&lt;/h2&gt;

&lt;p&gt;The merchant dashboard should not look like a blockchain explorer.&lt;/p&gt;

&lt;p&gt;It should answer business questions.&lt;/p&gt;

&lt;p&gt;For each checkout session, show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merchant order ID&lt;/li&gt;
&lt;li&gt;customer email or user ID&lt;/li&gt;
&lt;li&gt;amount&lt;/li&gt;
&lt;li&gt;coin and network&lt;/li&gt;
&lt;li&gt;OxaPay &lt;code&gt;track_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;payment status&lt;/li&gt;
&lt;li&gt;fulfillment status&lt;/li&gt;
&lt;li&gt;webhook event timeline&lt;/li&gt;
&lt;li&gt;transaction hash if available&lt;/li&gt;
&lt;li&gt;support notes&lt;/li&gt;
&lt;li&gt;created date&lt;/li&gt;
&lt;li&gt;paid date&lt;/li&gt;
&lt;li&gt;action buttons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;resend payment instructions&lt;/li&gt;
&lt;li&gt;copy payment status link&lt;/li&gt;
&lt;li&gt;retry fulfillment&lt;/li&gt;
&lt;li&gt;mark for manual review&lt;/li&gt;
&lt;li&gt;export record&lt;/li&gt;
&lt;li&gt;open merchant order&lt;/li&gt;
&lt;li&gt;fetch latest payment information by &lt;code&gt;track_id&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The OxaPay Payment Information endpoint is useful here because support may need to retrieve a specific payment using the &lt;code&gt;track_id&lt;/code&gt;. Payment History is useful for merchant reports and dashboard lists because it supports filtering and pagination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static addresses: when they make sense
&lt;/h2&gt;

&lt;p&gt;Most vertical checkouts should start with invoice or white-label payments.&lt;/p&gt;

&lt;p&gt;Static addresses are useful for a different pattern: ongoing deposits or account top-ups.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;game balance deposits&lt;/li&gt;
&lt;li&gt;marketplace wallet deposits&lt;/li&gt;
&lt;li&gt;user account balance top-ups&lt;/li&gt;
&lt;li&gt;investment-style account funding&lt;/li&gt;
&lt;li&gt;repeat deposits from the same customer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OxaPay's static address endpoint creates a static address for a specific currency and network linked to a unique &lt;code&gt;track_id&lt;/code&gt;, and callback URLs can be used to receive notifications about payments made to that address. The docs also note that static addresses with no transactions for six months may be revoked.&lt;/p&gt;

&lt;p&gt;That revocation note matters for product design.&lt;/p&gt;

&lt;p&gt;If you build a deposit product, your system should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store address ownership&lt;/li&gt;
&lt;li&gt;show address status&lt;/li&gt;
&lt;li&gt;refresh or recreate addresses when needed&lt;/li&gt;
&lt;li&gt;monitor incoming callbacks&lt;/li&gt;
&lt;li&gt;reconcile deposits by user&lt;/li&gt;
&lt;li&gt;avoid treating static addresses like permanent infrastructure without lifecycle management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Static address products can be valuable, but they are not the right starting point for every niche.&lt;/p&gt;

&lt;h2&gt;
  
  
  MVP scope
&lt;/h2&gt;

&lt;p&gt;A good MVP does not need to support every coin, every platform, every niche, and every fulfillment path.&lt;/p&gt;

&lt;p&gt;A strong MVP can be much narrower.&lt;/p&gt;

&lt;p&gt;Example MVP for hosting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one merchant&lt;/li&gt;
&lt;li&gt;hosted invoice flow&lt;/li&gt;
&lt;li&gt;one billing panel adapter&lt;/li&gt;
&lt;li&gt;webhook validation&lt;/li&gt;
&lt;li&gt;order status update&lt;/li&gt;
&lt;li&gt;service extension action&lt;/li&gt;
&lt;li&gt;customer payment status page&lt;/li&gt;
&lt;li&gt;admin dashboard with payment timeline&lt;/li&gt;
&lt;li&gt;daily CSV export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example MVP for courses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one course platform&lt;/li&gt;
&lt;li&gt;white-label checkout&lt;/li&gt;
&lt;li&gt;webhook validation&lt;/li&gt;
&lt;li&gt;access unlock&lt;/li&gt;
&lt;li&gt;student onboarding email&lt;/li&gt;
&lt;li&gt;failed/expired payment recovery&lt;/li&gt;
&lt;li&gt;admin payment search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example MVP for gaming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one game/community platform&lt;/li&gt;
&lt;li&gt;hosted invoice or white-label checkout&lt;/li&gt;
&lt;li&gt;user ID mapping&lt;/li&gt;
&lt;li&gt;confirmed payment → credit balance&lt;/li&gt;
&lt;li&gt;idempotency protection&lt;/li&gt;
&lt;li&gt;support timeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your MVP should prove one thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A merchant in this niche can receive crypto payments and trigger the correct business action without manual checking.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Production version
&lt;/h2&gt;

&lt;p&gt;Once the MVP works, the production version can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-merchant support&lt;/li&gt;
&lt;li&gt;merchant configuration panel&lt;/li&gt;
&lt;li&gt;custom checkout branding&lt;/li&gt;
&lt;li&gt;coin/network rules per merchant&lt;/li&gt;
&lt;li&gt;webhook event replay&lt;/li&gt;
&lt;li&gt;alerting&lt;/li&gt;
&lt;li&gt;role-based admin access&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;li&gt;reporting exports&lt;/li&gt;
&lt;li&gt;failed fulfillment queue&lt;/li&gt;
&lt;li&gt;support notes&lt;/li&gt;
&lt;li&gt;customer payment status pages&lt;/li&gt;
&lt;li&gt;plugin or platform-specific adapters&lt;/li&gt;
&lt;li&gt;usage-based billing&lt;/li&gt;
&lt;li&gt;hosted white-label checkout pages&lt;/li&gt;
&lt;li&gt;agency reseller mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not build all of this before selling.&lt;/p&gt;

&lt;p&gt;Build it after you know which niche pays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revenue model
&lt;/h2&gt;

&lt;p&gt;A vertical checkout can be monetized in several ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Setup fee
&lt;/h3&gt;

&lt;p&gt;This is the easiest starting point.&lt;/p&gt;

&lt;p&gt;You charge for installation, configuration, testing, and launch.&lt;/p&gt;

&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom hosting integration&lt;/li&gt;
&lt;li&gt;WooCommerce or WHMCS customization&lt;/li&gt;
&lt;li&gt;course platform integration&lt;/li&gt;
&lt;li&gt;one-off merchant implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Monthly maintenance
&lt;/h3&gt;

&lt;p&gt;You charge for monitoring, support, small improvements, webhook troubleshooting, and compatibility updates.&lt;/p&gt;

&lt;p&gt;Good for merchants who do not want to own the payment operations layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hosted SaaS subscription
&lt;/h3&gt;

&lt;p&gt;You host the checkout layer and charge merchants monthly.&lt;/p&gt;

&lt;p&gt;Good when multiple merchants in the same niche need the same workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. White-label agency package
&lt;/h3&gt;

&lt;p&gt;You sell the checkout system to agencies that already serve the niche.&lt;/p&gt;

&lt;p&gt;Good when direct merchant acquisition is hard.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Premium support or SLA
&lt;/h3&gt;

&lt;p&gt;You charge more for faster support, webhook monitoring, emergency fixes, reporting, or merchant-specific flows.&lt;/p&gt;

&lt;p&gt;Good for payment workflows where downtime creates real business pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much can it earn?
&lt;/h2&gt;

&lt;p&gt;No article can honestly guarantee income.&lt;/p&gt;

&lt;p&gt;Revenue depends on niche selection, trust, distribution, execution, support quality, and merchant volume.&lt;/p&gt;

&lt;p&gt;But the pricing logic is realistic because API integration, payment gateway customization, WooCommerce development, and payment automation are already paid services in the freelance market. Upwork has categories for API integration and payment gateway integration work, and Codeable publishes recommended rates for expert WooCommerce work.&lt;/p&gt;

&lt;p&gt;For a practical framing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product type&lt;/th&gt;
&lt;th&gt;Possible pricing style&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic OxaPay setup for one merchant&lt;/td&gt;
&lt;td&gt;Fixed project fee&lt;/td&gt;
&lt;td&gt;Usually easiest to sell, but least defensible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical checkout MVP&lt;/td&gt;
&lt;td&gt;Project fee + monthly support&lt;/td&gt;
&lt;td&gt;Stronger because it includes business workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted checkout for one niche&lt;/td&gt;
&lt;td&gt;Monthly SaaS&lt;/td&gt;
&lt;td&gt;Requires productization and support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency launch kit&lt;/td&gt;
&lt;td&gt;License + implementation support&lt;/td&gt;
&lt;td&gt;Good if agencies already serve the niche&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advanced custom checkout&lt;/td&gt;
&lt;td&gt;Premium project + retainer&lt;/td&gt;
&lt;td&gt;Best for merchants with real volume and operational pain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A developer should not position this as passive income.&lt;/p&gt;

&lt;p&gt;A better framing is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Start as a productized service. Turn repeated implementation patterns into software. Then sell the same vertical checkout to similar merchants.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a realistic path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to avoid
&lt;/h2&gt;

&lt;p&gt;Do not build a generic crypto checkout first.&lt;/p&gt;

&lt;p&gt;That is the trap.&lt;/p&gt;

&lt;p&gt;Avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trying to support every industry on day one&lt;/li&gt;
&lt;li&gt;building a full payment gateway brand&lt;/li&gt;
&lt;li&gt;promising instant settlement or guaranteed revenue&lt;/li&gt;
&lt;li&gt;ignoring webhook idempotency&lt;/li&gt;
&lt;li&gt;delivering goods on &lt;code&gt;Paying&lt;/code&gt; instead of &lt;code&gt;Paid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;treating static addresses as simple invoice replacements&lt;/li&gt;
&lt;li&gt;storing merchant API keys unencrypted&lt;/li&gt;
&lt;li&gt;skipping support and reconciliation flows&lt;/li&gt;
&lt;li&gt;assuming a beautiful checkout page is enough&lt;/li&gt;
&lt;li&gt;writing vague copy like “accept crypto easily”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A vertical checkout wins because it solves a specific merchant problem.&lt;/p&gt;

&lt;p&gt;Stay close to that problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and operational notes
&lt;/h2&gt;

&lt;p&gt;Payment software should be boring in production.&lt;/p&gt;

&lt;p&gt;Before selling this to a merchant, implement the basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validate webhook signatures using the raw request body&lt;/li&gt;
&lt;li&gt;use timing-safe comparison for signatures&lt;/li&gt;
&lt;li&gt;store merchant API keys encrypted&lt;/li&gt;
&lt;li&gt;separate sandbox and production environments&lt;/li&gt;
&lt;li&gt;make webhook processing idempotent&lt;/li&gt;
&lt;li&gt;store raw webhook events for debugging&lt;/li&gt;
&lt;li&gt;return the expected success response quickly&lt;/li&gt;
&lt;li&gt;use queues for slow fulfillment work&lt;/li&gt;
&lt;li&gt;avoid duplicate delivery&lt;/li&gt;
&lt;li&gt;log every fulfillment action&lt;/li&gt;
&lt;li&gt;provide manual review states&lt;/li&gt;
&lt;li&gt;restrict dashboard access by role&lt;/li&gt;
&lt;li&gt;define support boundaries clearly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For crypto checkout specifically, also include clear customer warnings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send only the selected coin&lt;/li&gt;
&lt;li&gt;use only the selected network&lt;/li&gt;
&lt;li&gt;respect the expiry timer&lt;/li&gt;
&lt;li&gt;do not pay again while the transaction is confirming&lt;/li&gt;
&lt;li&gt;contact support with order ID, not random screenshots only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not small details.&lt;/p&gt;

&lt;p&gt;They are what make the product merchant-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer checklist
&lt;/h2&gt;

&lt;p&gt;Before pitching a vertical checkout, prepare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one niche landing page&lt;/li&gt;
&lt;li&gt;one demo checkout&lt;/li&gt;
&lt;li&gt;one demo admin dashboard&lt;/li&gt;
&lt;li&gt;one webhook receiver&lt;/li&gt;
&lt;li&gt;one fulfillment adapter&lt;/li&gt;
&lt;li&gt;one customer status page&lt;/li&gt;
&lt;li&gt;one support timeline&lt;/li&gt;
&lt;li&gt;one export/report example&lt;/li&gt;
&lt;li&gt;one pricing page&lt;/li&gt;
&lt;li&gt;one setup checklist&lt;/li&gt;
&lt;li&gt;one security checklist&lt;/li&gt;
&lt;li&gt;one short video walkthrough&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo should not say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is a crypto payment API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is a checkout that solves payment and fulfillment for your specific business.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Example positioning for developers
&lt;/h2&gt;

&lt;p&gt;Here is how a developer could package the offer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weak positioning
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;I integrate OxaPay into your website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Better positioning
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;I build crypto checkout workflows for hosting companies that connect payment confirmation to invoice status, service renewal, customer instructions, and support visibility.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Even better positioning
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Crypto checkout for hosting providers: accept crypto payments, detect payment status, extend services after confirmation, reduce manual support checks, and keep a searchable payment timeline for every invoice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The third version is much easier for a merchant to understand.&lt;/p&gt;

&lt;p&gt;It names the niche.&lt;/p&gt;

&lt;p&gt;It names the workflow.&lt;/p&gt;

&lt;p&gt;It names the operational value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real product is not checkout
&lt;/h2&gt;

&lt;p&gt;The checkout page is only the visible part.&lt;/p&gt;

&lt;p&gt;The real product is the system that connects payment to the merchant's business outcome.&lt;/p&gt;

&lt;p&gt;For hosting, the outcome is renewal or provisioning.&lt;/p&gt;

&lt;p&gt;For courses, the outcome is access.&lt;/p&gt;

&lt;p&gt;For gaming, the outcome is credits or roles.&lt;/p&gt;

&lt;p&gt;For software, the outcome is license delivery.&lt;/p&gt;

&lt;p&gt;For agencies, the outcome is clean client payment tracking.&lt;/p&gt;

&lt;p&gt;The developer's advantage is not that they can call an API.&lt;/p&gt;

&lt;p&gt;Many developers can call an API.&lt;/p&gt;

&lt;p&gt;The advantage is understanding a niche deeply enough to turn payment events into business events.&lt;/p&gt;

&lt;p&gt;That is where a vertical crypto checkout becomes a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;A vertical crypto checkout is one of the most realistic business opportunities developers can build on top of crypto payment infrastructure.&lt;/p&gt;

&lt;p&gt;It is more defensible than a generic integration because it solves a specific merchant workflow.&lt;/p&gt;

&lt;p&gt;It is more valuable than a payment button because it connects payment status to fulfillment, access, support, reporting, and recovery.&lt;/p&gt;

&lt;p&gt;OxaPay provides useful primitives for this kind of product: hosted invoices, white-label payment details, static addresses, payment information, payment history, webhooks, plugins, and SDKs.&lt;/p&gt;

&lt;p&gt;The developer's job is to wrap those primitives in a niche-specific product that merchants can buy.&lt;/p&gt;

&lt;p&gt;Start narrow.&lt;/p&gt;

&lt;p&gt;Pick one niche.&lt;/p&gt;

&lt;p&gt;Build one checkout.&lt;/p&gt;

&lt;p&gt;Automate one important business action.&lt;/p&gt;

&lt;p&gt;Add support visibility.&lt;/p&gt;

&lt;p&gt;Then turn the repeated pattern into a product.&lt;/p&gt;

&lt;p&gt;That is how a developer moves from “I integrate payment APIs” to “I build payment products for merchants.”&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;OxaPay Generate White Label&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Generate Static Address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/plugins" rel="noopener noreferrer"&gt;OxaPay Plugins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;OxaPay PHP SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.upwork.com/freelance-jobs/api-integration/" rel="noopener noreferrer"&gt;Upwork API Integration Jobs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.upwork.com/hire/payment-gateway-integration-freelancers/us/" rel="noopener noreferrer"&gt;Upwork Payment Gateway Integration Specialists&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.codeable.io/blog/woocommerce-development-cost/" rel="noopener noreferrer"&gt;Codeable WooCommerce Developer Cost Context&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>crypto</category>
      <category>backend</category>
    </item>
    <item>
      <title>Build a Crypto PaymentOps Service for Merchants</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:55:01 +0000</pubDate>
      <link>https://dev.to/kevins1988/build-a-crypto-paymentops-service-for-merchants-3io2</link>
      <guid>https://dev.to/kevins1988/build-a-crypto-paymentops-service-for-merchants-3io2</guid>
      <description>&lt;p&gt;Most developers look at a payment API and think about checkout.&lt;/p&gt;

&lt;p&gt;Create an invoice. Redirect the customer. Receive a webhook. Mark the order as paid.&lt;/p&gt;

&lt;p&gt;That is useful, but it is not where the deeper business opportunity is.&lt;/p&gt;

&lt;p&gt;Merchants do not only need a way to accept crypto payments. Once they start receiving real payments from real customers, they need operational control around those payments. They need to know which order belongs to which payment, when a payment is safe to fulfill, what happened to underpaid or expired invoices, why a customer says they paid but the order is still pending, and how to produce payment reports for support and finance.&lt;/p&gt;

&lt;p&gt;That layer is what I call &lt;strong&gt;Crypto PaymentOps&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, we will use OxaPay as the example payment infrastructure because its documentation exposes the primitives a developer needs for this kind of service: hosted invoices, white-label payment requests, static addresses, payment information, payment history, payment statistics, webhooks, SDKs, plugins, and automation integrations.&lt;/p&gt;

&lt;p&gt;This is not a “get rich with crypto APIs” article. It is a practical blueprint for developers who want to build a real merchant-facing service around crypto payment operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Crypto PaymentOps Service&lt;/strong&gt; helps merchants accept, track, reconcile, and act on crypto payments without forcing them to build the operational backend themselves.&lt;/p&gt;

&lt;p&gt;The developer does not sell “I will connect your payment gateway.”&lt;/p&gt;

&lt;p&gt;The developer sells something more valuable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I will build and maintain the operational payment layer that connects crypto payments to your orders, customers, support workflows, fulfillment logic, reports, and alerts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;A simple integration is a one-time technical task. A PaymentOps service can become a productized service, a monthly retainer, a SaaS tool, or a niche integration package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this problem exists
&lt;/h2&gt;

&lt;p&gt;Crypto payments create a different operational model from card payments.&lt;/p&gt;

&lt;p&gt;In many card-based systems, the merchant thinks in terms of authorization, capture, refund, dispute, and settlement. In crypto payment flows, the merchant also has to reason about wallets, networks, addresses, confirmations, transaction hashes, invoice expiry, partial payments, and callback reliability.&lt;/p&gt;

&lt;p&gt;A small merchant may be able to check payments manually at low volume. That breaks down quickly when orders increase.&lt;/p&gt;

&lt;p&gt;Common merchant questions look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did this customer actually pay?&lt;/li&gt;
&lt;li&gt;Which order does this &lt;code&gt;track_id&lt;/code&gt; belong to?&lt;/li&gt;
&lt;li&gt;Can we deliver the product now, or should we wait?&lt;/li&gt;
&lt;li&gt;Why is the invoice expired if the customer says they sent funds?&lt;/li&gt;
&lt;li&gt;What happens when the payment is underpaid?&lt;/li&gt;
&lt;li&gt;Which payments were paid today?&lt;/li&gt;
&lt;li&gt;Which orders are still unresolved?&lt;/li&gt;
&lt;li&gt;Did the webhook fail, or did the customer never pay?&lt;/li&gt;
&lt;li&gt;Can support search by order ID, email, wallet address, or transaction hash?&lt;/li&gt;
&lt;li&gt;Can finance export a daily payment report?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the opportunity.&lt;/p&gt;

&lt;p&gt;Merchants do not pay only for API calls. They pay for fewer support tickets, fewer manual checks, cleaner order state, faster fulfillment, fewer missed payments, and better operational visibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OxaPay primitives you can build on
&lt;/h2&gt;

&lt;p&gt;Before designing the service, it helps to understand the building blocks.&lt;/p&gt;

&lt;p&gt;OxaPay documents several payment and operations endpoints that map directly to a PaymentOps product:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;What it gives you&lt;/th&gt;
&lt;th&gt;Why it matters for PaymentOps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;Generate Invoice&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creates a hosted payment session and returns a &lt;code&gt;payment_url&lt;/code&gt; and &lt;code&gt;track_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The simplest way to create a merchant payment object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;Generate White Label&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Returns payment details such as address, QR code, amount, currency, network, memo, and expiry&lt;/td&gt;
&lt;td&gt;Useful when the merchant wants to own the checkout UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;Generate Static Address&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creates a reusable address linked to a &lt;code&gt;track_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Useful for account deposits, top-ups, and recurring customer deposit flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;Payment Information&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Retrieves a specific payment by &lt;code&gt;track_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Required for manual checks, reconciliation, support tools, and webhook recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;Payment History&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Lists payments with filters such as type, status, currency, network, date, amount, and pagination&lt;/td&gt;
&lt;td&gt;Required for dashboards, reports, imports, and periodic reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-statistics" rel="noopener noreferrer"&gt;Payment Statistics&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Returns aggregated payment statistics grouped by cryptocurrency&lt;/td&gt;
&lt;td&gt;Useful for merchant reporting and operational summaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;Webhook&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sends JSON callbacks to a merchant &lt;code&gt;callback_url&lt;/code&gt; when payment status changes&lt;/td&gt;
&lt;td&gt;The event layer that drives fulfillment and automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;Laravel SDK&lt;/a&gt; / &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;SDK methods for payments, payouts, account data, and webhook validation&lt;/td&gt;
&lt;td&gt;Useful when you want faster implementation in common stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://apps.make.com/oxapay-crypto-pay-gtw" rel="noopener noreferrer"&gt;Make App&lt;/a&gt; / n8n workflows&lt;/td&gt;
&lt;td&gt;Low-code workflow options around invoices, webhooks, static addresses, payouts, and notifications&lt;/td&gt;
&lt;td&gt;Useful for merchants that need automation without a full custom backend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important point is that OxaPay is not only a payment page. It exposes payment state, history, statistics, callbacks, and integration paths. That gives developers enough surface area to build an operational service around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually building
&lt;/h2&gt;

&lt;p&gt;A Crypto PaymentOps Service can start as a custom service for one merchant, but it should be designed like a repeatable product.&lt;/p&gt;

&lt;p&gt;At minimum, you are building a backend and dashboard that sits between the merchant’s business system and OxaPay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merchant store / app / bot / CRM
        |
        | create payment request
        v
PaymentOps backend
        |
        | POST /payment/invoice or /payment/white-label
        v
OxaPay payment infrastructure
        |
        | customer pays
        v
OxaPay webhook -&amp;gt; PaymentOps backend
        |
        | validate HMAC, store event, update state
        v
Merchant actions
- mark order paid
- activate account
- deliver file
- notify support
- update CRM
- create reconciliation record
- generate daily report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The merchant sees the result as an operational product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment dashboard&lt;/li&gt;
&lt;li&gt;order/payment matching&lt;/li&gt;
&lt;li&gt;failed and unresolved payment queue&lt;/li&gt;
&lt;li&gt;customer support search&lt;/li&gt;
&lt;li&gt;Telegram/Slack/Discord alerts&lt;/li&gt;
&lt;li&gt;daily payment reports&lt;/li&gt;
&lt;li&gt;webhook logs&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;li&gt;fulfillment automation&lt;/li&gt;
&lt;li&gt;optional finance view&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is much easier to sell than a raw API integration because it maps to daily merchant problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who would pay for this?
&lt;/h2&gt;

&lt;p&gt;Not every merchant needs a PaymentOps service. The best customers are businesses where payment state affects fulfillment, access, support, or reporting.&lt;/p&gt;

&lt;p&gt;Good targets include:&lt;/p&gt;

&lt;h3&gt;
  
  
  Digital product sellers
&lt;/h3&gt;

&lt;p&gt;They sell license keys, downloadable files, templates, software, or paid resources. They need automatic delivery after payment and support visibility when something goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  SaaS and membership businesses
&lt;/h3&gt;

&lt;p&gt;They need to activate plans, extend access, downgrade expired users, and track payment status across user accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosting, VPN, and infrastructure sellers
&lt;/h3&gt;

&lt;p&gt;They usually have clear order states, service provisioning, renewals, and support tickets. A payment event often needs to trigger account activation or extension.&lt;/p&gt;

&lt;h3&gt;
  
  
  Telegram, Discord, and community businesses
&lt;/h3&gt;

&lt;p&gt;They need to sell access, paid roles, private channels, premium groups, and digital content. The payment is only one step; access control is the real product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agencies and remote service providers
&lt;/h3&gt;

&lt;p&gt;They work with international clients and need a clean way to issue invoices, track payment status, notify the team, and export records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Marketplaces and multi-vendor platforms
&lt;/h3&gt;

&lt;p&gt;They eventually need payout workflows, revenue splits, and more advanced reporting. This is harder, but it can become a higher-value PaymentOps project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Businesses already accepting crypto manually
&lt;/h3&gt;

&lt;p&gt;This is one of the easiest segments to sell to. They already believe in crypto payments. Their pain is operational chaos: manual wallet checks, screenshots from customers, inconsistent order states, and weak reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the MVP should include
&lt;/h2&gt;

&lt;p&gt;Do not start by building a full SaaS platform.&lt;/p&gt;

&lt;p&gt;Start with a merchant-specific MVP that solves a narrow operational problem end to end.&lt;/p&gt;

&lt;p&gt;A strong MVP includes seven pieces.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Payment creation
&lt;/h3&gt;

&lt;p&gt;Create an OxaPay invoice when the merchant creates an order.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;POST /payment/invoice&lt;/code&gt; endpoint requires an &lt;code&gt;amount&lt;/code&gt; and can include fields such as &lt;code&gt;currency&lt;/code&gt;, &lt;code&gt;lifetime&lt;/code&gt;, &lt;code&gt;callback_url&lt;/code&gt;, &lt;code&gt;return_url&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;order_id&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and &lt;code&gt;sandbox&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A basic Node.js example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createCryptoInvoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/payment/invoice`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/thank-you`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OxaPay invoice request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paymentUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expiredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expired_at&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your service should store the OxaPay &lt;code&gt;track_id&lt;/code&gt; next to the merchant’s internal &lt;code&gt;order_id&lt;/code&gt;. That mapping is the foundation of reconciliation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Webhook receiver
&lt;/h3&gt;

&lt;p&gt;Webhook handling is the most important part of the service.&lt;/p&gt;

&lt;p&gt;OxaPay sends payment updates to the &lt;code&gt;callback_url&lt;/code&gt; you provide in merchant requests. The webhook documentation says the receiver should accept HTTPS &lt;code&gt;POST&lt;/code&gt; requests with &lt;code&gt;application/json&lt;/code&gt;, return HTTP 200 with content &lt;code&gt;ok&lt;/code&gt;, and validate the HMAC signature using the merchant API key.&lt;/p&gt;

&lt;p&gt;A simplified Express receiver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Important: keep the raw body for HMAC validation.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hmac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;safeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectedHmac&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processOxaPayEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// OxaPay expects HTTP 200 with content "ok".&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;bBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook handler should never blindly fulfill an order just because a callback arrived. It should validate the signature, check the payment status, verify the amount/order mapping, and process the event idempotently.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Idempotent event processing
&lt;/h3&gt;

&lt;p&gt;Payment callbacks can be retried. Network failures happen. Your endpoint may receive the same event more than once.&lt;/p&gt;

&lt;p&gt;OxaPay’s webhook docs describe retry behavior when delivery fails. That means idempotency is not optional.&lt;/p&gt;

&lt;p&gt;A practical pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processOxaPayEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;track_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unmatchedWebhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No local payment session found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;eventKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;eventKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;rawPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paymentStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fulfillmentJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deliver_after_payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;queued&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact payload shape should be confirmed against the live webhook payload you receive in testing, but the architectural rule is stable: store every payment event, update payment state once, and trigger fulfillment only from safe states.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Payment state machine
&lt;/h3&gt;

&lt;p&gt;Do not reduce every payment to &lt;code&gt;pending&lt;/code&gt; and &lt;code&gt;paid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A PaymentOps product should model payment lifecycle states explicitly. OxaPay’s payment status documentation includes states such as &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;paying&lt;/code&gt;, &lt;code&gt;paid&lt;/code&gt;, &lt;code&gt;manual_accept&lt;/code&gt;, &lt;code&gt;underpaid&lt;/code&gt;, &lt;code&gt;refunding&lt;/code&gt;, &lt;code&gt;refunded&lt;/code&gt;, and &lt;code&gt;expired&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For merchant operations, you can map them into action categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OxaPay status&lt;/th&gt;
&lt;th&gt;Merchant interpretation&lt;/th&gt;
&lt;th&gt;Recommended action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payment created, payer has not selected payment currency yet&lt;/td&gt;
&lt;td&gt;Show invoice as created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waiting&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payer selected currency/network, waiting for funds&lt;/td&gt;
&lt;td&gt;Keep order pending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paying&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payment attempt is in progress&lt;/td&gt;
&lt;td&gt;Do not fulfill yet; wait for &lt;code&gt;paid&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payment completed&lt;/td&gt;
&lt;td&gt;Fulfill order / activate service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;underpaid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payment amount is below requested amount&lt;/td&gt;
&lt;td&gt;Put into review queue or ask customer to complete payment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Payment window closed&lt;/td&gt;
&lt;td&gt;Cancel or regenerate payment session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;manual_accept&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Merchant accepted payment manually&lt;/td&gt;
&lt;td&gt;Fulfill if merchant policy allows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refunding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refund in progress&lt;/td&gt;
&lt;td&gt;Pause fulfillment or mark for support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refunded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refund completed&lt;/td&gt;
&lt;td&gt;Close order or reverse access&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This state machine is one of the reasons merchants may pay for your service. They do not want to invent operational rules for each payment edge case.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Support and reconciliation dashboard
&lt;/h3&gt;

&lt;p&gt;The dashboard does not need to be beautiful at first. It needs to answer operational questions quickly.&lt;/p&gt;

&lt;p&gt;The MVP dashboard should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search by order ID&lt;/li&gt;
&lt;li&gt;search by OxaPay &lt;code&gt;track_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;search by customer email&lt;/li&gt;
&lt;li&gt;list of paid payments&lt;/li&gt;
&lt;li&gt;list of underpaid payments&lt;/li&gt;
&lt;li&gt;list of expired payments&lt;/li&gt;
&lt;li&gt;list of orders paid but not fulfilled&lt;/li&gt;
&lt;li&gt;list of webhook events&lt;/li&gt;
&lt;li&gt;transaction hash view if available through payment information&lt;/li&gt;
&lt;li&gt;export CSV for a selected date range&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;Payment Information&lt;/a&gt; endpoint is important here because it returns detailed data for a specific &lt;code&gt;track_id&lt;/code&gt;, including payment status and transaction-level fields. The &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;Payment History&lt;/a&gt; endpoint is useful for filtered reporting and back-office lists.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Alerts and notifications
&lt;/h3&gt;

&lt;p&gt;A PaymentOps service becomes much more valuable when the merchant does not need to log in constantly.&lt;/p&gt;

&lt;p&gt;Add alerts for events like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;new paid order&lt;/li&gt;
&lt;li&gt;high-value payment&lt;/li&gt;
&lt;li&gt;underpaid payment&lt;/li&gt;
&lt;li&gt;expired invoice&lt;/li&gt;
&lt;li&gt;webhook signature failure&lt;/li&gt;
&lt;li&gt;payment received but order not found&lt;/li&gt;
&lt;li&gt;paid payment with failed fulfillment&lt;/li&gt;
&lt;li&gt;daily summary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can send alerts to Slack, Telegram, Discord, email, or the merchant’s existing support tool.&lt;/p&gt;

&lt;p&gt;OxaPay also documents n8n and Make-based automation workflows for use cases like payment notifications, digital delivery, and messaging. That matters because you can offer two service tiers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;custom backend integration for serious merchants&lt;/li&gt;
&lt;li&gt;low-code automation setup for smaller merchants&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  7. Daily merchant report
&lt;/h3&gt;

&lt;p&gt;A daily report is simple but valuable.&lt;/p&gt;

&lt;p&gt;It can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total paid payments&lt;/li&gt;
&lt;li&gt;total received amount by currency&lt;/li&gt;
&lt;li&gt;number of expired invoices&lt;/li&gt;
&lt;li&gt;number of underpaid payments&lt;/li&gt;
&lt;li&gt;unresolved support cases&lt;/li&gt;
&lt;li&gt;failed fulfillment jobs&lt;/li&gt;
&lt;li&gt;payout or settlement notes if relevant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OxaPay’s &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-statistics" rel="noopener noreferrer"&gt;Payment Statistics&lt;/a&gt; endpoint can support aggregated reporting, while Payment History can support detailed exports.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical database model
&lt;/h2&gt;

&lt;p&gt;Here is a simple schema you can adapt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;merchants&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;oxapay_merchant_key_encrypted&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;default_currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'unpaid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;fulfillment_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'not_fulfilled'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'oxapay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expired_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fulfillment_jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;reconciliation_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;merchant_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;issue_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can keep this much simpler for the first merchant. But the core entities should remain: orders, payment sessions, payment events, fulfillment jobs, and reconciliation items.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production architecture
&lt;/h2&gt;

&lt;p&gt;An MVP can be a single backend. A production service should separate payment ingestion from business actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              +--------------------+
              | Merchant frontend  |
              +---------+----------+
                        |
                        v
              +--------------------+
              | PaymentOps API     |
              +---------+----------+
                        |
        create invoice  |  query history/info
                        v
              +--------------------+
              | OxaPay API         |
              +---------+----------+
                        |
                        | webhook
                        v
              +--------------------+
              | Webhook receiver   |
              +---------+----------+
                        |
                        v
              +--------------------+
              | Event store        |
              +---------+----------+
                        |
                        v
              +--------------------+
              | Job queue          |
              +---------+----------+
                        |
        +---------------+----------------+
        |               |                |
        v               v                v
  Fulfillment       Notifications     Reconciliation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design keeps the webhook receiver fast. It validates the callback, stores the event, returns &lt;code&gt;ok&lt;/code&gt;, and lets workers handle slower downstream tasks.&lt;/p&gt;

&lt;p&gt;That matters because payment webhooks are infrastructure events. Your webhook endpoint should not wait on email providers, CRM APIs, Discord bots, or fulfillment systems before acknowledging the callback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The service packages you can sell
&lt;/h2&gt;

&lt;p&gt;A PaymentOps business becomes easier to sell when you productize it.&lt;/p&gt;

&lt;p&gt;Here are practical packages a developer could offer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package 1: Crypto Payment Setup
&lt;/h3&gt;

&lt;p&gt;For merchants who only need a reliable first integration.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OxaPay invoice creation&lt;/li&gt;
&lt;li&gt;order mapping&lt;/li&gt;
&lt;li&gt;webhook receiver&lt;/li&gt;
&lt;li&gt;HMAC validation&lt;/li&gt;
&lt;li&gt;paid order activation&lt;/li&gt;
&lt;li&gt;basic admin view&lt;/li&gt;
&lt;li&gt;basic testing in sandbox&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible pricing model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one-time setup fee&lt;/li&gt;
&lt;li&gt;optional monthly maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the easiest entry point, but it is also the most commoditized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package 2: PaymentOps Dashboard
&lt;/h3&gt;

&lt;p&gt;For merchants who already accept crypto or expect regular volume.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment session dashboard&lt;/li&gt;
&lt;li&gt;order/payment matching&lt;/li&gt;
&lt;li&gt;webhook logs&lt;/li&gt;
&lt;li&gt;underpaid/expired queues&lt;/li&gt;
&lt;li&gt;manual review notes&lt;/li&gt;
&lt;li&gt;daily CSV export&lt;/li&gt;
&lt;li&gt;team alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible pricing model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup fee&lt;/li&gt;
&lt;li&gt;monthly retainer&lt;/li&gt;
&lt;li&gt;optional per-seat support pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is stronger because it solves ongoing operational pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package 3: Fulfillment Automation
&lt;/h3&gt;

&lt;p&gt;For digital product, SaaS, membership, hosting, and community businesses.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid payment -&amp;gt; deliver file&lt;/li&gt;
&lt;li&gt;paid payment -&amp;gt; send license key&lt;/li&gt;
&lt;li&gt;paid payment -&amp;gt; activate SaaS plan&lt;/li&gt;
&lt;li&gt;paid payment -&amp;gt; add user to Telegram/Discord&lt;/li&gt;
&lt;li&gt;expired payment -&amp;gt; send reminder&lt;/li&gt;
&lt;li&gt;failed fulfillment -&amp;gt; alert support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible pricing model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;implementation fee&lt;/li&gt;
&lt;li&gt;monthly monitoring fee&lt;/li&gt;
&lt;li&gt;premium support SLA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is often easier to justify because the merchant sees direct time savings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package 4: Managed Crypto PaymentOps
&lt;/h3&gt;

&lt;p&gt;For merchants who want ongoing operational support.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;weekly payment review&lt;/li&gt;
&lt;li&gt;unresolved payment queue management&lt;/li&gt;
&lt;li&gt;finance export&lt;/li&gt;
&lt;li&gt;support workflow optimization&lt;/li&gt;
&lt;li&gt;webhook monitoring&lt;/li&gt;
&lt;li&gt;merchant staff documentation&lt;/li&gt;
&lt;li&gt;incident response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Possible pricing model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;monthly retainer&lt;/li&gt;
&lt;li&gt;volume-based operations fee&lt;/li&gt;
&lt;li&gt;custom enterprise setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This model is not just software. It is a service business around payment operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  How much can this make?
&lt;/h2&gt;

&lt;p&gt;There is no honest universal answer.&lt;/p&gt;

&lt;p&gt;Revenue depends on your niche, technical skill, distribution, support quality, merchant volume, and whether you sell one-time setup or recurring operations.&lt;/p&gt;

&lt;p&gt;But the monetization paths are clear.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;How you get paid&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup project&lt;/td&gt;
&lt;td&gt;Fixed fee for implementation&lt;/td&gt;
&lt;td&gt;First clients, simple integrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly maintenance&lt;/td&gt;
&lt;td&gt;Recurring fee for monitoring and updates&lt;/td&gt;
&lt;td&gt;Merchants that depend on payment automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS dashboard&lt;/td&gt;
&lt;td&gt;Monthly subscription&lt;/td&gt;
&lt;td&gt;Reusable product across similar merchants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed service&lt;/td&gt;
&lt;td&gt;Retainer for operational support&lt;/td&gt;
&lt;td&gt;Higher-volume merchants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency package&lt;/td&gt;
&lt;td&gt;White-label kit sold to agencies&lt;/td&gt;
&lt;td&gt;Developers with agency partnerships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom automation&lt;/td&gt;
&lt;td&gt;Per-workflow fee&lt;/td&gt;
&lt;td&gt;Merchants with specific fulfillment logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Freelance and backend/API integration rates vary widely. Public freelancer marketplace pages such as Upwork’s developer and API integration rate pages show broad ranges depending on experience, stack, and project complexity. Use those ranges only as market context, not as a promise.&lt;/p&gt;

&lt;p&gt;A realistic early-stage positioning might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offer&lt;/th&gt;
&lt;th&gt;Low-end positioning&lt;/th&gt;
&lt;th&gt;Higher-value positioning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic OxaPay integration&lt;/td&gt;
&lt;td&gt;A few hundred dollars&lt;/td&gt;
&lt;td&gt;$1k+ if tied to order automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook + fulfillment setup&lt;/td&gt;
&lt;td&gt;Several hundred dollars&lt;/td&gt;
&lt;td&gt;A few thousand dollars for custom systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merchant dashboard&lt;/td&gt;
&lt;td&gt;Small monthly fee&lt;/td&gt;
&lt;td&gt;Higher recurring fee if it becomes daily ops tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed PaymentOps&lt;/td&gt;
&lt;td&gt;Monthly support retainer&lt;/td&gt;
&lt;td&gt;Premium retainer if tied to SLA and finance workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical package&lt;/td&gt;
&lt;td&gt;Implementation fee&lt;/td&gt;
&lt;td&gt;License + support + customization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The best path is not to compete as “the cheapest payment integration developer.” The better path is to own a narrow operational outcome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“I help digital product sellers deliver files automatically after crypto payment.”&lt;/li&gt;
&lt;li&gt;“I help hosting companies activate crypto-paid orders without manual checks.”&lt;/li&gt;
&lt;li&gt;“I help Telegram communities sell paid access with crypto and automated membership control.”&lt;/li&gt;
&lt;li&gt;“I help merchants reconcile OxaPay payments with orders and support tickets.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Specific outcomes sell better than generic integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes this a real business instead of a side script?
&lt;/h2&gt;

&lt;p&gt;A script becomes a business when it has repeatable delivery, clear positioning, support boundaries, and a defined customer.&lt;/p&gt;

&lt;p&gt;For PaymentOps, the business value comes from four things.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. You own the merchant’s operational workflow
&lt;/h3&gt;

&lt;p&gt;The payment API is only one piece. The workflow includes order status, support, fulfillment, reporting, and alerts.&lt;/p&gt;

&lt;p&gt;Once your system becomes part of that workflow, the merchant is less likely to treat your work as a disposable setup task.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. You reduce manual work
&lt;/h3&gt;

&lt;p&gt;Manual payment checking is expensive even when nobody calculates it. Staff time, customer complaints, missed orders, duplicate support tickets, and unclear finance records all cost money.&lt;/p&gt;

&lt;p&gt;Your service should make those costs visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. You create recurring maintenance needs
&lt;/h3&gt;

&lt;p&gt;Payment operations need monitoring. Webhooks can fail. APIs change. Stores change. Staff ask for reports. Merchants add new products or workflows.&lt;/p&gt;

&lt;p&gt;That supports monthly pricing.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. You can specialize by niche
&lt;/h3&gt;

&lt;p&gt;A generic crypto payment dashboard is hard to sell.&lt;/p&gt;

&lt;p&gt;A dashboard for hosting companies, digital product sellers, paid Discord communities, Telegram course sellers, or SaaS plan activation is easier to explain and easier to price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and operational rules
&lt;/h2&gt;

&lt;p&gt;If you build this service, treat it like payment infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate every webhook
&lt;/h3&gt;

&lt;p&gt;OxaPay uses an HMAC signature over the raw request body. Validate it before processing any payment event.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not fulfill on weak states
&lt;/h3&gt;

&lt;p&gt;Do not deliver products on &lt;code&gt;paying&lt;/code&gt;. Wait for &lt;code&gt;paid&lt;/code&gt; unless the merchant explicitly defines a different manual policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store raw events
&lt;/h3&gt;

&lt;p&gt;Raw webhook payloads help with debugging, reconciliation, and support. Store them securely and avoid exposing sensitive data unnecessarily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Process events idempotently
&lt;/h3&gt;

&lt;p&gt;A repeated webhook should not trigger duplicate delivery, duplicate license keys, duplicate account activation, or duplicate notifications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encrypt API keys
&lt;/h3&gt;

&lt;p&gt;If your service stores merchant API keys, encrypt them. Limit who can access them. Do not log them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate merchant keys from payout keys
&lt;/h3&gt;

&lt;p&gt;If your PaymentOps service later adds payouts, treat payout keys as higher-risk credentials. OxaPay’s docs distinguish merchant, payout, and general API keys. Your system should also separate them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add manual review queues
&lt;/h3&gt;

&lt;p&gt;Underpaid, expired, unmatched, and suspicious payments should go to a review queue. Do not hide edge cases from the merchant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Give support a timeline
&lt;/h3&gt;

&lt;p&gt;A support agent should see the payment timeline: invoice created, webhook received, status changed, fulfillment queued, fulfillment completed or failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test with sandbox and real webhook tools
&lt;/h3&gt;

&lt;p&gt;Use sandbox mode for payment creation where applicable. For webhook testing, OxaPay’s docs mention public webhook testing tools and tunneling tools such as webhook.site, requestcatcher.com, and ngrok because callbacks need a reachable HTTPS endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first version you should build
&lt;/h2&gt;

&lt;p&gt;Here is a practical build order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 1: Merchant-specific MVP
&lt;/h3&gt;

&lt;p&gt;Build for one narrow use case.&lt;/p&gt;

&lt;p&gt;Example: a digital product seller wants to sell license keys.&lt;/p&gt;

&lt;p&gt;Minimum scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create OxaPay invoice&lt;/li&gt;
&lt;li&gt;store &lt;code&gt;track_id&lt;/code&gt; with order&lt;/li&gt;
&lt;li&gt;receive and validate webhook&lt;/li&gt;
&lt;li&gt;mark order paid only on &lt;code&gt;paid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;send license key&lt;/li&gt;
&lt;li&gt;show payment status in admin panel&lt;/li&gt;
&lt;li&gt;log failed fulfillment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 2: Operational dashboard
&lt;/h3&gt;

&lt;p&gt;Add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment list&lt;/li&gt;
&lt;li&gt;filters by status&lt;/li&gt;
&lt;li&gt;search by order ID and &lt;code&gt;track_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;underpaid/expired queue&lt;/li&gt;
&lt;li&gt;webhook event log&lt;/li&gt;
&lt;li&gt;daily CSV export&lt;/li&gt;
&lt;li&gt;admin notes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 3: Alerts and reporting
&lt;/h3&gt;

&lt;p&gt;Add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegram/Slack/Discord alerts&lt;/li&gt;
&lt;li&gt;daily summary&lt;/li&gt;
&lt;li&gt;high-value payment alerts&lt;/li&gt;
&lt;li&gt;failed fulfillment alerts&lt;/li&gt;
&lt;li&gt;unresolved payment digest&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 4: Productize
&lt;/h3&gt;

&lt;p&gt;Turn the implementation into a repeatable package.&lt;/p&gt;

&lt;p&gt;Add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;onboarding checklist&lt;/li&gt;
&lt;li&gt;configuration screen&lt;/li&gt;
&lt;li&gt;merchant documentation&lt;/li&gt;
&lt;li&gt;reusable webhook module&lt;/li&gt;
&lt;li&gt;reusable fulfillment adapters&lt;/li&gt;
&lt;li&gt;pricing page&lt;/li&gt;
&lt;li&gt;demo account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not start with a large multi-merchant SaaS unless you already have distribution. Start with one merchant segment, solve the operational workflow deeply, then reuse the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example niche: digital product seller
&lt;/h2&gt;

&lt;p&gt;Let’s make the idea concrete.&lt;/p&gt;

&lt;p&gt;A merchant sells downloadable templates and software licenses. They already receive international demand but do not want to handle card payments for every region.&lt;/p&gt;

&lt;p&gt;You build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a crypto checkout button&lt;/li&gt;
&lt;li&gt;OxaPay invoice generation&lt;/li&gt;
&lt;li&gt;webhook validation&lt;/li&gt;
&lt;li&gt;license key delivery after &lt;code&gt;paid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;payment status page for customers&lt;/li&gt;
&lt;li&gt;support dashboard for unresolved payments&lt;/li&gt;
&lt;li&gt;CSV export for finance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The merchant pays because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customers can pay in crypto&lt;/li&gt;
&lt;li&gt;staff no longer manually check payments&lt;/li&gt;
&lt;li&gt;customers get products faster&lt;/li&gt;
&lt;li&gt;support can see payment state&lt;/li&gt;
&lt;li&gt;finance gets cleaner exports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core implementation is not technically impossible. The value is in packaging the workflow so the merchant does not have to think about payment state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example niche: hosting provider
&lt;/h2&gt;

&lt;p&gt;A hosting provider has a different workflow.&lt;/p&gt;

&lt;p&gt;A customer buys a monthly service. The payment event should extend the service period or provision a new account.&lt;/p&gt;

&lt;p&gt;You build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OxaPay invoice creation from hosting order&lt;/li&gt;
&lt;li&gt;webhook listener&lt;/li&gt;
&lt;li&gt;service provisioning job&lt;/li&gt;
&lt;li&gt;renewal reminder&lt;/li&gt;
&lt;li&gt;expired invoice handling&lt;/li&gt;
&lt;li&gt;admin dashboard&lt;/li&gt;
&lt;li&gt;support payment timeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The merchant pays because payment state now controls service access automatically.&lt;/p&gt;

&lt;p&gt;This is a better offer than “I integrate a crypto gateway” because it connects payment to revenue operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example niche: paid community
&lt;/h2&gt;

&lt;p&gt;A paid Telegram or Discord community does not mainly need checkout. It needs access control.&lt;/p&gt;

&lt;p&gt;You build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment command or checkout page&lt;/li&gt;
&lt;li&gt;OxaPay invoice&lt;/li&gt;
&lt;li&gt;webhook confirmation&lt;/li&gt;
&lt;li&gt;role/channel access after &lt;code&gt;paid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;access expiry logic&lt;/li&gt;
&lt;li&gt;renewal reminder&lt;/li&gt;
&lt;li&gt;admin revenue dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The merchant pays because access management is painful to handle manually.&lt;/p&gt;

&lt;p&gt;This niche can later become a separate article, product, or SaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to avoid
&lt;/h2&gt;

&lt;p&gt;A PaymentOps service can fail if you position it badly.&lt;/p&gt;

&lt;p&gt;Avoid these mistakes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not sell only “crypto payment integration”
&lt;/h3&gt;

&lt;p&gt;That sounds like a commodity task.&lt;/p&gt;

&lt;p&gt;Sell payment operations, fulfillment, support visibility, and reconciliation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not build for everyone
&lt;/h3&gt;

&lt;p&gt;A generic merchant dashboard is harder to sell than a dashboard for a specific niche.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not promise income
&lt;/h3&gt;

&lt;p&gt;Developers can monetize this, but revenue depends on sales, niche selection, trust, and execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not ignore support workflows
&lt;/h3&gt;

&lt;p&gt;Support is where many payment problems become visible. If support cannot use your system, the merchant will still feel operational pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not skip security
&lt;/h3&gt;

&lt;p&gt;Webhook validation, API key handling, idempotency, and access control are part of the product, not optional extras.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not overclaim what the payment provider does
&lt;/h3&gt;

&lt;p&gt;Use the documentation carefully. For example, if a claim is not in the docs, do not build your sales pitch around it. For OxaPay, the documented features are already enough: invoice generation, white-label payment, static addresses, webhooks, histories, statistics, SDKs, plugins, and automation integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A landing page structure for your service
&lt;/h2&gt;

&lt;p&gt;If you wanted to sell this as a developer service, your page could look like this:&lt;/p&gt;

&lt;h3&gt;
  
  
  Headline
&lt;/h3&gt;

&lt;p&gt;Crypto payment operations for digital merchants — built on OxaPay.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subheadline
&lt;/h3&gt;

&lt;p&gt;We help merchants create crypto invoices, track payment status, automate fulfillment, handle underpaid and expired payments, and give support teams a clear payment dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who it is for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;SaaS founders&lt;/li&gt;
&lt;li&gt;digital product sellers&lt;/li&gt;
&lt;li&gt;hosting providers&lt;/li&gt;
&lt;li&gt;paid communities&lt;/li&gt;
&lt;li&gt;international service providers&lt;/li&gt;
&lt;li&gt;agencies accepting crypto payments&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What is included
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;OxaPay invoice integration&lt;/li&gt;
&lt;li&gt;webhook validation&lt;/li&gt;
&lt;li&gt;order/payment matching&lt;/li&gt;
&lt;li&gt;paid order automation&lt;/li&gt;
&lt;li&gt;unresolved payment queue&lt;/li&gt;
&lt;li&gt;support dashboard&lt;/li&gt;
&lt;li&gt;daily reports&lt;/li&gt;
&lt;li&gt;alerting&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Business outcome
&lt;/h3&gt;

&lt;p&gt;Less manual payment checking. Faster fulfillment. Cleaner support. Better payment visibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical outcome
&lt;/h3&gt;

&lt;p&gt;A reliable event-driven payment workflow connected to the merchant’s existing stack.&lt;/p&gt;

&lt;p&gt;This positioning is much stronger than “I can integrate OxaPay.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer checklist
&lt;/h2&gt;

&lt;p&gt;Before selling your first PaymentOps package, prepare these assets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a demo checkout flow&lt;/li&gt;
&lt;li&gt;a demo dashboard&lt;/li&gt;
&lt;li&gt;webhook validation code&lt;/li&gt;
&lt;li&gt;idempotent event processor&lt;/li&gt;
&lt;li&gt;order/payment matching logic&lt;/li&gt;
&lt;li&gt;basic CSV export&lt;/li&gt;
&lt;li&gt;one fulfillment adapter&lt;/li&gt;
&lt;li&gt;support issue timeline&lt;/li&gt;
&lt;li&gt;merchant onboarding checklist&lt;/li&gt;
&lt;li&gt;clear support boundaries&lt;/li&gt;
&lt;li&gt;simple pricing packages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your first client does not need every feature. But they need to see that you understand payment operations, not just API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;The business opportunity is not simply accepting crypto payments.&lt;/p&gt;

&lt;p&gt;The opportunity is helping merchants operate crypto payments reliably.&lt;/p&gt;

&lt;p&gt;OxaPay provides the payment infrastructure primitives: invoices, white-label payment requests, static addresses, payment information, histories, statistics, webhooks, SDKs, plugins, and automation options. A developer can use those primitives to build a merchant-facing PaymentOps service that handles order matching, fulfillment, support visibility, alerts, reports, and reconciliation.&lt;/p&gt;

&lt;p&gt;That is a real product opportunity because it solves a real merchant problem.&lt;/p&gt;

&lt;p&gt;Merchants do not want to become payment infrastructure engineers. They want orders paid, products delivered, customers supported, and finance records cleaned up.&lt;/p&gt;

&lt;p&gt;A developer who can turn payment APIs into that operational layer can sell more than code.&lt;/p&gt;

&lt;p&gt;They can sell reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-statistics" rel="noopener noreferrer"&gt;OxaPay Payment Statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;OxaPay White Label Payment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Static Address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;OxaPay Laravel SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.make.com/oxapay-crypto-pay-gtw" rel="noopener noreferrer"&gt;OxaPay Make App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.upwork.com/resources/upwork-hourly-rates" rel="noopener noreferrer"&gt;Upwork Developer Rate Context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.upwork.com/hire/api-integration-freelancers/" rel="noopener noreferrer"&gt;Upwork API Integration Freelancer Context&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>crypto</category>
      <category>backend</category>
    </item>
    <item>
      <title>10 Crypto Payment Products Developers Can Build for Merchants</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:54:47 +0000</pubDate>
      <link>https://dev.to/kevins1988/10-crypto-payment-products-developers-can-build-for-merchants-2cbj</link>
      <guid>https://dev.to/kevins1988/10-crypto-payment-products-developers-can-build-for-merchants-2cbj</guid>
      <description>&lt;p&gt;Most developers see a payment API and think about one thing: checkout.&lt;/p&gt;

&lt;p&gt;Create an invoice. Redirect the customer. Wait for a callback. Mark the order as paid.&lt;/p&gt;

&lt;p&gt;That is useful, but it is only the first layer.&lt;/p&gt;

&lt;p&gt;The bigger opportunity is not simply helping merchants accept crypto payments. The bigger opportunity is helping them operate crypto payments after the customer clicks pay.&lt;/p&gt;

&lt;p&gt;That means order activation, payment status tracking, support workflows, access control, reconciliation, payout queues, revenue sharing, automation, reporting, renewal reminders, and customer-facing payment visibility.&lt;/p&gt;

&lt;p&gt;That is where developers can build real products.&lt;/p&gt;

&lt;p&gt;In this article, I will use &lt;a href="https://docs.oxapay.com/" rel="noopener noreferrer"&gt;OxaPay&lt;/a&gt; as the example crypto payment infrastructure because its documentation exposes the primitives developers need for merchant-facing products: hosted invoices, white-label payment details, static addresses, payment information, payment history, payment status tables, webhooks, payout APIs, SDKs, plugins, and automation integrations.&lt;/p&gt;

&lt;p&gt;This is not a “make money fast with crypto” article.&lt;/p&gt;

&lt;p&gt;It is a product map for developers who want to build useful software or services for merchants that want crypto payments without building payment operations from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core thesis
&lt;/h2&gt;

&lt;p&gt;A payment gateway gives merchants a way to receive money.&lt;/p&gt;

&lt;p&gt;A developer business is built when you solve the operational problems around receiving money.&lt;/p&gt;

&lt;p&gt;For crypto payments, those problems often look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which customer paid this invoice?&lt;/li&gt;
&lt;li&gt;Which order should be fulfilled?&lt;/li&gt;
&lt;li&gt;Is the payment confirmed enough to deliver the product?&lt;/li&gt;
&lt;li&gt;Why does the customer say they paid while the order is still pending?&lt;/li&gt;
&lt;li&gt;Was the invoice expired, underpaid, failed, or still waiting?&lt;/li&gt;
&lt;li&gt;Did the webhook arrive?&lt;/li&gt;
&lt;li&gt;Did the webhook get processed twice?&lt;/li&gt;
&lt;li&gt;Can support search by order ID, email, &lt;code&gt;track_id&lt;/code&gt;, wallet, or transaction hash?&lt;/li&gt;
&lt;li&gt;Can finance export payment reports?&lt;/li&gt;
&lt;li&gt;Can a marketplace split revenue between sellers?&lt;/li&gt;
&lt;li&gt;Can a SaaS app activate a plan after a successful crypto payment?&lt;/li&gt;
&lt;li&gt;Can a Telegram community grant paid access automatically?&lt;/li&gt;
&lt;li&gt;Can a digital seller sell globally without manually checking every payment?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the kinds of problems merchants pay to solve.&lt;/p&gt;

&lt;p&gt;The payment API is the foundation. The product is the operational layer you build on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is a real developer opportunity
&lt;/h2&gt;

&lt;p&gt;Payment integration is already a paid development category. Freelance marketplaces list payment gateway integration jobs, and merchants regularly pay developers to integrate payment systems into stores, SaaS apps, marketplaces, and internal tools. Upwork, for example, has a dedicated category for &lt;a href="https://www.upwork.com/freelance-jobs/payment-gateway-integration/" rel="noopener noreferrer"&gt;payment gateway integration jobs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Developer pricing varies widely by skill, geography, niche, and positioning. Upwork reports software developer rates commonly ranging from about &lt;a href="https://www.upwork.com/hire/software-developers/cost/" rel="noopener noreferrer"&gt;$10 to $100 per hour&lt;/a&gt;, while specialist WooCommerce development can command higher ranges according to platforms such as &lt;a href="https://www.codeable.io/blog/woocommerce-development-cost/" rel="noopener noreferrer"&gt;Codeable&lt;/a&gt;. These numbers are not income promises. They simply show that merchants already budget for integration, automation, and commerce infrastructure work.&lt;/p&gt;

&lt;p&gt;The stronger opportunity is not to sell a generic “crypto payment setup.”&lt;/p&gt;

&lt;p&gt;The stronger opportunity is to package a repeatable solution for a specific merchant pain.&lt;/p&gt;

&lt;p&gt;A developer can monetize this in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one-time implementation fees&lt;/li&gt;
&lt;li&gt;monthly maintenance retainers&lt;/li&gt;
&lt;li&gt;hosted SaaS subscriptions&lt;/li&gt;
&lt;li&gt;per-merchant licensing&lt;/li&gt;
&lt;li&gt;agency packages&lt;/li&gt;
&lt;li&gt;paid boilerplates&lt;/li&gt;
&lt;li&gt;custom automation packages&lt;/li&gt;
&lt;li&gt;support and monitoring plans&lt;/li&gt;
&lt;li&gt;managed reconciliation services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is productization.&lt;/p&gt;

&lt;p&gt;Do not sell hours. Sell a clear outcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  The OxaPay primitives behind these product ideas
&lt;/h2&gt;

&lt;p&gt;Before looking at the 10 products, let’s map the infrastructure primitives.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;What it gives you&lt;/th&gt;
&lt;th&gt;Product use cases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;Generate Invoice&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creates a hosted payment session and returns a payment URL and payment reference&lt;/td&gt;
&lt;td&gt;checkout, SaaS billing, digital goods, Telegram access, invoices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;Generate White Label&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Returns payment details such as address, amount, currency, network, QR code, and expiry so you can build your own UI&lt;/td&gt;
&lt;td&gt;vertical checkout, branded checkout, embedded SaaS payment screens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;Generate Static Address&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creates a reusable address linked to a &lt;code&gt;track_id&lt;/code&gt;; callbacks can notify your server when payments arrive&lt;/td&gt;
&lt;td&gt;deposits, account top-ups, customer wallets, repeated payments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;Payment Information&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Retrieves a specific payment by &lt;code&gt;track_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;support lookup, manual investigation, retry recovery, status pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;Payment History&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Lists payments with filters and pagination&lt;/td&gt;
&lt;td&gt;reconciliation, reporting, dashboards, backfill jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-status-table" rel="noopener noreferrer"&gt;Payment Status Table&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Defines payment lifecycle states such as &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;paying&lt;/code&gt;, &lt;code&gt;paid&lt;/code&gt;, &lt;code&gt;underpaid&lt;/code&gt;, and &lt;code&gt;expired&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;internal state machines, support tools, fulfillment rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;Webhook&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sends payment or payout updates to your &lt;code&gt;callback_url&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;event-driven fulfillment, automation, alerts, status sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/generate-payout" rel="noopener noreferrer"&gt;Generate Payout&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creates a cryptocurrency payout request to a specified address&lt;/td&gt;
&lt;td&gt;marketplaces, affiliates, contractor payouts, revenue split systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-history" rel="noopener noreferrer"&gt;Payout History&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Lists payout records with filters and pagination&lt;/td&gt;
&lt;td&gt;payout reporting, audit, reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-status-table" rel="noopener noreferrer"&gt;Payout Status Table&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Defines payout lifecycle states such as processing, pending, confirming, and confirmed&lt;/td&gt;
&lt;td&gt;payout queues, approval workflows, payout dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt;, &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;PHP SDK&lt;/a&gt;, &lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;Laravel SDK&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;SDK methods for payments, payouts, swaps, account data, and webhook handling&lt;/td&gt;
&lt;td&gt;faster implementation in common backend stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation/telegram-bot" rel="noopener noreferrer"&gt;Make automation&lt;/a&gt;, n8n automation, plugins&lt;/td&gt;
&lt;td&gt;Low-code and platform integrations&lt;/td&gt;
&lt;td&gt;automation studios, agency launch kits, quick merchant deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those primitives are enough to build more than a checkout page.&lt;/p&gt;

&lt;p&gt;They are enough to build merchant payment products.&lt;/p&gt;




&lt;h2&gt;
  
  
  The common architecture
&lt;/h2&gt;

&lt;p&gt;Most of the product ideas in this article share a similar event-driven structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merchant app / store / bot / SaaS
        |
        | create payment request
        v
OxaPay invoice / white-label payment / static address
        |
        | customer pays
        v
Webhook callback to your backend
        |
        | validate HMAC, store event, update state
        v
Business action
        |
        | activate order / grant access / create report / queue payout
        v
Dashboard, support console, finance export, notifications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The strongest systems should not rely on webhooks alone.&lt;/p&gt;

&lt;p&gt;A production-ready architecture usually needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhook ingestion&lt;/li&gt;
&lt;li&gt;signature validation&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;event logging&lt;/li&gt;
&lt;li&gt;internal payment state machine&lt;/li&gt;
&lt;li&gt;periodic backfill using payment history&lt;/li&gt;
&lt;li&gt;manual investigation tools&lt;/li&gt;
&lt;li&gt;support visibility&lt;/li&gt;
&lt;li&gt;alerting&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;li&gt;secure API key handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A basic implementation can be small. A serious merchant-facing product needs operational discipline.&lt;/p&gt;




&lt;h2&gt;
  
  
  A minimal invoice example
&lt;/h2&gt;

&lt;p&gt;Here is a simplified Node.js example showing how a merchant-facing product might create a payment session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/payments/create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customerEmail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing orderId or amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.oxapay.com/v1/payment/invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customerEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callback_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/payment`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Payment for order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Store the local order, OxaPay track_id, payment_url, amount, and status.&lt;/span&gt;
  &lt;span class="c1"&gt;// The exact response shape should be confirmed against the current docs.&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation details depend on your stack and the exact response shape returned by the API, but the product pattern is consistent: create a payment object, store the reference, and wait for verified status updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  A minimal webhook receiver
&lt;/h2&gt;

&lt;p&gt;OxaPay’s SDK documentation notes that webhook validation uses an HMAC header calculated with SHA-512 over the raw request body. Always verify the exact header name and payload behavior against the current documentation and SDK for the stack you use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/oxapay/payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HMAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expectedHmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OXAPAY_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;receivedHmac&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expectedHmac&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid webhook signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Store raw event.&lt;/span&gt;
    &lt;span class="c1"&gt;// 2. Check idempotency.&lt;/span&gt;
    &lt;span class="c1"&gt;// 3. Update internal payment state.&lt;/span&gt;
    &lt;span class="c1"&gt;// 4. Trigger business action only once.&lt;/span&gt;
    &lt;span class="c1"&gt;// 5. Return 200 quickly.&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the code itself.&lt;/p&gt;

&lt;p&gt;The important part is the discipline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validate the webhook&lt;/li&gt;
&lt;li&gt;store the raw event&lt;/li&gt;
&lt;li&gt;make processing idempotent&lt;/li&gt;
&lt;li&gt;do not perform irreversible fulfillment twice&lt;/li&gt;
&lt;li&gt;have a backfill job in case callbacks are delayed, missed, or not processed&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The 10 product ideas
&lt;/h2&gt;

&lt;p&gt;Now let’s look at the 10 developer business ideas.&lt;/p&gt;

&lt;p&gt;Each idea is not just “connect OxaPay.”&lt;/p&gt;

&lt;p&gt;Each idea is a product or productized service a developer could sell to merchants.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Crypto PaymentOps Service for Merchants
&lt;/h2&gt;

&lt;p&gt;A Crypto PaymentOps service helps merchants manage the operational layer around crypto payments.&lt;/p&gt;

&lt;p&gt;It is not just checkout.&lt;/p&gt;

&lt;p&gt;It includes invoice creation, payment status tracking, webhook handling, merchant alerts, payment reports, support tools, and reconciliation basics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who would pay for this?
&lt;/h3&gt;

&lt;p&gt;Good target customers include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;digital product stores&lt;/li&gt;
&lt;li&gt;SaaS apps&lt;/li&gt;
&lt;li&gt;software license sellers&lt;/li&gt;
&lt;li&gt;hosting providers&lt;/li&gt;
&lt;li&gt;VPN or proxy resellers&lt;/li&gt;
&lt;li&gt;online course sellers&lt;/li&gt;
&lt;li&gt;international service providers&lt;/li&gt;
&lt;li&gt;small businesses accepting crypto manually today&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These merchants often do not want to build a full payment operations backend. They want orders to update correctly, customers to receive products, and support staff to know what happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;A minimal PaymentOps product can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice creation endpoint&lt;/li&gt;
&lt;li&gt;local payment table&lt;/li&gt;
&lt;li&gt;webhook receiver&lt;/li&gt;
&lt;li&gt;payment status mapping&lt;/li&gt;
&lt;li&gt;merchant dashboard&lt;/li&gt;
&lt;li&gt;unresolved payment queue&lt;/li&gt;
&lt;li&gt;email or Telegram alerts&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;li&gt;daily payment report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A production version can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;role-based access&lt;/li&gt;
&lt;li&gt;multi-merchant support&lt;/li&gt;
&lt;li&gt;automated backfill&lt;/li&gt;
&lt;li&gt;support notes&lt;/li&gt;
&lt;li&gt;reconciliation rules&lt;/li&gt;
&lt;li&gt;failed webhook retry handling&lt;/li&gt;
&lt;li&gt;payment anomaly detection&lt;/li&gt;
&lt;li&gt;payout visibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;li&gt;Payment Information&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;Payment Status Table&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;SDKs&lt;/li&gt;
&lt;li&gt;optionally Payment Statistics&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;This works well as a service package:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;What it includes&lt;/th&gt;
&lt;th&gt;Revenue style&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;Payment flow, webhook, dashboard&lt;/td&gt;
&lt;td&gt;one-time fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed ops&lt;/td&gt;
&lt;td&gt;Monitoring, reports, small fixes&lt;/td&gt;
&lt;td&gt;monthly retainer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advanced ops&lt;/td&gt;
&lt;td&gt;reconciliation, alerts, support views&lt;/td&gt;
&lt;td&gt;higher monthly fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom ops&lt;/td&gt;
&lt;td&gt;niche-specific workflows&lt;/td&gt;
&lt;td&gt;custom pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is often the best first product to build because it is broad enough to sell, but concrete enough to explain.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Vertical Crypto Checkout for a Specific Niche
&lt;/h2&gt;

&lt;p&gt;Generic checkout is hard to sell.&lt;/p&gt;

&lt;p&gt;Vertical checkout is easier.&lt;/p&gt;

&lt;p&gt;A vertical checkout solves the payment flow for a specific niche such as hosting, SaaS, courses, gaming communities, digital downloads, or VPN resellers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why niche matters
&lt;/h3&gt;

&lt;p&gt;A hosting company does not only need a payment page. It needs service provisioning.&lt;/p&gt;

&lt;p&gt;A course seller does not only need a payment page. They need enrollment and access control.&lt;/p&gt;

&lt;p&gt;A gaming community does not only need a payment page. It needs roles, credits, or inventory updates.&lt;/p&gt;

&lt;p&gt;The payment primitive is the same. The business workflow is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;For a hosting niche, the flow might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer selects hosting plan
  -&amp;gt; checkout creates crypto invoice
  -&amp;gt; webhook confirms payment
  -&amp;gt; hosting account is provisioned
  -&amp;gt; invoice and server details are emailed
  -&amp;gt; renewal reminder is scheduled
  -&amp;gt; expired invoices are cancelled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a course niche:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer selects course
  -&amp;gt; invoice is created
  -&amp;gt; payment is confirmed
  -&amp;gt; student account is created
  -&amp;gt; course access is unlocked
  -&amp;gt; receipt and onboarding email are sent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate Invoice for hosted payment pages&lt;/li&gt;
&lt;li&gt;Generate White Label for branded checkout&lt;/li&gt;
&lt;li&gt;Webhook for confirmation&lt;/li&gt;
&lt;li&gt;Payment Information for support lookup&lt;/li&gt;
&lt;li&gt;Payment History for reporting&lt;/li&gt;
&lt;li&gt;plugins or SDKs depending on the merchant stack&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;A vertical checkout can be sold as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;implementation package&lt;/li&gt;
&lt;li&gt;monthly license&lt;/li&gt;
&lt;li&gt;white-label agency product&lt;/li&gt;
&lt;li&gt;hosted checkout SaaS&lt;/li&gt;
&lt;li&gt;niche plugin&lt;/li&gt;
&lt;li&gt;maintenance plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is to speak the merchant’s language.&lt;/p&gt;

&lt;p&gt;Do not sell “crypto invoice integration.”&lt;/p&gt;

&lt;p&gt;Sell “crypto checkout for hosting renewals,” “crypto checkout for digital downloads,” or “crypto checkout for course access.”&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Telegram Paid Access System with Crypto Payments
&lt;/h2&gt;

&lt;p&gt;A Telegram paid access system is more than a payment bot.&lt;/p&gt;

&lt;p&gt;The real product is access management.&lt;/p&gt;

&lt;p&gt;Many Telegram-based businesses sell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid channels&lt;/li&gt;
&lt;li&gt;private communities&lt;/li&gt;
&lt;li&gt;trading groups&lt;/li&gt;
&lt;li&gt;education groups&lt;/li&gt;
&lt;li&gt;digital files&lt;/li&gt;
&lt;li&gt;premium alerts&lt;/li&gt;
&lt;li&gt;creator memberships&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Their pain is not only collecting payment.&lt;/p&gt;

&lt;p&gt;Their pain is granting access, renewing access, removing expired members, handling failed payments, and answering support questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;A strong product flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User starts Telegram bot
  -&amp;gt; selects plan
  -&amp;gt; bot creates OxaPay invoice
  -&amp;gt; user pays
  -&amp;gt; webhook confirms payment
  -&amp;gt; bot creates or sends invite link
  -&amp;gt; membership record is created
  -&amp;gt; renewal reminder is scheduled
  -&amp;gt; expired users are removed or downgraded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;Payment Information&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;Make automation for Telegram workflows&lt;/li&gt;
&lt;li&gt;optionally Static Address for recurring deposit-style flows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Technical components
&lt;/h3&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegram bot&lt;/li&gt;
&lt;li&gt;payment session table&lt;/li&gt;
&lt;li&gt;membership table&lt;/li&gt;
&lt;li&gt;webhook receiver&lt;/li&gt;
&lt;li&gt;invite link generation&lt;/li&gt;
&lt;li&gt;expiry scheduler&lt;/li&gt;
&lt;li&gt;admin dashboard&lt;/li&gt;
&lt;li&gt;support lookup by Telegram user ID and &lt;code&gt;track_id&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;This can be sold as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup fee per channel&lt;/li&gt;
&lt;li&gt;monthly subscription per community&lt;/li&gt;
&lt;li&gt;percentage of sales for small creators&lt;/li&gt;
&lt;li&gt;white-label bot for agencies&lt;/li&gt;
&lt;li&gt;custom bot for high-value communities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a strong developer opportunity because many Telegram merchants are operationally informal. They need automation, not just a payment link.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Crypto Revenue Split and Payout System
&lt;/h2&gt;

&lt;p&gt;A revenue split system helps businesses distribute money after payments arrive.&lt;/p&gt;

&lt;p&gt;This is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;marketplaces&lt;/li&gt;
&lt;li&gt;affiliate programs&lt;/li&gt;
&lt;li&gt;creator platforms&lt;/li&gt;
&lt;li&gt;course platforms with multiple instructors&lt;/li&gt;
&lt;li&gt;agencies with contractors&lt;/li&gt;
&lt;li&gt;communities with partners&lt;/li&gt;
&lt;li&gt;reseller networks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;The architecture should separate payment intake from payout execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer payment
  -&amp;gt; payment confirmed
  -&amp;gt; revenue recorded in ledger
  -&amp;gt; split rules applied
  -&amp;gt; payable balances updated
  -&amp;gt; payout queue created
  -&amp;gt; admin approves payout
  -&amp;gt; payout request executed
  -&amp;gt; payout status tracked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key product is not merely “send payouts.”&lt;/p&gt;

&lt;p&gt;The key product is the ledger and approval workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;Generate Payout&lt;/li&gt;
&lt;li&gt;Payout Information&lt;/li&gt;
&lt;li&gt;Payout History&lt;/li&gt;
&lt;li&gt;Payout Status Table&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Important design rules
&lt;/h3&gt;

&lt;p&gt;A serious payout system needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;immutable ledger entries&lt;/li&gt;
&lt;li&gt;payout approval steps&lt;/li&gt;
&lt;li&gt;idempotency keys&lt;/li&gt;
&lt;li&gt;partner balance tracking&lt;/li&gt;
&lt;li&gt;payout limits&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;li&gt;failed payout handling&lt;/li&gt;
&lt;li&gt;manual review for suspicious requests&lt;/li&gt;
&lt;li&gt;clear tax and compliance boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;This product can command higher value because it touches money movement and business operations.&lt;/p&gt;

&lt;p&gt;Possible models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SaaS subscription&lt;/li&gt;
&lt;li&gt;custom implementation&lt;/li&gt;
&lt;li&gt;payout operations fee&lt;/li&gt;
&lt;li&gt;marketplace-specific module&lt;/li&gt;
&lt;li&gt;managed payout service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This idea is powerful, but it is also more sensitive than a simple checkout product. Developers should be careful with security, permissions, legal boundaries, and merchant responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Crypto Payment Reconciliation Tool
&lt;/h2&gt;

&lt;p&gt;Reconciliation is one of the most underrated crypto payment product opportunities.&lt;/p&gt;

&lt;p&gt;At low volume, a merchant can manually check payments.&lt;/p&gt;

&lt;p&gt;At higher volume, that breaks.&lt;/p&gt;

&lt;p&gt;They need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which payments match which orders&lt;/li&gt;
&lt;li&gt;which paid invoices were not fulfilled&lt;/li&gt;
&lt;li&gt;which orders are fulfilled but not paid&lt;/li&gt;
&lt;li&gt;which payments are underpaid&lt;/li&gt;
&lt;li&gt;which invoices expired&lt;/li&gt;
&lt;li&gt;which callbacks failed&lt;/li&gt;
&lt;li&gt;which static address deposits are unresolved&lt;/li&gt;
&lt;li&gt;which records finance should export&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;A reconciliation tool compares three sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Merchant orders
OxaPay payment records
Internal fulfillment records
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it creates a queue of exceptions.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid but not fulfilled&lt;/li&gt;
&lt;li&gt;fulfilled but not paid&lt;/li&gt;
&lt;li&gt;expired but customer claims paid&lt;/li&gt;
&lt;li&gt;underpaid and needs support review&lt;/li&gt;
&lt;li&gt;unknown payment&lt;/li&gt;
&lt;li&gt;duplicate callback&lt;/li&gt;
&lt;li&gt;status mismatch&lt;/li&gt;
&lt;li&gt;stale pending invoice&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Payment Information&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;Payment Status Table&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;Static Address&lt;/li&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Technical architecture
&lt;/h3&gt;

&lt;p&gt;A good reconciliation tool needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhook event log&lt;/li&gt;
&lt;li&gt;periodic payment history sync&lt;/li&gt;
&lt;li&gt;order import&lt;/li&gt;
&lt;li&gt;matching rules&lt;/li&gt;
&lt;li&gt;exception queue&lt;/li&gt;
&lt;li&gt;manual resolution notes&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;li&gt;audit trail&lt;/li&gt;
&lt;li&gt;support and finance views&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;This can be sold as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;monthly SaaS&lt;/li&gt;
&lt;li&gt;reporting add-on&lt;/li&gt;
&lt;li&gt;managed reconciliation service&lt;/li&gt;
&lt;li&gt;finance operations dashboard&lt;/li&gt;
&lt;li&gt;custom integration for merchants with higher volume&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the strongest opportunities in the series because it solves a pain that appears after merchants start getting real traction.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Payment Automation Studio for Crypto Merchants
&lt;/h2&gt;

&lt;p&gt;A Payment Automation Studio helps merchants connect crypto payment events to business actions.&lt;/p&gt;

&lt;p&gt;This can start as a collection of workflows. It can grow into a workflow product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example automations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;paid invoice -&amp;gt; send license key&lt;/li&gt;
&lt;li&gt;paid invoice -&amp;gt; activate SaaS plan&lt;/li&gt;
&lt;li&gt;paid invoice -&amp;gt; add user to Telegram or Discord&lt;/li&gt;
&lt;li&gt;paid invoice -&amp;gt; update Google Sheet&lt;/li&gt;
&lt;li&gt;paid invoice -&amp;gt; create CRM record&lt;/li&gt;
&lt;li&gt;expired invoice -&amp;gt; send reminder&lt;/li&gt;
&lt;li&gt;underpaid invoice -&amp;gt; create support ticket&lt;/li&gt;
&lt;li&gt;payout confirmed -&amp;gt; notify contractor&lt;/li&gt;
&lt;li&gt;static address deposit -&amp;gt; credit account balance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;li&gt;Static Address&lt;/li&gt;
&lt;li&gt;Payment Information&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;Generate Payout&lt;/li&gt;
&lt;li&gt;Make automation&lt;/li&gt;
&lt;li&gt;n8n workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OxaPay’s Make and n8n documentation is especially relevant here because it shows how payment events can be connected to external tools without forcing every merchant to run a custom backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;You can build this three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Low-code implementation service&lt;/strong&gt; using Make or n8n.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom workflow backend&lt;/strong&gt; with your own connectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid template + managed service&lt;/strong&gt; where merchants get prebuilt flows and you monitor them.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;Automation products can be sold as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup package&lt;/li&gt;
&lt;li&gt;workflow template pack&lt;/li&gt;
&lt;li&gt;monthly monitoring&lt;/li&gt;
&lt;li&gt;managed automation support&lt;/li&gt;
&lt;li&gt;custom connector development&lt;/li&gt;
&lt;li&gt;agency implementation package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The customer is not buying a webhook.&lt;/p&gt;

&lt;p&gt;They are buying fewer manual tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Merchant Crypto Launch Kit for Agencies
&lt;/h2&gt;

&lt;p&gt;This idea is aimed at developers who want to sell to agencies instead of individual merchants.&lt;/p&gt;

&lt;p&gt;Agencies already have clients. Many of them build websites, stores, communities, landing pages, funnels, SaaS MVPs, or membership products.&lt;/p&gt;

&lt;p&gt;A developer can package a repeatable crypto payment launch kit that agencies resell to their clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the kit includes
&lt;/h3&gt;

&lt;p&gt;A strong launch kit can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client discovery checklist&lt;/li&gt;
&lt;li&gt;integration decision tree&lt;/li&gt;
&lt;li&gt;checkout templates&lt;/li&gt;
&lt;li&gt;hosted invoice setup&lt;/li&gt;
&lt;li&gt;white-label checkout option&lt;/li&gt;
&lt;li&gt;static address option&lt;/li&gt;
&lt;li&gt;webhook receiver&lt;/li&gt;
&lt;li&gt;merchant dashboard&lt;/li&gt;
&lt;li&gt;support SOPs&lt;/li&gt;
&lt;li&gt;customer email templates&lt;/li&gt;
&lt;li&gt;payment status page&lt;/li&gt;
&lt;li&gt;QA checklist&lt;/li&gt;
&lt;li&gt;handoff documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;li&gt;Generate White Label&lt;/li&gt;
&lt;li&gt;Generate Static Address&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;SDKs&lt;/li&gt;
&lt;li&gt;plugins&lt;/li&gt;
&lt;li&gt;automation integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why agencies might buy it
&lt;/h3&gt;

&lt;p&gt;Agencies do not want to research every crypto payment detail from scratch for each client.&lt;/p&gt;

&lt;p&gt;They want something repeatable.&lt;/p&gt;

&lt;p&gt;They want a tested package they can sell with confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;You can monetize this as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agency license&lt;/li&gt;
&lt;li&gt;per-client implementation fee&lt;/li&gt;
&lt;li&gt;white-label support package&lt;/li&gt;
&lt;li&gt;training package&lt;/li&gt;
&lt;li&gt;custom integration support&lt;/li&gt;
&lt;li&gt;recurring maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a distribution strategy, not just a technical idea.&lt;/p&gt;

&lt;p&gt;Instead of finding every merchant yourself, you enable agencies that already have merchant relationships.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Crypto Payment Module for SaaS Apps
&lt;/h2&gt;

&lt;p&gt;Many SaaS founders want to accept crypto, but they do not want to build billing logic from scratch.&lt;/p&gt;

&lt;p&gt;A developer can build a reusable payment module for SaaS applications.&lt;/p&gt;

&lt;p&gt;This is not the same as a payment button.&lt;/p&gt;

&lt;p&gt;A SaaS payment module needs to manage plan activation, subscription state, grace periods, payment sessions, and admin visibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;A useful SaaS module can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create payment session&lt;/li&gt;
&lt;li&gt;map payment to user account&lt;/li&gt;
&lt;li&gt;activate plan after payment&lt;/li&gt;
&lt;li&gt;store subscription period&lt;/li&gt;
&lt;li&gt;handle expiry&lt;/li&gt;
&lt;li&gt;handle grace period&lt;/li&gt;
&lt;li&gt;show billing status to user&lt;/li&gt;
&lt;li&gt;show payment history to admin&lt;/li&gt;
&lt;li&gt;sync missed webhooks&lt;/li&gt;
&lt;li&gt;provide feature-gating middleware&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;li&gt;Generate White Label&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;Payment Information&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;SDKs&lt;/li&gt;
&lt;li&gt;optionally Static Address for account credit/top-up models&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Important limitation
&lt;/h3&gt;

&lt;p&gt;Do not present this as card-style automatic recurring billing unless your product actually implements a compliant recurring workflow around reminders, renewed invoices, access expiration, and user actions.&lt;/p&gt;

&lt;p&gt;Crypto payments are usually better modeled as invoice-based renewals, prepaid balances, or manual renewal flows unless a specific product design supports something more advanced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;This product can be sold as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid boilerplate&lt;/li&gt;
&lt;li&gt;Laravel package&lt;/li&gt;
&lt;li&gt;Node.js package&lt;/li&gt;
&lt;li&gt;Django app&lt;/li&gt;
&lt;li&gt;Next.js starter module&lt;/li&gt;
&lt;li&gt;open-source core plus paid support&lt;/li&gt;
&lt;li&gt;hosted billing add-on&lt;/li&gt;
&lt;li&gt;custom integration service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a strong opportunity for developers who already work with SaaS founders.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Crypto Payment Support Desk
&lt;/h2&gt;

&lt;p&gt;Payment support is a real operational problem.&lt;/p&gt;

&lt;p&gt;Customers often ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I paid. Why is my order still pending?&lt;/li&gt;
&lt;li&gt;I sent funds to the wrong network. What now?&lt;/li&gt;
&lt;li&gt;My invoice expired, but I already paid.&lt;/li&gt;
&lt;li&gt;Why does it say underpaid?&lt;/li&gt;
&lt;li&gt;Where is my access?&lt;/li&gt;
&lt;li&gt;Why did the payment not confirm?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Crypto Payment Support Desk helps support agents investigate these issues quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;The product can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search by order ID, email, &lt;code&gt;track_id&lt;/code&gt;, transaction hash, or static address&lt;/li&gt;
&lt;li&gt;payment timeline&lt;/li&gt;
&lt;li&gt;invoice status&lt;/li&gt;
&lt;li&gt;webhook event history&lt;/li&gt;
&lt;li&gt;support issue classification&lt;/li&gt;
&lt;li&gt;customer-facing payment status page&lt;/li&gt;
&lt;li&gt;response templates&lt;/li&gt;
&lt;li&gt;escalation notes&lt;/li&gt;
&lt;li&gt;unresolved payment queue&lt;/li&gt;
&lt;li&gt;agent permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Payment Information&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;Payment Status Table&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;Static Address&lt;/li&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why merchants pay
&lt;/h3&gt;

&lt;p&gt;Support time is expensive.&lt;/p&gt;

&lt;p&gt;A merchant may not pay for “API integration,” but they may pay to reduce payment-related tickets, shorten investigation time, and give agents a clear timeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;This can be sold as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;monthly SaaS&lt;/li&gt;
&lt;li&gt;per-agent pricing&lt;/li&gt;
&lt;li&gt;setup package&lt;/li&gt;
&lt;li&gt;support operations add-on&lt;/li&gt;
&lt;li&gt;bundled PaymentOps feature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the less obvious ideas, which makes it interesting. Many developers build checkout. Fewer build the support layer around checkout.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Cross-Border Crypto Payment Stack for Digital Sellers
&lt;/h2&gt;

&lt;p&gt;Digital sellers often sell across borders long before they have sophisticated payment infrastructure.&lt;/p&gt;

&lt;p&gt;They may sell software licenses, templates, courses, services, memberships, community access, reports, files, or SaaS plans to customers in many countries.&lt;/p&gt;

&lt;p&gt;Their problem is not only payment acceptance.&lt;/p&gt;

&lt;p&gt;Their problem is the full stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checkout&lt;/li&gt;
&lt;li&gt;customer instructions&lt;/li&gt;
&lt;li&gt;payment tracking&lt;/li&gt;
&lt;li&gt;fulfillment&lt;/li&gt;
&lt;li&gt;support&lt;/li&gt;
&lt;li&gt;reconciliation&lt;/li&gt;
&lt;li&gt;reporting&lt;/li&gt;
&lt;li&gt;optional payout&lt;/li&gt;
&lt;li&gt;operational documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you build
&lt;/h3&gt;

&lt;p&gt;A cross-border crypto payment stack can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stablecoin-first checkout flow&lt;/li&gt;
&lt;li&gt;hosted invoice or branded payment UI&lt;/li&gt;
&lt;li&gt;clear payment instructions&lt;/li&gt;
&lt;li&gt;webhook-based fulfillment&lt;/li&gt;
&lt;li&gt;customer payment status page&lt;/li&gt;
&lt;li&gt;support console&lt;/li&gt;
&lt;li&gt;reconciliation exports&lt;/li&gt;
&lt;li&gt;payment history backfill&lt;/li&gt;
&lt;li&gt;optional payout workflow&lt;/li&gt;
&lt;li&gt;merchant onboarding docs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OxaPay features used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generate Invoice&lt;/li&gt;
&lt;li&gt;Generate White Label&lt;/li&gt;
&lt;li&gt;Static Address&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;li&gt;Payment Information&lt;/li&gt;
&lt;li&gt;Payment History&lt;/li&gt;
&lt;li&gt;Accepted Currencies&lt;/li&gt;
&lt;li&gt;Payout API&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Who would pay
&lt;/h3&gt;

&lt;p&gt;Potential customers include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;software license sellers&lt;/li&gt;
&lt;li&gt;digital product stores&lt;/li&gt;
&lt;li&gt;education platforms&lt;/li&gt;
&lt;li&gt;remote service providers&lt;/li&gt;
&lt;li&gt;indie SaaS builders&lt;/li&gt;
&lt;li&gt;agencies selling global services&lt;/li&gt;
&lt;li&gt;creator businesses with international audiences&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Revenue model
&lt;/h3&gt;

&lt;p&gt;This can be sold as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;premium setup package&lt;/li&gt;
&lt;li&gt;monthly operations plan&lt;/li&gt;
&lt;li&gt;support and reporting add-on&lt;/li&gt;
&lt;li&gt;custom integration&lt;/li&gt;
&lt;li&gt;niche-specific launch kit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This idea should be handled carefully. Developers should avoid making broad claims about legal availability, compliance, tax treatment, or guaranteed settlement outcomes. The product should focus on technical infrastructure and merchant operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to choose which product to build first
&lt;/h2&gt;

&lt;p&gt;Not every idea is equally good for every developer.&lt;/p&gt;

&lt;p&gt;Here is a practical selection matrix.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product idea&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Difficulty&lt;/th&gt;
&lt;th&gt;Sales cycle&lt;/th&gt;
&lt;th&gt;Recurring revenue potential&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PaymentOps Service&lt;/td&gt;
&lt;td&gt;freelancers, small agencies&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;short to medium&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical Checkout&lt;/td&gt;
&lt;td&gt;niche builders&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram Paid Access&lt;/td&gt;
&lt;td&gt;bot developers&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;short&lt;/td&gt;
&lt;td&gt;medium to strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue Split + Payout&lt;/td&gt;
&lt;td&gt;marketplace developers&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;medium to long&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconciliation Tool&lt;/td&gt;
&lt;td&gt;ops/finance-focused builders&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation Studio&lt;/td&gt;
&lt;td&gt;low-code and backend developers&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;short to medium&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency Launch Kit&lt;/td&gt;
&lt;td&gt;developers with agency network&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS Payment Module&lt;/td&gt;
&lt;td&gt;framework/package developers&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment Support Desk&lt;/td&gt;
&lt;td&gt;support/ops SaaS builders&lt;/td&gt;
&lt;td&gt;medium to high&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-Border Stack&lt;/td&gt;
&lt;td&gt;implementation agencies&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are starting from zero, the best first products are usually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PaymentOps Service&lt;/li&gt;
&lt;li&gt;Payment Automation Studio&lt;/li&gt;
&lt;li&gt;Telegram Paid Access System&lt;/li&gt;
&lt;li&gt;Vertical Crypto Checkout&lt;/li&gt;
&lt;li&gt;SaaS Payment Module&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you already have experience with marketplaces, finance tools, or operational dashboards, then reconciliation, revenue split, payout systems, and support desks become more attractive.&lt;/p&gt;




&lt;h2&gt;
  
  
  MVP scope: what to build first
&lt;/h2&gt;

&lt;p&gt;A mistake developers often make is trying to build the full platform immediately.&lt;/p&gt;

&lt;p&gt;Start with the smallest valuable workflow.&lt;/p&gt;

&lt;p&gt;A good MVP should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one merchant&lt;/li&gt;
&lt;li&gt;one payment creation flow&lt;/li&gt;
&lt;li&gt;one webhook receiver&lt;/li&gt;
&lt;li&gt;one internal payment state table&lt;/li&gt;
&lt;li&gt;one dashboard view&lt;/li&gt;
&lt;li&gt;one support lookup page&lt;/li&gt;
&lt;li&gt;one export or notification&lt;/li&gt;
&lt;li&gt;one clearly defined business action after payment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not start with multi-tenant SaaS, role-based permissions, advanced analytics, payout automation, agency portals, and workflow builders all at once.&lt;/p&gt;

&lt;p&gt;Pick one merchant pain.&lt;/p&gt;

&lt;p&gt;Solve it well.&lt;/p&gt;

&lt;p&gt;Then package it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production requirements developers should not ignore
&lt;/h2&gt;

&lt;p&gt;Payment products require more discipline than normal CRUD apps.&lt;/p&gt;

&lt;p&gt;At minimum, think about the following.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Idempotency
&lt;/h3&gt;

&lt;p&gt;Webhooks can be retried or delivered more than once. Your business action must not run twice.&lt;/p&gt;

&lt;p&gt;Do not deliver the same digital product twice unless it is safe.&lt;/p&gt;

&lt;p&gt;Do not grant duplicate membership periods by accident.&lt;/p&gt;

&lt;p&gt;Do not create duplicate payout requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Raw event logging
&lt;/h3&gt;

&lt;p&gt;Store the raw webhook payload before processing it.&lt;/p&gt;

&lt;p&gt;This gives you a forensic trail when something goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Signature validation
&lt;/h3&gt;

&lt;p&gt;Validate webhook signatures using the provider’s documented method. OxaPay SDK documentation notes HMAC validation over the raw request body. Do not validate against a parsed and re-serialized JSON object.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. State mapping
&lt;/h3&gt;

&lt;p&gt;Do not expose raw provider states directly as your internal business states.&lt;/p&gt;

&lt;p&gt;Create your own state machine.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider status: paid
internal payment status: confirmed
order status: ready_to_fulfill
fulfillment status: pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are different layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Backfill jobs
&lt;/h3&gt;

&lt;p&gt;Do not rely only on webhooks.&lt;/p&gt;

&lt;p&gt;Use payment history or payment information endpoints to reconcile missed or delayed events.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Secrets management
&lt;/h3&gt;

&lt;p&gt;Do not store API keys in frontend code.&lt;/p&gt;

&lt;p&gt;Use environment variables, secret managers, scoped credentials, and least-privilege access.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Manual review
&lt;/h3&gt;

&lt;p&gt;Not every case should be fully automated.&lt;/p&gt;

&lt;p&gt;Underpaid payments, suspicious mismatches, unknown deposits, payout failures, and customer disputes should often go to a review queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Compliance boundaries
&lt;/h3&gt;

&lt;p&gt;If your product touches payouts, revenue sharing, cross-border selling, or customer funds, be explicit about merchant responsibility, legal boundaries, tax exports, and operational limits.&lt;/p&gt;

&lt;p&gt;Developers should avoid presenting themselves as banks, custodians, regulated financial institutions, or legal advisors unless they are actually operating within the required framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revenue expectations: how to talk about money responsibly
&lt;/h2&gt;

&lt;p&gt;It is tempting to say developers can make a specific amount per month.&lt;/p&gt;

&lt;p&gt;That would be misleading.&lt;/p&gt;

&lt;p&gt;Revenue depends on positioning, niche, distribution, trust, technical quality, support quality, and merchant volume.&lt;/p&gt;

&lt;p&gt;A more responsible way to think about revenue is by product type.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Typical monetization logic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple integration&lt;/td&gt;
&lt;td&gt;one-time setup fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PaymentOps service&lt;/td&gt;
&lt;td&gt;setup fee + monthly retainer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation studio&lt;/td&gt;
&lt;td&gt;workflow setup + monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical checkout&lt;/td&gt;
&lt;td&gt;license + implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS module&lt;/td&gt;
&lt;td&gt;paid package, support, hosted add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconciliation tool&lt;/td&gt;
&lt;td&gt;monthly SaaS or managed service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payout system&lt;/td&gt;
&lt;td&gt;custom implementation + operations fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency launch kit&lt;/td&gt;
&lt;td&gt;agency license + per-client support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support desk&lt;/td&gt;
&lt;td&gt;per-agent or per-merchant SaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-border stack&lt;/td&gt;
&lt;td&gt;premium setup + monthly ops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Freelance market data shows that merchants already pay for payment integration and developer services, but the strongest pricing comes from specialization. A generic integration is easy to compare. A niche operational product is harder to replace.&lt;/p&gt;

&lt;p&gt;That is why the best product question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I integrate crypto payments?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which merchant operation can I own better than a generic integration developer?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Content and distribution strategy for developers
&lt;/h2&gt;

&lt;p&gt;Building the product is only half the work.&lt;/p&gt;

&lt;p&gt;You also need a way to get merchants.&lt;/p&gt;

&lt;p&gt;Here are practical distribution angles.&lt;/p&gt;

&lt;h3&gt;
  
  
  For freelancers
&lt;/h3&gt;

&lt;p&gt;Create service pages like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crypto payment setup for WooCommerce stores&lt;/li&gt;
&lt;li&gt;OxaPay webhook automation for digital products&lt;/li&gt;
&lt;li&gt;Telegram paid access bot setup&lt;/li&gt;
&lt;li&gt;Crypto payment reconciliation dashboard&lt;/li&gt;
&lt;li&gt;Crypto checkout for SaaS plans&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For SaaS builders
&lt;/h3&gt;

&lt;p&gt;Build narrow landing pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crypto billing module for Laravel SaaS&lt;/li&gt;
&lt;li&gt;Crypto payment support desk for digital sellers&lt;/li&gt;
&lt;li&gt;Crypto reconciliation for merchants&lt;/li&gt;
&lt;li&gt;Telegram paid community billing tool&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For agencies
&lt;/h3&gt;

&lt;p&gt;Sell enablement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;white-label crypto payment launch kit&lt;/li&gt;
&lt;li&gt;agency-ready checkout templates&lt;/li&gt;
&lt;li&gt;support SOPs&lt;/li&gt;
&lt;li&gt;webhook integration package&lt;/li&gt;
&lt;li&gt;merchant onboarding documents&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For open-source developers
&lt;/h3&gt;

&lt;p&gt;Use open source as a trust layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;release a starter package&lt;/li&gt;
&lt;li&gt;provide a demo app&lt;/li&gt;
&lt;li&gt;publish webhook examples&lt;/li&gt;
&lt;li&gt;document security assumptions&lt;/li&gt;
&lt;li&gt;offer paid implementation or hosting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product does not sell itself because it uses crypto.&lt;/p&gt;

&lt;p&gt;It sells when it removes a concrete merchant headache.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical build roadmap
&lt;/h2&gt;

&lt;p&gt;Here is a realistic way to approach the whole opportunity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 1: Pick one niche and one workflow
&lt;/h3&gt;

&lt;p&gt;Do not pick “all merchants.”&lt;/p&gt;

&lt;p&gt;Pick something like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegram educators&lt;/li&gt;
&lt;li&gt;WooCommerce digital product sellers&lt;/li&gt;
&lt;li&gt;SaaS founders using Laravel&lt;/li&gt;
&lt;li&gt;Discord communities&lt;/li&gt;
&lt;li&gt;hosting resellers&lt;/li&gt;
&lt;li&gt;software license sellers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then pick one workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paid invoice -&amp;gt; unlock access&lt;/li&gt;
&lt;li&gt;paid invoice -&amp;gt; deliver license&lt;/li&gt;
&lt;li&gt;paid invoice -&amp;gt; mark order paid&lt;/li&gt;
&lt;li&gt;payment history -&amp;gt; reconciliation report&lt;/li&gt;
&lt;li&gt;static address deposit -&amp;gt; credit balance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 2: Build the technical core
&lt;/h3&gt;

&lt;p&gt;Implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;payment creation&lt;/li&gt;
&lt;li&gt;webhook validation&lt;/li&gt;
&lt;li&gt;event store&lt;/li&gt;
&lt;li&gt;internal state machine&lt;/li&gt;
&lt;li&gt;business action&lt;/li&gt;
&lt;li&gt;admin view&lt;/li&gt;
&lt;li&gt;support lookup&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 3: Add reliability
&lt;/h3&gt;

&lt;p&gt;Add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;backfill job&lt;/li&gt;
&lt;li&gt;manual review queue&lt;/li&gt;
&lt;li&gt;alerts&lt;/li&gt;
&lt;li&gt;export&lt;/li&gt;
&lt;li&gt;basic audit logs&lt;/li&gt;
&lt;li&gt;onboarding docs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Week 4: Package and sell
&lt;/h3&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;landing page&lt;/li&gt;
&lt;li&gt;demo video&lt;/li&gt;
&lt;li&gt;pricing page&lt;/li&gt;
&lt;li&gt;merchant onboarding checklist&lt;/li&gt;
&lt;li&gt;support SOP&lt;/li&gt;
&lt;li&gt;terms of responsibility&lt;/li&gt;
&lt;li&gt;case-study style demo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to build everything.&lt;/p&gt;

&lt;p&gt;The goal is to prove that one merchant pain can be solved repeatedly.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to use this article as a series map
&lt;/h2&gt;

&lt;p&gt;This pillar article introduces the full opportunity landscape.&lt;/p&gt;

&lt;p&gt;Each idea deserves a deeper technical article:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a Crypto PaymentOps Service for Merchants&lt;/li&gt;
&lt;li&gt;Build a Vertical Crypto Checkout for a Specific Niche&lt;/li&gt;
&lt;li&gt;Build a Telegram Paid Access System with Crypto Payments&lt;/li&gt;
&lt;li&gt;Build a Crypto Revenue Split and Payout System&lt;/li&gt;
&lt;li&gt;Build a Crypto Payment Reconciliation Tool&lt;/li&gt;
&lt;li&gt;Build a Payment Automation Studio for Crypto Merchants&lt;/li&gt;
&lt;li&gt;Build a Merchant Crypto Launch Kit for Agencies&lt;/li&gt;
&lt;li&gt;Build a Crypto Payment Module for SaaS Apps&lt;/li&gt;
&lt;li&gt;Build a Crypto Payment Support Desk&lt;/li&gt;
&lt;li&gt;Build a Cross-Border Crypto Payment Stack for Digital Sellers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each deep dive should answer the same questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What merchant problem does this solve?&lt;/li&gt;
&lt;li&gt;Who would pay for it?&lt;/li&gt;
&lt;li&gt;Which OxaPay primitives are used?&lt;/li&gt;
&lt;li&gt;What is the technical architecture?&lt;/li&gt;
&lt;li&gt;What is the MVP?&lt;/li&gt;
&lt;li&gt;What does production require?&lt;/li&gt;
&lt;li&gt;How can a developer monetize it?&lt;/li&gt;
&lt;li&gt;What risks should not be ignored?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That structure keeps the series practical instead of becoming a list of vague business ideas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final developer takeaway
&lt;/h2&gt;

&lt;p&gt;The opportunity is not “developers can integrate crypto payments.”&lt;/p&gt;

&lt;p&gt;That is too small.&lt;/p&gt;

&lt;p&gt;The real opportunity is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Developers can build merchant-facing products on top of crypto payment infrastructure: checkout systems, automation studios, reconciliation tools, paid access systems, payout workflows, support desks, SaaS billing modules, agency launch kits, and cross-border payment stacks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OxaPay is useful as an example because it provides the payment primitives developers need: invoices, white-label payment details, static addresses, webhooks, payment information, payment history, payout APIs, SDKs, plugins, and automation integrations.&lt;/p&gt;

&lt;p&gt;But the business value does not come from calling an API.&lt;/p&gt;

&lt;p&gt;The business value comes from owning a painful merchant workflow and turning it into a repeatable product.&lt;/p&gt;

&lt;p&gt;Start with one niche.&lt;/p&gt;

&lt;p&gt;Solve one payment operation deeply.&lt;/p&gt;

&lt;p&gt;Then turn it into a product merchants understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/" rel="noopener noreferrer"&gt;OxaPay Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;OxaPay Generate Invoice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-white-label" rel="noopener noreferrer"&gt;OxaPay Generate White Label&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/generate-static-address" rel="noopener noreferrer"&gt;OxaPay Generate Static Address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;OxaPay Payment Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-history" rel="noopener noreferrer"&gt;OxaPay Payment History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payment/payment-status-table" rel="noopener noreferrer"&gt;OxaPay Payment Status Table&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;OxaPay Webhook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/generate-payout" rel="noopener noreferrer"&gt;OxaPay Generate Payout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-history" rel="noopener noreferrer"&gt;OxaPay Payout History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/api-reference/payout/payout-status-table" rel="noopener noreferrer"&gt;OxaPay Payout Status Table&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk" rel="noopener noreferrer"&gt;OxaPay Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk" rel="noopener noreferrer"&gt;OxaPay PHP SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk" rel="noopener noreferrer"&gt;OxaPay Laravel SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation/telegram-bot" rel="noopener noreferrer"&gt;OxaPay Telegram Automation with Make&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation/shopify" rel="noopener noreferrer"&gt;OxaPay Shopify Automation with Make&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.upwork.com/freelance-jobs/payment-gateway-integration/" rel="noopener noreferrer"&gt;Upwork Payment Gateway Integration Jobs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.upwork.com/hire/software-developers/cost/" rel="noopener noreferrer"&gt;Upwork Software Developer Cost Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.codeable.io/blog/woocommerce-development-cost/" rel="noopener noreferrer"&gt;Codeable WooCommerce Development Cost Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>crypto</category>
      <category>saas</category>
    </item>
    <item>
      <title>Crypto Payment Reconciliation: The Layer Most Developers Forget</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Thu, 09 Jul 2026 11:28:29 +0000</pubDate>
      <link>https://dev.to/kevins1988/crypto-payment-reconciliation-the-layer-most-developers-forget-gk4</link>
      <guid>https://dev.to/kevins1988/crypto-payment-reconciliation-the-layer-most-developers-forget-gk4</guid>
      <description>&lt;h1&gt;
  
  
  Crypto Payment Reconciliation: The Layer Most Developers Forget
&lt;/h1&gt;

&lt;p&gt;Most developers treat crypto payment integration as a webhook problem.&lt;/p&gt;

&lt;p&gt;Create an invoice.&lt;br&gt;&lt;br&gt;
Wait for the callback.&lt;br&gt;&lt;br&gt;
Update the order.&lt;br&gt;&lt;br&gt;
Move on.&lt;/p&gt;

&lt;p&gt;That works until the first real production mismatch appears.&lt;/p&gt;

&lt;p&gt;The gateway says an invoice is paid, but your database still says pending.&lt;br&gt;&lt;br&gt;
The customer has a transaction hash, but the order was never updated.&lt;br&gt;&lt;br&gt;
A webhook arrived, but your server returned &lt;code&gt;500&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
A payment was completed after the invoice expired.&lt;br&gt;&lt;br&gt;
Support sees an unpaid order, finance sees received funds, and engineering has to open logs to understand what happened.&lt;/p&gt;

&lt;p&gt;That is the moment you discover the missing layer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Webhooks tell your system what changed. Reconciliation proves your system still agrees with reality.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Crypto payment reconciliation is the process of comparing your local payment state with external payment truth: the gateway, the blockchain, settlement records, order records, and ledger entries.&lt;/p&gt;

&lt;p&gt;It is not optional.&lt;/p&gt;

&lt;p&gt;It is what prevents small asynchronous failures from becoming lost revenue, incorrect fulfillment, duplicate credits, support chaos, and accounting gaps.&lt;/p&gt;

&lt;p&gt;This article explains how to design crypto payment reconciliation from a developer’s perspective: state drift, source-of-truth boundaries, webhook failure, invoice lookup, transaction matching, audit trails, anomaly detection, repair jobs, settlement reconciliation, and the operational dashboards that make crypto payment systems trustworthy in production.&lt;/p&gt;

&lt;p&gt;I’ll use OxaPay as one implementation reference where useful because its API exposes common reconciliation primitives such as invoice generation, &lt;code&gt;track_id&lt;/code&gt;, signed webhooks, payment statuses, and payment lookup by &lt;code&gt;track_id&lt;/code&gt;. The architecture itself applies to most crypto payment gateways.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;A webhook-driven payment system asks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did we receive the event?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reconciled payment system asks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Does every system agree on what happened?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a much stronger question.&lt;/p&gt;

&lt;p&gt;A reliable crypto payment system must compare at least these records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local order
local invoice
local payment events
gateway payment status
blockchain transaction data
merchant balance / settlement record
business fulfillment state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those records disagree, the system needs to detect the mismatch, repair it safely when possible, or send it to manual review with enough context for support.&lt;/p&gt;

&lt;p&gt;That is reconciliation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Reconciliation Exists
&lt;/h2&gt;

&lt;p&gt;Crypto payments are asynchronous.&lt;/p&gt;

&lt;p&gt;The user pushes funds from a wallet or exchange.&lt;br&gt;&lt;br&gt;
The gateway monitors the blockchain.&lt;br&gt;&lt;br&gt;
The gateway sends callbacks to your server.&lt;br&gt;&lt;br&gt;
Your server updates local state.&lt;br&gt;&lt;br&gt;
Your order system triggers fulfillment.&lt;br&gt;&lt;br&gt;
Your finance system records revenue.&lt;br&gt;&lt;br&gt;
Your settlement system moves funds later.&lt;/p&gt;

&lt;p&gt;That flow contains many boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wallet → blockchain
blockchain → gateway
gateway → webhook
webhook → database
database → order system
order system → fulfillment
payment balance → accounting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every boundary can fail.&lt;/p&gt;

&lt;p&gt;A webhook may be delayed.&lt;br&gt;&lt;br&gt;
Your server may be down.&lt;br&gt;&lt;br&gt;
A queue worker may crash.&lt;br&gt;&lt;br&gt;
A database transaction may roll back.&lt;br&gt;&lt;br&gt;
A customer may pay after expiration.&lt;br&gt;&lt;br&gt;
A duplicate event may be ignored incorrectly.&lt;br&gt;&lt;br&gt;
A provider status may update after your local status freezes.&lt;br&gt;&lt;br&gt;
A paid invoice may not trigger fulfillment.&lt;br&gt;&lt;br&gt;
A fulfilled order may not have a final payment record.&lt;/p&gt;

&lt;p&gt;Reconciliation exists because no distributed payment system should assume that one event path is perfect.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Most Common Production Mismatch
&lt;/h2&gt;

&lt;p&gt;The most common mismatch looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gateway: invoice paid
Merchant DB: payment pending
WooCommerce / Shopify / app order: unpaid
Customer: already sent funds
Support: confused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can happen even when every system is “working.”&lt;/p&gt;

&lt;p&gt;Maybe the webhook was sent, but your endpoint timed out.&lt;/p&gt;

&lt;p&gt;Maybe your endpoint received the webhook, but a later database write failed.&lt;/p&gt;

&lt;p&gt;Maybe your code updated the payment table but did not update the order table.&lt;/p&gt;

&lt;p&gt;Maybe the provider retried the callback, but your idempotency logic incorrectly treated it as already processed.&lt;/p&gt;

&lt;p&gt;Maybe the customer paid after expiration and your system ignored the event.&lt;/p&gt;

&lt;p&gt;Without reconciliation, this mismatch stays hidden until the customer complains.&lt;/p&gt;

&lt;p&gt;With reconciliation, the system finds it automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciliation Is Not Polling Instead of Webhooks
&lt;/h2&gt;

&lt;p&gt;Some developers hear “reconciliation” and think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should I just poll the payment API instead of using webhooks?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;That is the wrong framing.&lt;/p&gt;

&lt;p&gt;Webhooks and reconciliation solve different problems.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;td&gt;Fast event delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconciliation&lt;/td&gt;
&lt;td&gt;Consistency verification and repair&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Webhooks are for responsiveness.&lt;/p&gt;

&lt;p&gt;Reconciliation is for correctness.&lt;/p&gt;

&lt;p&gt;A good system uses both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Webhook path:
payment event happens
→ provider sends callback
→ merchant updates state quickly

Reconciliation path:
scheduled job checks unresolved or risky records
→ compares local state with provider state
→ repairs mismatch or flags review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Webhooks are the primary update mechanism.&lt;/p&gt;

&lt;p&gt;Reconciliation is the safety net.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reconciliation Mindset
&lt;/h2&gt;

&lt;p&gt;A payment reconciliation system should be designed around one principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every important payment fact must be independently verifiable later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means you should be able to answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which order was this payment for?
Which invoice did the gateway create?
Which provider reference identifies it?
Which webhook events arrived?
Which transaction hash was detected?
Which asset and network were used?
What amount was expected?
What amount was received?
What did the gateway report?
What did our database record?
What business action did we take?
Was fulfillment triggered?
Was settlement completed?
Did finance record it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you cannot answer these questions from stored data, reconciliation will become guesswork.&lt;/p&gt;

&lt;p&gt;Guesswork does not scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Systems That Need to Agree
&lt;/h2&gt;

&lt;p&gt;Crypto payment reconciliation is not only “database vs provider.”&lt;/p&gt;

&lt;p&gt;It often involves several systems.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Order system
2. Invoice/payment table
3. Webhook event table
4. Gateway payment API
5. Blockchain transaction data
6. Fulfillment system
7. Merchant balance / settlement records
8. Accounting or reporting layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small merchant may only have the first four.&lt;/p&gt;

&lt;p&gt;A larger platform may have all eight.&lt;/p&gt;

&lt;p&gt;The architecture should make each layer explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Practical Reconciliation Model
&lt;/h2&gt;

&lt;p&gt;At minimum, store these local records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orders
payments / invoices
payment_events
payment_transactions
settlements or balance movements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not need a huge system on day one, but you need enough structure to compare states.&lt;/p&gt;

&lt;p&gt;A minimal payment table might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pricing_currency&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expected_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;selected_asset&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;selected_network&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;gateway_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;business_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_provider_check_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_reconciled_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reconciliation_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'not_checked'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A webhook event table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;crypto_payment_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_hash&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processing_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'received'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;processing_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A transaction table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;crypto_payment_transactions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tx_hash&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;asset&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expected_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;received_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confirmations&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tx_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;detected_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;confirmed_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tables give your system enough memory to understand what happened.&lt;/p&gt;

&lt;p&gt;Without this structure, reconciliation has nothing reliable to compare.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source of Truth Is Not One Thing
&lt;/h2&gt;

&lt;p&gt;A common mistake is asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is the source of truth?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In payment systems, the better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Source of truth for what?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Different systems own different facts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fact&lt;/th&gt;
&lt;th&gt;Likely Source of Truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Customer ordered item&lt;/td&gt;
&lt;td&gt;Merchant order system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invoice was created&lt;/td&gt;
&lt;td&gt;Payment provider + local invoice table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider payment status&lt;/td&gt;
&lt;td&gt;Gateway API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blockchain transaction exists&lt;/td&gt;
&lt;td&gt;Blockchain / gateway monitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook was received&lt;/td&gt;
&lt;td&gt;Local event table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order was fulfilled&lt;/td&gt;
&lt;td&gt;Merchant fulfillment system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funds were settled&lt;/td&gt;
&lt;td&gt;Gateway balance / settlement ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue was recognized&lt;/td&gt;
&lt;td&gt;Accounting system&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Trying to force one system to be the source of truth for everything creates bad architecture.&lt;/p&gt;

&lt;p&gt;Reconciliation works by comparing specialized truths.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local State vs External Truth
&lt;/h2&gt;

&lt;p&gt;Your database may say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment_status = waiting_for_payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway may say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status = paid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not automatically mean your database is wrong.&lt;/p&gt;

&lt;p&gt;It means your database is stale.&lt;/p&gt;

&lt;p&gt;The reconciliation system needs to ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can we safely move local status from waiting_for_payment to paid?
Was the payment already fulfilled?
Was the order cancelled?
Did the invoice expire?
Was the payment underpaid?
Is this a duplicate provider result?
Does this require manual review?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reconciliation job should not blindly overwrite state.&lt;/p&gt;

&lt;p&gt;It should apply safe transitions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reconciliation Loop
&lt;/h2&gt;

&lt;p&gt;A basic reconciliation loop looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Select payments that need checking
2. Fetch external payment truth
3. Normalize external status
4. Compare with local state
5. Decide whether to repair, ignore, or flag
6. Apply idempotent state transition
7. Record reconciliation result
8. Emit alerts or admin notes if needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In code terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcileOpenPayments&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findPaymentsNeedingReconciliation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchProviderPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider_track_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;externalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeProviderStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapGatewayStatusToPaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;externalStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decideReconciliationAction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;localPayment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;nextStatus&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyReconciliationDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;recordReconciliationFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the language.&lt;/p&gt;

&lt;p&gt;The important part is the decision layer.&lt;/p&gt;

&lt;p&gt;Reconciliation should not be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch provider status → overwrite local status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch provider status → compare → validate transition → repair safely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Which Payments Should Be Reconciled?
&lt;/h2&gt;

&lt;p&gt;You do not need to check every payment forever.&lt;/p&gt;

&lt;p&gt;Start with records that are likely to drift.&lt;/p&gt;

&lt;p&gt;Good candidates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created
waiting_for_payment
payment_detected
confirming
underpaid
manual_review
expired_recently
paid_but_not_fulfilled
fulfilled_but_payment_not_final
webhook_failed
event_received_but_not_processed
settlement_pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can define selection rules like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'waiting_for_payment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'payment_detected'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'confirming'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'underpaid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'manual_review'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add special checks for stale states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'confirming'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 minutes'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for suspicious business mismatches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;business_status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'fulfilled'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'10 minutes'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last query is powerful.&lt;/p&gt;

&lt;p&gt;It catches payments that succeeded but did not trigger the expected business action.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciliation Frequency
&lt;/h2&gt;

&lt;p&gt;Reconciliation does not need to run at the same frequency for every payment.&lt;/p&gt;

&lt;p&gt;Use different schedules for different risk zones.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Payment Group&lt;/th&gt;
&lt;th&gt;Suggested Frequency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;newly created invoices&lt;/td&gt;
&lt;td&gt;every 1–2 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;payment detected / confirming&lt;/td&gt;
&lt;td&gt;every 1–5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;underpaid / manual review&lt;/td&gt;
&lt;td&gt;every 10–30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expired within last few hours&lt;/td&gt;
&lt;td&gt;every 10–30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;paid but not fulfilled&lt;/td&gt;
&lt;td&gt;every few minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;settlement pending&lt;/td&gt;
&lt;td&gt;hourly or daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;historical audit&lt;/td&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The goal is not to overload the provider API.&lt;/p&gt;

&lt;p&gt;The goal is to reduce the time that your system remains inconsistent.&lt;/p&gt;

&lt;p&gt;Use backoff.&lt;/p&gt;

&lt;p&gt;A payment that was created two minutes ago deserves more attention than a failed invoice from three weeks ago.&lt;/p&gt;




&lt;h2&gt;
  
  
  Avoid Infinite Reconciliation
&lt;/h2&gt;

&lt;p&gt;A reconciliation system should not keep retrying forever with no strategy.&lt;/p&gt;

&lt;p&gt;Store retry metadata.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;reconciliation_attempts&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;next_reconciliation_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;last_reconciliation_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;attempt 1 → retry in 1 minute
attempt 2 → retry in 5 minutes
attempt 3 → retry in 15 minutes
attempt 4 → retry in 1 hour
attempt 5 → manual review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents a broken payment from consuming resources forever.&lt;/p&gt;

&lt;p&gt;It also makes stuck cases visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mapping Gateway Statuses
&lt;/h2&gt;

&lt;p&gt;Every provider has its own status vocabulary.&lt;/p&gt;

&lt;p&gt;Your application should not scatter provider-specific strings throughout the codebase.&lt;/p&gt;

&lt;p&gt;Normalize them.&lt;/p&gt;

&lt;p&gt;Using OxaPay as an implementation reference, payment statuses may include values such as &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;paying&lt;/code&gt;, &lt;code&gt;paid&lt;/code&gt;, &lt;code&gt;manual_accept&lt;/code&gt;, &lt;code&gt;underpaid&lt;/code&gt;, &lt;code&gt;refunding&lt;/code&gt;, &lt;code&gt;refunded&lt;/code&gt;, and &lt;code&gt;expired&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A simple mapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mapGatewayStatusToPaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting_for_payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paying&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;manual_accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid_manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;underpaid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;underpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;refunding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;refunded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives your system one internal language.&lt;/p&gt;

&lt;p&gt;If you change providers later, most business logic remains stable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Safe State Transitions
&lt;/h2&gt;

&lt;p&gt;Reconciliation must respect valid state transitions.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowedTransitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting_for_payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;waiting_for_payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;underpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;payment_detected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirming&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;underpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;confirming&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;underpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;underpaid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;late_payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;late_payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;paid_manual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;refunding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;refunded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;manual_review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;allowedTransitions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why allow &lt;code&gt;expired → late_payment&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Because crypto payments can arrive after expiration.&lt;/p&gt;

&lt;p&gt;The invoice may be expired from a checkout perspective, but money still moved.&lt;/p&gt;

&lt;p&gt;A good reconciliation system makes that visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Matrix
&lt;/h2&gt;

&lt;p&gt;Reconciliation should produce decisions, not just updates.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Local State&lt;/th&gt;
&lt;th&gt;Provider State&lt;/th&gt;
&lt;th&gt;Business Context&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waiting_for_payment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;order still active&lt;/td&gt;
&lt;td&gt;repair to &lt;code&gt;paid&lt;/code&gt;, trigger fulfillment once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waiting_for_payment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no transaction&lt;/td&gt;
&lt;td&gt;mark expired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;late payment allowed&lt;/td&gt;
&lt;td&gt;move to &lt;code&gt;late_payment&lt;/code&gt; or &lt;code&gt;paid&lt;/code&gt; based on policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;already fulfilled&lt;/td&gt;
&lt;td&gt;no-op&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;refunded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;refund confirmed&lt;/td&gt;
&lt;td&gt;update refund status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;underpaid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;remaining amount completed&lt;/td&gt;
&lt;td&gt;repair to &lt;code&gt;paid&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;underpaid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;impossible regression&lt;/td&gt;
&lt;td&gt;flag manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;webhook missed&lt;/td&gt;
&lt;td&gt;repair to &lt;code&gt;paid&lt;/code&gt;, record missed event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;provider not found&lt;/td&gt;
&lt;td&gt;provider error&lt;/td&gt;
&lt;td&gt;retry later, do not downgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fulfilled&lt;/code&gt; order&lt;/td&gt;
&lt;td&gt;unpaid provider&lt;/td&gt;
&lt;td&gt;serious mismatch&lt;/td&gt;
&lt;td&gt;manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is where production maturity shows.&lt;/p&gt;

&lt;p&gt;A reconciliation system should be careful with destructive changes.&lt;/p&gt;

&lt;p&gt;For example, do not move a paid order back to unpaid just because one provider lookup fails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fetching Provider Payment Information
&lt;/h2&gt;

&lt;p&gt;Most payment gateways expose some way to look up a payment by provider reference.&lt;/p&gt;

&lt;p&gt;In OxaPay, the &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;Payment Information endpoint&lt;/a&gt; lets developers retrieve payment details by &lt;code&gt;track_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET https://api.oxapay.com/v1/payment/184747701 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"merchant_api_key: &lt;/span&gt;&lt;span class="nv"&gt;$OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchOxaPayPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.oxapay.com/v1/payment/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OXAPAY_MERCHANT_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Payment lookup failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your own abstraction, do not let the rest of the system depend on one provider response shape.&lt;/p&gt;

&lt;p&gt;Wrap it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchProviderPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchOxaPayPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oxapay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;gatewayStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expectedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;receivedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;received_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;txHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tx_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unsupported provider: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes reconciliation easier to maintain if your payment infrastructure changes later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repairing State Safely
&lt;/h2&gt;

&lt;p&gt;When reconciliation finds a mismatch, repair should be idempotent.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyReconciliationDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reconciliation_runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;previous_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatewayStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;raw_provider_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;reconciliation_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;last_reconciled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repair_status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;payment_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;gateway_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatewayStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;last_provider_check_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;last_reconciled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;reconciliation_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repaired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fulfillOrderOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fulfillOrderOnce&lt;/code&gt; function matters.&lt;/p&gt;

&lt;p&gt;Reconciliation may discover a missed paid status.&lt;/p&gt;

&lt;p&gt;That does not mean it should blindly trigger fulfillment again.&lt;/p&gt;

&lt;p&gt;Use a uniqueness rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;order_fulfillments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;fulfilled_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then fulfillment becomes safe to call from both webhook processing and reconciliation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciliation Run Table
&lt;/h2&gt;

&lt;p&gt;Store reconciliation runs.&lt;/p&gt;

&lt;p&gt;Do not just update payment rows silently.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payment_reconciliation_runs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_track_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;local_status_before&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider_status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;raw_provider_payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This table gives you an audit trail.&lt;/p&gt;

&lt;p&gt;It answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When did we check this payment?
What did the provider say?
What did our system say before the check?
What did reconciliation decide?
Did we repair anything?
Why was it sent to manual review?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is extremely useful for production support.&lt;/p&gt;




&lt;h2&gt;
  
  
  Webhook Events and Reconciliation Should Share the Same State Engine
&lt;/h2&gt;

&lt;p&gt;Do not build two separate status systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;webhook handler status logic
reconciliation job status logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates divergence.&lt;/p&gt;

&lt;p&gt;Both should call the same internal state engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;webhook event
        |
        v
normalize provider status
        |
        v
apply payment transition
        |
        v
business side effects

reconciliation lookup
        |
        v
normalize provider status
        |
        v
apply payment transition
        |
        v
business side effects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source is different.&lt;/p&gt;

&lt;p&gt;The transition engine should be the same.&lt;/p&gt;

&lt;p&gt;That makes behavior consistent whether the update came from a webhook or from reconciliation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciliation and Idempotency Work Together
&lt;/h2&gt;

&lt;p&gt;Reconciliation will find the same paid payment more than once.&lt;/p&gt;

&lt;p&gt;That is normal.&lt;/p&gt;

&lt;p&gt;Your system should handle it.&lt;/p&gt;

&lt;p&gt;A payment already marked paid should not trigger fulfillment again.&lt;/p&gt;

&lt;p&gt;An already recorded transaction should not be duplicated.&lt;/p&gt;

&lt;p&gt;An already refunded order should not be refunded twice.&lt;/p&gt;

&lt;p&gt;Idempotency rules should exist at multiple levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Idempotency Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Webhook event&lt;/td&gt;
&lt;td&gt;unique event hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction&lt;/td&gt;
&lt;td&gt;unique provider + tx hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment state&lt;/td&gt;
&lt;td&gt;safe transition guard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fulfillment&lt;/td&gt;
&lt;td&gt;unique order fulfillment record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credit balance&lt;/td&gt;
&lt;td&gt;unique ledger entry reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund&lt;/td&gt;
&lt;td&gt;unique refund reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconciliation&lt;/td&gt;
&lt;td&gt;no repeated side effects for same state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Idempotency is not one feature.&lt;/p&gt;

&lt;p&gt;It is a pattern across the whole payment system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciling Transactions
&lt;/h2&gt;

&lt;p&gt;Payment status is not the only thing to reconcile.&lt;/p&gt;

&lt;p&gt;You may also need transaction reconciliation.&lt;/p&gt;

&lt;p&gt;For each provider payment, compare transaction-level data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tx_hash
asset
network
received_amount
confirmation count
detected_at
confirmed_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the provider reports a transaction that your local system does not have, insert it.&lt;/p&gt;

&lt;p&gt;If your local transaction exists but confirmation status changed, update it.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcileTransactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transactions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_payment_transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tx_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;received_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tx_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;detected_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detected_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;confirmed_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmed_at&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tx_hash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One invoice can have multiple transactions.&lt;/p&gt;

&lt;p&gt;Your reconciliation model should allow that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciling Amounts
&lt;/h2&gt;

&lt;p&gt;Amount reconciliation is subtle in crypto.&lt;/p&gt;

&lt;p&gt;A merchant may price an invoice in fiat, while the customer pays in crypto.&lt;/p&gt;

&lt;p&gt;You need to compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expected commercial amount
expected crypto amount
received crypto amount
received fiat-equivalent amount
fees
settlement amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At minimum, store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pricing_currency
expected_amount
selected_asset
selected_network
expected_crypto_amount
received_crypto_amount
received_fiat_value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then detect cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;received = expected&lt;/td&gt;
&lt;td&gt;normal paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;received &amp;lt; expected&lt;/td&gt;
&lt;td&gt;underpaid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;received &amp;gt; expected&lt;/td&gt;
&lt;td&gt;overpaid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;received after expiration&lt;/td&gt;
&lt;td&gt;late payment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;received on wrong network&lt;/td&gt;
&lt;td&gt;support/recovery issue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;received in multiple txs&lt;/td&gt;
&lt;td&gt;aggregate before deciding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;provider says paid but local amount missing&lt;/td&gt;
&lt;td&gt;repair local amount&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Never rely only on a boolean &lt;code&gt;paid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Payment amount is part of the truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciling Orders
&lt;/h2&gt;

&lt;p&gt;A payment can be paid while the order is not fulfilled.&lt;/p&gt;

&lt;p&gt;That is a mismatch.&lt;/p&gt;

&lt;p&gt;A payment can be unpaid while the order is fulfilled.&lt;/p&gt;

&lt;p&gt;That is a more serious mismatch.&lt;/p&gt;

&lt;p&gt;Order reconciliation checks business effects.&lt;/p&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fulfillment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'not_fulfilled'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'10 minutes'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the opposite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'paid_manual'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fulfillment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'fulfilled'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first case may require repair.&lt;/p&gt;

&lt;p&gt;The second case requires investigation.&lt;/p&gt;

&lt;p&gt;Reconciliation is not only about fixing pending payments.&lt;/p&gt;

&lt;p&gt;It also catches dangerous business inconsistencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciling Settlement
&lt;/h2&gt;

&lt;p&gt;Payment completion and settlement are different.&lt;/p&gt;

&lt;p&gt;A customer may pay an invoice, but funds may still need to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;credited to merchant balance
converted
withdrawn
posted to ledger
matched to accounting records
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For small integrations, settlement reconciliation may be manual.&lt;/p&gt;

&lt;p&gt;For larger systems, it needs to be explicit.&lt;/p&gt;

&lt;p&gt;Separate these fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment_status
settlement_status
ledger_status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payment_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;paid&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;settlement_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;credited_to_balance&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ledger_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;posted&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A paid invoice that never appears in finance reporting is still a problem.&lt;/p&gt;

&lt;p&gt;It may not affect the customer, but it affects the business.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling Late Payments
&lt;/h2&gt;

&lt;p&gt;Late payments are a major reason reconciliation exists.&lt;/p&gt;

&lt;p&gt;A customer may pay after invoice expiration because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they used an exchange withdrawal&lt;/li&gt;
&lt;li&gt;they waited too long&lt;/li&gt;
&lt;li&gt;the blockchain was congested&lt;/li&gt;
&lt;li&gt;the payment page expired before the transaction appeared&lt;/li&gt;
&lt;li&gt;they copied the address and paid later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The provider may later show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expired invoice received funds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your local system may show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order cancelled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reconciliation should detect this and move the payment into a visible state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expired
→ late_payment
→ manual_review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decideLatePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatewayStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;nextStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;late_payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_received_after_expiration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not silently ignore late money movement.&lt;/p&gt;

&lt;p&gt;Even if the order should not be fulfilled, finance and support need to know funds arrived.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling Underpayments
&lt;/h2&gt;

&lt;p&gt;Underpaid invoices should not disappear into failure.&lt;/p&gt;

&lt;p&gt;Reconciliation should keep checking if the provider allows additional payments or if the customer completes the remaining amount later.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider: underpaid
Local: waiting_for_payment
Decision: move to underpaid, notify customer/support
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider: paid
Local: underpaid
Decision: repair to paid, trigger fulfillment once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why &lt;code&gt;underpaid → paid&lt;/code&gt; should be a valid transition.&lt;/p&gt;

&lt;p&gt;A strict state machine that treats underpaid as terminal creates unnecessary manual work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling Overpayments
&lt;/h2&gt;

&lt;p&gt;Overpayment reconciliation should record the excess.&lt;/p&gt;

&lt;p&gt;Do not simply mark paid and discard the difference.&lt;/p&gt;

&lt;p&gt;Possible handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mark invoice paid
record overpaid amount
create admin note
optionally create refund/credit task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expected_amount
received_amount
overpaid_amount
overpayment_policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters for accounting and customer trust.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling Provider API Failures
&lt;/h2&gt;

&lt;p&gt;A reconciliation job depends on provider API availability.&lt;/p&gt;

&lt;p&gt;That API can fail.&lt;/p&gt;

&lt;p&gt;Your job should not panic.&lt;/p&gt;

&lt;p&gt;Classify provider errors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error Type&lt;/th&gt;
&lt;th&gt;Handling&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;timeout&lt;/td&gt;
&lt;td&gt;retry with backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429 rate limit&lt;/td&gt;
&lt;td&gt;slow down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx provider error&lt;/td&gt;
&lt;td&gt;retry later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401 auth error&lt;/td&gt;
&lt;td&gt;alert immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;payment not found&lt;/td&gt;
&lt;td&gt;retry if recent, manual review if persistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;malformed response&lt;/td&gt;
&lt;td&gt;alert engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;network error&lt;/td&gt;
&lt;td&gt;retry later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not downgrade payments because one lookup failed.&lt;/p&gt;

&lt;p&gt;A provider error means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;external truth unavailable right now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Rate Limits and Batching
&lt;/h2&gt;

&lt;p&gt;Reconciliation can create unnecessary API load if implemented carelessly.&lt;/p&gt;

&lt;p&gt;Good practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reconcile only unresolved or risky payments&lt;/li&gt;
&lt;li&gt;prioritize recent invoices&lt;/li&gt;
&lt;li&gt;use exponential backoff&lt;/li&gt;
&lt;li&gt;stop checking terminal states after a safe window&lt;/li&gt;
&lt;li&gt;batch where provider supports it&lt;/li&gt;
&lt;li&gt;cache recent lookup results briefly&lt;/li&gt;
&lt;li&gt;avoid multiple workers reconciling the same payment at once&lt;/li&gt;
&lt;li&gt;respect provider rate limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use locks.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;reconciliation_locked_until&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worker selection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;crypto_payments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;next_reconciliation_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;reconciliation_locked_until&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;reconciliation_locked_until&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then lock selected rows before processing.&lt;/p&gt;

&lt;p&gt;Without locks, multiple workers may reconcile the same payment and create duplicate work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Manual Review Is a Feature
&lt;/h2&gt;

&lt;p&gt;Manual review should not be treated as failure.&lt;/p&gt;

&lt;p&gt;It is a valid state.&lt;/p&gt;

&lt;p&gt;Some cases should not be repaired automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider says paid, order was already refunded
provider status regressed unexpectedly
payment arrived after order cancellation
wrong network suspected
large overpayment
provider data incomplete
transaction hash conflicts with another invoice
fulfilled order has unpaid payment status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manual review should include context.&lt;/p&gt;

&lt;p&gt;A good review record includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order_id
provider_track_id
local_status
provider_status
expected_amount
received_amount
asset
network
tx_hash
reason
recommended action
raw provider payload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Support should not see only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;manual_review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They should see why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Alerting Rules
&lt;/h2&gt;

&lt;p&gt;Not every mismatch needs a pager.&lt;/p&gt;

&lt;p&gt;But some do.&lt;/p&gt;

&lt;p&gt;Useful alert categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alert&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;paid provider status but local pending for more than 10 minutes&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fulfilled order with unpaid payment status&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webhook signature failures spike&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;provider API authentication failure&lt;/td&gt;
&lt;td&gt;critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reconciliation job not running&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;many expired invoices later paid&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;underpayment rate increasing&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;duplicate webhook rate unusually high&lt;/td&gt;
&lt;td&gt;low/medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;settlement mismatch&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;manual review queue growing&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Metrics should support alerts.&lt;/p&gt;

&lt;p&gt;Do not rely only on logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciliation Metrics
&lt;/h2&gt;

&lt;p&gt;Track these metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open_payments_count
stale_pending_count
paid_not_fulfilled_count
fulfilled_not_paid_count
webhook_failed_count
reconciliation_run_count
reconciliation_repair_count
manual_review_count
provider_lookup_failure_count
average_time_to_payment
average_time_to_reconciliation_repair
underpayment_rate
late_payment_rate
settlement_mismatch_count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These metrics tell you whether your payment system is healthy.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;high &lt;code&gt;stale_pending_count&lt;/code&gt; may mean callbacks are failing&lt;/li&gt;
&lt;li&gt;high &lt;code&gt;paid_not_fulfilled_count&lt;/code&gt; may mean order transition logic is broken&lt;/li&gt;
&lt;li&gt;high &lt;code&gt;manual_review_count&lt;/code&gt; may mean policies are unclear&lt;/li&gt;
&lt;li&gt;high &lt;code&gt;provider_lookup_failure_count&lt;/code&gt; may mean API or auth issues&lt;/li&gt;
&lt;li&gt;high &lt;code&gt;late_payment_rate&lt;/code&gt; may mean invoice lifetime is too short&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reconciliation is not just repair.&lt;/p&gt;

&lt;p&gt;It is observability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Admin and Support View
&lt;/h2&gt;

&lt;p&gt;A reconciliation system should surface useful information to humans.&lt;/p&gt;

&lt;p&gt;For each payment, show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order_id
provider
track_id
payment_url
gateway_status
payment_status
business_status
expected_amount
received_amount
asset
network
tx_hash
invoice_created_at
expires_at
paid_at
last_webhook_at
last_provider_check_at
last_reconciled_at
reconciliation_status
manual_review_reason
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also show the timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12:00 invoice created
12:03 webhook received: waiting
12:06 webhook received: paying
12:08 webhook failed during processing
12:12 reconciliation checked provider: paid
12:12 local state repaired: waiting_for_payment → paid
12:12 order fulfillment triggered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of timeline reduces support time dramatically.&lt;/p&gt;

&lt;p&gt;It also reduces pressure on engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example Implementation: OxaPay as a Reference
&lt;/h2&gt;

&lt;p&gt;OxaPay is useful as an implementation reference because it exposes the core primitives a reconciliation system needs.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/api-reference/payment/generate-invoice" rel="noopener noreferrer"&gt;Generate Invoice endpoint&lt;/a&gt; creates a payment request and returns a &lt;code&gt;track_id&lt;/code&gt; and &lt;code&gt;payment_url&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/webhook" rel="noopener noreferrer"&gt;Webhook documentation&lt;/a&gt; explains callback delivery, HMAC SHA-512 validation over the raw POST body, and the need to return HTTP 200 with &lt;code&gt;ok&lt;/code&gt; after successful processing.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-status-table" rel="noopener noreferrer"&gt;Payment Status Table&lt;/a&gt; gives developers provider-side statuses that can be mapped into internal payment states.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.oxapay.com/api-reference/payment/payment-information" rel="noopener noreferrer"&gt;Payment Information endpoint&lt;/a&gt; lets your backend retrieve payment details by &lt;code&gt;track_id&lt;/code&gt;, which is exactly what reconciliation needs.&lt;/p&gt;

&lt;p&gt;The important point is not that reconciliation belongs to one provider.&lt;/p&gt;

&lt;p&gt;The important point is that reliable crypto payment systems need these primitives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider reference
signed event delivery
status table
payment lookup
local event storage
idempotent state transitions
manual review path
reconciliation job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether you use OxaPay, another gateway, or your own infrastructure, those primitives still need to exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Minimal Reconciliation Architecture
&lt;/h2&gt;

&lt;p&gt;A practical architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Webhook Receiver
  |
  | signed payment events
  v
Event Store
  |
  | raw events, hashes, processing status
  v
Payment State Engine
  |
  | normalize status, apply transitions
  v
Order / Business Layer
  |
  | fulfill, hold, refund, notify
  ^
  |
Reconciliation Worker
  |
  | provider lookup by track_id
  v
Provider Payment API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Webhooks and reconciliation should both feed the same payment state engine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Do not build separate logic paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  Minimal Reconciliation Pseudocode
&lt;/h2&gt;

&lt;p&gt;Here is the whole system in compact form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runReconciliationBatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;selectPaymentsForReconciliation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reconcileOnePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcileOnePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchProviderPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider_track_id&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapGatewayStatusToPaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatewayStatus&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decideReconciliationAction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;nextStatus&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applyDecisionIdempotently&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;decision&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decideReconciliationAction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerPayment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;already_consistent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;canTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_state_transition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;nextStatus&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;late_payment_after_expiration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;nextStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;late_payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repair_and_fulfill_once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider_paid_local_not_paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;nextStatus&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repair_status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;safe_state_repair&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;nextStatus&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not complete production code, but the shape is right.&lt;/p&gt;

&lt;p&gt;It has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selection
provider lookup
normalization
decision
safe transition
manual review
idempotent repair
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the core of reconciliation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing Reconciliation
&lt;/h2&gt;

&lt;p&gt;Test reconciliation directly.&lt;/p&gt;

&lt;p&gt;Do not only test webhooks.&lt;/p&gt;

&lt;p&gt;Important test cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider paid, local pending
provider paid, local expired
provider underpaid, local waiting
provider paid, local underpaid
provider expired, local waiting
provider refunded, local paid
provider lookup timeout
provider auth failure
provider not found
order fulfilled, payment not paid
payment paid, order not fulfilled
duplicate reconciliation run
two workers select same payment
manual review case
late payment after expiration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each case should have an expected decision.&lt;/p&gt;

&lt;p&gt;If you cannot define the expected decision, your production system will behave inconsistently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping crypto payment reconciliation, verify this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every invoice stores provider_track_id
Every webhook event is stored with a unique hash
Provider statuses are normalized in one place
Payment state transitions are guarded
Fulfillment is idempotent
Paid payments can be repaired if webhook fails
Expired invoices can detect late payments
Underpaid invoices can later become paid
Provider lookup failures do not downgrade local state
Manual review has reasons and context
Reconciliation runs are logged
Reconciliation has backoff and retry limits
Workers use locks or safe selection
Metrics track stale states and repairs
Support can search by order_id, track_id, tx_hash
Settlement state is separate from payment state
Webhooks and reconciliation use the same state engine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checklist is the difference between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We hope callbacks arrive.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We can prove our payment state is correct.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Crypto payment reconciliation is easy to ignore because the first integration usually works without it.&lt;/p&gt;

&lt;p&gt;The first invoice is created.&lt;br&gt;&lt;br&gt;
The first webhook arrives.&lt;br&gt;&lt;br&gt;
The first order is marked paid.&lt;br&gt;&lt;br&gt;
Everything looks done.&lt;/p&gt;

&lt;p&gt;But production is not the first transaction.&lt;/p&gt;

&lt;p&gt;Production is the thousandth transaction, the missed callback, the late payment, the underpaid invoice, the duplicate webhook, the customer with a transaction hash, the order stuck in pending, and the finance report that does not match the gateway balance.&lt;/p&gt;

&lt;p&gt;That is where reconciliation matters.&lt;/p&gt;

&lt;p&gt;Webhooks make crypto payment systems responsive.&lt;/p&gt;

&lt;p&gt;Reconciliation makes them trustworthy.&lt;/p&gt;

&lt;p&gt;If you are building a crypto payment integration, do not stop at:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we receive payment events?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask the harder question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we prove later that our local payment state still matches what actually happened?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question is where serious payment infrastructure begins.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>backend</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Shopify Payments Are Simple, Until They Break</title>
      <dc:creator>kevin.s</dc:creator>
      <pubDate>Tue, 05 May 2026 08:24:23 +0000</pubDate>
      <link>https://dev.to/kevins1988/shopify-payments-are-simple-until-they-break-12nl</link>
      <guid>https://dev.to/kevins1988/shopify-payments-are-simple-until-they-break-12nl</guid>
      <description>&lt;p&gt;Shopify makes it easy to create an order.&lt;/p&gt;

&lt;p&gt;That is not the hard part.&lt;/p&gt;

&lt;p&gt;The hard part starts when the payment does not behave like a clean, instant, single-step event.&lt;/p&gt;

&lt;p&gt;A webhook arrives twice.&lt;br&gt;&lt;br&gt;
A payment is delayed.&lt;br&gt;&lt;br&gt;
The amount is slightly different.&lt;br&gt;&lt;br&gt;
The order was already cancelled.&lt;br&gt;&lt;br&gt;
The customer says they paid, but your system still shows pending.&lt;/p&gt;

&lt;p&gt;If your integration assumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;payment_received&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;order_paid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it will probably work in testing.&lt;/p&gt;

&lt;p&gt;It will fail in production.&lt;/p&gt;

&lt;p&gt;The real challenge is not “how to accept a payment on Shopify.”&lt;br&gt;&lt;br&gt;
It is how to design a payment flow that stays consistent when payment events are asynchronous, duplicated, delayed, or incomplete.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Core Rule: Order State and Payment State Must Be Separate
&lt;/h2&gt;

&lt;p&gt;A Shopify order and an external payment are related, but they are not the same object.&lt;/p&gt;

&lt;p&gt;The order belongs to Shopify.&lt;br&gt;&lt;br&gt;
The payment belongs to your system.&lt;/p&gt;

&lt;p&gt;If you merge them too early, you lose control.&lt;/p&gt;

&lt;p&gt;A better architecture looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frglfjoyjf5pj6ik128cr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frglfjoyjf5pj6ik128cr.png" alt=" " width="800" height="1743"&gt;&lt;/a&gt;&lt;br&gt;
The payment system should decide payment state first.&lt;/p&gt;
&lt;h2&gt;
  
  
  Only after that should Shopify be updated.
&lt;/h2&gt;
&lt;h2&gt;
  
  
  A Practical Payment State Machine
&lt;/h2&gt;

&lt;p&gt;For an external payment flow, you need states.&lt;/p&gt;

&lt;p&gt;Not just paid and unpaid.&lt;/p&gt;

&lt;p&gt;A more realistic model looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpi558aka2jyhszj43jm1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpi558aka2jyhszj43jm1.png" alt=" " width="715" height="862"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A payment lifecycle might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;AWAITING_CONFIRMATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;awaiting_confirmation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;CONFIRMED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;UNDERPAID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;underpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;OVERPAID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;overpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;EXPIRED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;FAILED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;This matters because a webhook is not your source of truth.&lt;/p&gt;

&lt;p&gt;A webhook is only a signal that something changed.&lt;/p&gt;

&lt;p&gt;Your database should store the current payment state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Model: Store Payments Separately
&lt;/h2&gt;

&lt;p&gt;Here is a simple payment record model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// payments table / collection&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pay_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;shopifyOrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gid://shopify/Order/123456789&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;providerInvoiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inv_987&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;expectedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;receivedAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-05-05T12:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-05-05T11:30:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-05-05T11:30:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also need a webhook event log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// webhook_events table / collection&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evt_abc123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;providerInvoiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inv_987&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment.confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;processedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-05-05T11:45:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this second table, duplicate webhooks will eventually hurt you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Express Webhook Handler
&lt;/h2&gt;

&lt;p&gt;Below is a simplified but realistic Node.js example.&lt;/p&gt;

&lt;p&gt;It shows:&lt;/p&gt;

&lt;p&gt;idempotency&lt;br&gt;
signature verification placeholder&lt;br&gt;
payment lookup&lt;br&gt;
state transition&lt;br&gt;
Shopify order sync&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Important: For production, you should use raw body for signature verification&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;AWAITING_CONFIRMATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;awaiting_confirmation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;CONFIRMED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;UNDERPAID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;underpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;OVERPAID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;overpaid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;EXPIRED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;FAILED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;       &lt;span class="c1"&gt;// key = providerInvoiceId&lt;/span&gt;
  &lt;span class="na"&gt;webhookEvents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;  &lt;span class="c1"&gt;// for idempotency&lt;/span&gt;

  &lt;span class="nf"&gt;getPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;savePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;providerInvoiceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;hasProcessedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;storeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhookEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// TODO: Implement real signature verification in production&lt;/span&gt;
  &lt;span class="c1"&gt;// Example:&lt;/span&gt;
  &lt;span class="c1"&gt;// const signature = req.headers["x-signature"];&lt;/span&gt;
  &lt;span class="c1"&gt;// const payload = JSON.stringify(req.body); // Use raw body in real implementation&lt;/span&gt;
  &lt;span class="c1"&gt;// const expected = crypto&lt;/span&gt;
  &lt;span class="c1"&gt;//   .createHmac("sha256", process.env.WEBHOOK_SECRET)&lt;/span&gt;
  &lt;span class="c1"&gt;//   .update(payload)&lt;/span&gt;
  &lt;span class="c1"&gt;//   .digest("hex");&lt;/span&gt;
  &lt;span class="c1"&gt;// return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// For development only&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateNextPaymentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expectedAmount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;received&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAmount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FAILED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EXPIRED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWAITING_CONFIRMATION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;received&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNDERPAID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;received&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OVERPAID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CONFIRMED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markShopifyOrderAsPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Implement Shopify Admin API call here&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[Shopify] Marking order as paid: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addShopifyOrderNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Implement Shopify Admin API call here&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[Shopify] Adding note to order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;syncShopifyOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CONFIRMED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markShopifyOrderAsPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shopifyOrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNDERPAID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addShopifyOrderNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shopifyOrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;`Payment underpaid. Expected &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expectedAmount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, received &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAmount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;PaymentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EXPIRED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addShopifyOrderNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shopifyOrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment request expired before confirmation.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ====================== WEBHOOK ENDPOINT ======================&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid webhook payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Idempotency check&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasProcessedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;duplicate_ignored&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Calculate new state&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateNextPaymentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Update payment&lt;/span&gt;
    &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAmount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAmount&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAmount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Save changes&lt;/span&gt;
    &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;savePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Sync with Shopify&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;syncShopifyOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="na"&gt;paymentStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextState&lt;/span&gt; 
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Webhook handling failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Internal webhook error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Shopify Sync Should Be a Separate Function
&lt;/h2&gt;

&lt;p&gt;Avoid placing Shopify API logic directly inside the webhook handler.&lt;/p&gt;

&lt;p&gt;Keep it separate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markShopifyOrderAsPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Use Shopify Admin API here.&lt;/span&gt;
  &lt;span class="c1"&gt;// In production, this may involve transactions, order notes,&lt;/span&gt;
  &lt;span class="c1"&gt;// tags, or fulfillment logic depending on your setup.&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Marking Shopify order as paid: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addShopifyOrderNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Use Shopify Admin API to add an order note or tag.&lt;/span&gt;
  &lt;span class="c1"&gt;// Useful for underpaid, expired, or manual-review cases.&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Adding note to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because payment state and order update are two different responsibilities.&lt;/p&gt;

&lt;p&gt;Your webhook handler should process payment truth.&lt;/p&gt;

&lt;p&gt;Your Shopify sync layer should translate that truth into order actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Idempotency Matters
&lt;/h2&gt;

&lt;p&gt;Payment providers may retry webhooks.&lt;/p&gt;

&lt;p&gt;Network issues happen.&lt;/p&gt;

&lt;p&gt;Your endpoint may respond late.&lt;/p&gt;

&lt;p&gt;So the same event can arrive more than once.&lt;/p&gt;

&lt;p&gt;Bad logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markShopifyOrderAsPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendCustomerEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createFulfillment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this runs twice, you may send duplicate emails or trigger duplicate fulfillment.&lt;/p&gt;

&lt;p&gt;Better logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasProcessedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, this should be enforced at the database level with a unique constraint on eventId.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where External Payment Flows Usually Break
&lt;/h2&gt;

&lt;p&gt;Most bugs come from assumptions like these:&lt;/p&gt;

&lt;p&gt;“The webhook will arrive once.”&lt;/p&gt;

&lt;p&gt;It may not.&lt;/p&gt;

&lt;p&gt;“The payment amount will match exactly.”&lt;/p&gt;

&lt;p&gt;Network fees, wrong coin selection, or user mistakes can create mismatches.&lt;/p&gt;

&lt;p&gt;“The order still exists.”&lt;/p&gt;

&lt;p&gt;The customer may cancel, or your system may expire the order.&lt;/p&gt;

&lt;p&gt;“Confirmed means fulfilled.”&lt;/p&gt;

&lt;p&gt;Not always. Some products may require fraud checks, manual review, or additional business logic.&lt;/p&gt;

&lt;p&gt;This is why your integration should never be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;webhook&lt;/span&gt; &lt;span class="nx"&gt;received&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="nx"&gt;paid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;webhook&lt;/span&gt; &lt;span class="nx"&gt;received&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;validate&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;deduplicate&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;apply&lt;/span&gt; &lt;span class="nx"&gt;business&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;sync&lt;/span&gt; &lt;span class="nx"&gt;Shopify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where OxaPay Fits in This Architecture
&lt;/h2&gt;

&lt;p&gt;At this point, the natural question is not “which provider should I use?”&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;p&gt;Do I want to build the payment lifecycle myself, or use a payment system that already gives me structured invoices, trackable payment states, and callbacks?&lt;/p&gt;

&lt;p&gt;For crypto payment flows, &lt;a href="https://oxapay.com/" rel="noopener noreferrer"&gt;OxaPay &lt;/a&gt;fits as the external payment layer in this architecture.&lt;/p&gt;

&lt;p&gt;The useful part is not just that it accepts crypto.&lt;/p&gt;

&lt;p&gt;The useful part is that it gives you a structured payment object instead of forcing you to track raw wallet transactions manually.&lt;/p&gt;

&lt;p&gt;A Shopify flow can look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyv4gg930lsvmqtcsh7pr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyv4gg930lsvmqtcsh7pr.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not about replacing Shopify’s checkout logic.&lt;/p&gt;

&lt;p&gt;It is about adding a structured external flow for cases where a merchant wants to support crypto payments alongside their existing payment setup.&lt;/p&gt;

&lt;p&gt;Useful references:&lt;/p&gt;

&lt;p&gt;OxaPay Shopify automation guide:&lt;br&gt;
&lt;a href="https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation/shopify" rel="noopener noreferrer"&gt;https://docs.oxapay.com/welcome-to-oxapay/integrations/make-automation/shopify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make integration page:&lt;br&gt;
&lt;a href="https://www.make.com/en/integrations/oxapay-crypto-pay-gtw/shopify" rel="noopener noreferrer"&gt;https://www.make.com/en/integrations/oxapay-crypto-pay-gtw/shopify&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Checklist for Developers
&lt;/h2&gt;

&lt;p&gt;Before shipping an external payment flow, check this:&lt;/p&gt;

&lt;p&gt;Do you store payments separately from Shopify orders?&lt;/p&gt;

&lt;p&gt;Do you have a unique payment reference?&lt;/p&gt;

&lt;p&gt;Do you verify webhook signatures?&lt;/p&gt;

&lt;p&gt;Do you deduplicate webhook events?&lt;/p&gt;

&lt;p&gt;Do you handle underpaid, overpaid, expired, and failed payments?&lt;/p&gt;

&lt;p&gt;Do you separate payment state updates from Shopify order sync?&lt;/p&gt;

&lt;p&gt;Do you have logs for reconciliation?&lt;/p&gt;

&lt;p&gt;Do you avoid assuming that one webhook means one final payment?&lt;/p&gt;

&lt;p&gt;If the answer is no to any of these, your payment flow may work in testing but fail in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Payment integrations fail when they treat money movement like a simple API response.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;A reliable Shopify external payment flow needs state, idempotency, verification, and clear separation between payment logic and order logic.&lt;/p&gt;

&lt;p&gt;The goal is not to make the first payment work.&lt;/p&gt;

&lt;p&gt;The goal is to make the thousandth payment work, even when the webhook arrives twice, the payment is late, and the customer is already asking support what happened.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
