Production IntegrationThe Observatory (Lunar Cow Publishing)B2B SaaS / Ad ManagementFebruary 8, 2025

Multi-Source Payment Processing with Intelligent Webhook Routing

How I built a sophisticated payment system for The Observatory platform that handles both external contract purchases and in-app revision payments, featuring metadata-driven routing, client-side polling, and database-backed revision accounting.

Observatory platform revision purchase flow

Project Overview

The Observatory manages the complete lifecycle of chamber of commerce advertising contracts, from initial purchase through design approval. The platform required a payment integration to handle two distinct revenue streams: initial ad contracts purchased via an external Rate Card Landing Page, and in-app revision purchases ($150 for 2 additional revisions) for advertisers needing to exceed their included revision limit.

2 Systems
Payment Sources
Observatory + Rate Card Landing Page
$150
Revision Price
2 additional revisions per purchase
Live
Production Uptime
Handling real customer payments
Payment Intents
Integration Type
With webhook event handling

Background & Context

The Observatory operates a hybrid payment model: (1) Initial ad contract sales processed through an external Rate Card Landing Page system, creating pending contracts and triggering user onboarding, and (2) In-app revision purchases enabling advertisers to exceed their 3 included revisions by purchasing additional revision credits within the platform.

Timeline
3 weeks
Team Size
1 developer
Tech Stack
Next.js 14TypeScriptStripe Payment Intents APIStripe ElementsSupabase (PostgreSQL)AWS S3React Context APISWR (Data Fetching)

Key Challenges

Multi-Source Payment Attribution

Challenge

The platform receives payments from two distinct sources: Observatory in-app revision purchases (with contractId metadata) and external Rate Card Landing Page purchases (with advertiserId, sizeId, publicationId metadata). A single webhook endpoint needed to intelligently route these payments to different processing logic without database lookups.

Solution

Implemented metadata-based routing in the webhook handler. Observatory purchases include contractId in metadata and trigger revision credit insertion. RLP purchases include advertiser/size/publication metadata and are simply acknowledged (processing handled by external system). This elegant solution eliminates the need for separate webhook endpoints, multiple Stripe accounts, or complex routing logic.

Webhook Delay User Experience

Challenge

Users completing payment see success from Stripe immediately, but the webhook can take 1-5 seconds to arrive and process. During this delay, users would see stale data (no new revisions) and the 'Purchase Revisions' button remained disabled, creating confusion about whether payment succeeded.

Solution

Built a sophisticated client-side polling mechanism with PendingPaymentModal. When users return from Stripe redirect, the component detects the payment_intent_client_secret query parameter, verifies payment success, then polls the database every 6 seconds until revision count increases. A loading modal provides clear feedback during the wait, creating a smooth UX despite webhook latency.

Revision Accounting System

Challenge

The platform needed to track available revisions across multiple purchases per contract. Formula: Available = 3 (base) + Σ(purchased) - Σ(requested). This required distinguishing between revision purchases (adds credits) and revision requests (consumes credits) while handling edge cases like multiple rapid purchases and potential webhook failures.

Solution

Implemented a database-backed accounting system with contract_revisions table storing each purchase (payment_id for idempotency, count always 2). Custom React hooks calculate available revisions: useNumRevisionsRemaining queries contract_revisions for total purchased, useNumRevisionsRequested counts ad_artwork submissions marked as revisions, then computes the difference. Additive counting prevents duplicate processing if webhooks fire multiple times.

Idempotent Webhook Processing

Challenge

Stripe webhooks can be delivered multiple times or out of order. Without idempotency protection, duplicate webhook events could credit the same payment twice, giving customers free revisions. The system needed to gracefully handle retries and ensure exactly-once processing semantics.

Solution

Added unique constraint on contract_revisions.payment_id (Stripe PaymentIntent ID). First webhook processes successfully, subsequent deliveries fail the database insert due to unique constraint violation, naturally providing idempotency. Webhook handler returns 200 regardless (preventing Stripe retry storms) and logs the duplicate attempt for monitoring.

Technical Approach

The implementation uses Stripe Payment Intents API for flexible payment handling with strong 3D Secure support. Architecture separates concerns: BuyRevisionModal creates Payment Intent on modal open, CheckoutForm handles card collection and payment confirmation via Stripe Elements, PendingPaymentModal manages post-payment polling, and webhook handlers perform server-side verification and database updates. The pending payment pattern handles the asynchronous nature of webhooks gracefully.

Payment Intent Creation with Metadata

API route creates $150 Payment Intent with contractId in metadata. Query parameter approach simplifies implementation and enables caching. Metadata serves as single source of truth for webhook routing, eliminating need for database lookups during webhook processing.

Embedded Checkout with Stripe Elements

Custom CheckoutForm component integrates Stripe PaymentElement with modal UI. Uses return_url strategy pointing to same page to preserve context after 3D Secure redirects. Type-specific error handling provides clear user feedback for card errors vs. validation errors vs. unexpected failures.

Client-Side Polling Mechanism

PendingPaymentModal detects return from Stripe via payment_intent_client_secret query parameter, retrieves payment status, then polls database every 6 seconds using SWR mutate() until revision count increases. Interval chosen to balance responsiveness (typical webhook arrival 1-3s) with server load.

Metadata-Based Webhook Routing

Single webhook endpoint handles multiple payment sources. Checks metadata for contractId (Observatory) vs. advertiserId/sizeId/publicationId (RLP). Routes to appropriate processing logic or gracefully acknowledges. Returns 400 on missing/invalid metadata to trigger Stripe retry for transient errors.

Revision Accounting Hooks

React hooks encapsulate business logic: useNumRevisionsRemaining calculates available revisions from database state, useNumRevisionsRequested counts submitted artwork marked as revisions. Hooks use useMemo for performance and automatically trigger UI updates when underlying data changes via SWR revalidation.

Database-Backed Idempotency

contract_revisions table with unique constraint on payment_id ensures exactly-once processing. Atomic operations prevent race conditions. Additive counting (sum all purchases) handles multiple rapid purchases gracefully without complex locking.

Results & Impact

The payment system successfully launched in production and processes real customer transactions with zero manual intervention. The multi-source routing architecture enables seamless integration between external contract sales and in-app revision purchases. Client-side polling creates a polished user experience despite webhook latency. The revision accounting system handles edge cases gracefully, and idempotency protection prevents duplicate crediting. Most importantly, the architecture scales effortlessly to handle growth from dozens to thousands of monthly transactions.

<200ms
Webhook Processing
Average processing time
~6 seconds
Payment Confirmation
Average user wait time
100%
Idempotency
Via database constraints
Zero
Manual Intervention
Fully automated workflow

The revision payment system just works. Customers can purchase additional revisions instantly, and we never have to think about it. The integration handles everything automatically.

Ed Johnsen
President, Lunar Cow Publishing

Key Takeaways

  • Stripe metadata is incredibly powerful for routing multi-source payments without database lookups
  • Client-side polling is simpler than WebSockets for low-frequency async events like webhook processing
  • Payment Intent API provides superior flexibility over legacy Charges API for handling authentication flows
  • Always disable Next.js body parsing for webhook routes to enable signature verification with raw body
  • Database unique constraints provide elegant idempotency protection without complex application logic
  • Webhook delays are real-always show loading state to users after payment confirmation
  • Test webhook signature verification locally using stripe CLI: stripe listen --forward-to localhost:3000
  • Type-safe server actions (TypeScript + Next.js) prevent bugs in payment amount calculations and metadata handling
  • SWR cache invalidation with mutate() works beautifully for polling patterns in React
  • Additive accounting (sum all transactions) is more robust than maintaining running balance counters

Related Services

Interested in similar work for your project?

Ben Harris, CEO of Lunar Cow Publishing

"I highly recommend Dean to anyone looking for a reliable and professional partner for their software development needs."

Ben Harris - CEO of Lunar Cow Publishing

Need similar work for your project?

Book a free consultation to discuss how we can help you achieve results like these.