DEV Community

Afaq Shahid Khan
Afaq Shahid Khan

Posted on

Implementing Tap Payments in a SaaS Subscription System (Node.js + Express + Sequelize)

When building a SaaS product with paid plans, payment handling is never just about charging a card. You need subscriptions, renewals, upgrades, proration, invoices, webhooks, and failure handling — all working reliably.

In this post, I’ll walk through how to integrate Tap Payments into a Node.js + Express + Sequelize backend for a band-based subscription model, using a real production-style architecture.

This guide focuses on backend design, security, and reliability, not just “making a payment work”.

🧱 Architecture Overview

Our system is built around these core components:

  • Express (TypeScript) — API layer
  • Sequelize — relational data modeling
  • Tap Payments API — card payments
  • Webhook-based state sync — payment truth source
  • Band-based licensing — users pay per seat band

High-level flow:

Client → Backend → Tap Checkout
↘ Webhook → Backend → DB




## 📦 Subscription & Payment Data Model

### Subscription (Account-level)

Each account has **exactly one subscription**:

```

ts
Subscription
- accountId
- bandSize
- subscriptionType (trial | paid | suspended)
- startDate / endDate
- unitPrice
- totalAmount
- status


Enter fullscreen mode Exit fullscreen mode

SubscriptionPayment (Immutable Ledger)

Every charge creates a payment record:


ts
SubscriptionPayment
- subscriptionId
- tapChargeId
- amount
- currency
- paymentMethod (card | invoice)
- status (pending | paid | failed)
- paidAt


Enter fullscreen mode Exit fullscreen mode

Key principle:

Subscription state changes ONLY after payment confirmation (via webhook).


🔐 Tap API Client (Centralized & Secure)

We isolate Tap API communication in a single client:


ts
const tapAxios = axios.create({
  baseURL: "https://api.tap.company/v2",
  headers: {
    Authorization: `Bearer ${process.env.TAP_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
});


Enter fullscreen mode Exit fullscreen mode

This avoids:

  • Token leaks
  • Duplicate logic
  • Inconsistent headers

💳 Creating a Charge (Backend-Controlled)

When a user activates or upgrades a subscription:

  1. Create subscription & pending payment in DB
  2. Create a Tap charge
  3. Redirect user to Tap checkout

ts
export const createTapCharge = async (
  amount: number,
  subscriptionPaymentId: string,
  customer: Customer,
  redirectUrl: string
) => {
  return tapAxios.post("/charges", {
    amount,
    currency: "OMR",
    customer,
    source: { id: "src_card" },
    redirect: { url: redirectUrl },
    metadata: {
      subscriptionPaymentId,
      type: "subscription",
    },
  });
};


Enter fullscreen mode Exit fullscreen mode

Why metadata matters

We store subscriptionPaymentId inside Tap metadata.

This allows:

  • Webhook → DB mapping
  • Idempotent processing
  • No guessing which payment belongs to which subscription

🔁 Redirect-Based Flow (User Experience)

After charge creation, backend returns:


json
{
  "paymentUrl": "https://tap.company/checkout/...",
  "chargeId": "chg_xxx"
}


Enter fullscreen mode Exit fullscreen mode

Frontend simply redirects the user.


🪝 Webhooks: The Source of Truth

Never trust frontend redirects alone.

Why Webhooks?

  • Users may close the tab
  • Network failures happen
  • Redirects can be spoofed

Secure Webhook Verification

Tap signs every webhook payload.


ts
const expectedSignature = crypto
  .createHmac("sha256", TAP_WEBHOOK_SECRET)
  .update(rawBody)
  .digest("hex");


Enter fullscreen mode Exit fullscreen mode

Only verified requests are processed.


🔄 Webhook Processing Logic

On webhook receipt:

  1. Verify signature
  2. Extract subscriptionPaymentId
  3. Update payment status
  4. Update subscription if payment succeeded

ts
if (status === "CAPTURED") {
  await payment.update({ status: "paid", paidAt: new Date() });

  await subscription.update({
    status: "active",
    subscriptionType: "paid",
    endDate: nextYear,
  });
}


Enter fullscreen mode Exit fullscreen mode

Important rule:

Subscription state is updated ONLY inside webhook logic.


📈 Subscription Upgrade with Proration

Upgrades are prorated based on remaining time:


ts
const remainingYears =
  (subscription.endDate.getTime() - Date.now()) /
  (365.25 * 24 * 60 * 60 * 1000);

const proratedAmount =
  additionalUsers * UNIT_PRICE * remainingYears;


Enter fullscreen mode Exit fullscreen mode

This creates:

  • A new pending payment
  • A new Tap charge
  • No disruption to current billing cycle

📉 Downgrades (Deferred)

Downgrades:

  • Apply on next renewal
  • Do NOT refund automatically
  • Prevent abuse near renewal date

ts
if (isWithinLockoutWindow(subscription.endDate)) {
  throw new Error("Cannot downgrade near renewal");
}


Enter fullscreen mode Exit fullscreen mode

🧾 Invoice-Based Billing (Enterprise Ready)

For corporate customers:

  • No Tap charge created
  • Payment marked as invoice
  • Admin manually confirms payment

ts
await SubscriptionPayment.create({
  paymentMethod: "invoice",
  status: "paid",
});


Enter fullscreen mode Exit fullscreen mode

This allows:

  • Offline billing
  • Bank transfers
  • Enterprise workflows

🚫 Automatic Suspension for Non-Payment

A scheduled job checks overdue subscriptions:


ts
if (nextPaymentDue < cutoffDate) {
  await subscription.update({ status: "suspended" });
}


Enter fullscreen mode Exit fullscreen mode

This ensures:

  • Fair enforcement
  • Predictable access control
  • No manual intervention

🔐 Authorization & Safety

Critical actions are protected:

  • Only Account Owners can:

    • Activate plans
    • Upgrade/downgrade
  • All subscription updates use DB transactions

  • Payments are immutable records


✅ Key Takeaways

✔ Backend owns all payment logic
✔ Webhooks are the source of truth
✔ Metadata connects payments to business logic
✔ Subscriptions ≠ payments
✔ Design for upgrades, failures, and audits


🚀 Final Thoughts

Tap Payments integrates cleanly into modern SaaS backends when you treat payments as state machines, not API calls.

If you’re building:

  • Multi-tenant SaaS
  • Seat-based pricing
  • Annual subscriptions
  • Enterprise billing

This architecture will scale with confidence.

Top comments (0)