Another Stripe Integration Didn't Make Sense
Today I built the first working version of pay.1j2.com, a central payment service designed to sit between my websites and Stripe.
The idea came from looking at the different projects I'm building and realising that several of them will eventually need to accept payments.
Loyalty needs recurring subscriptions for businesses upgrading to Loyalty Pro.
Cock Your Hat may eventually sell physical products I've created.
My smaller websites and mini-sites may need simple Buy Now buttons, support links or one-off payments.
I also have existing hosting customers who pay for recurring services.
I could have integrated Stripe separately into every website, but that would mean repeating the same work each time.
Every project would need its own checkout code, Stripe configuration, webhook handling, payment records, subscription logic and security checks.
That felt inefficient.
Instead, I decided to build the payment integration once and place it behind a central domain.
pay.1j2.com.
What 1J2 Pay Actually Does
1J2 Pay isn't really a shop, a shopping basket or a complete payment platform.
It's more like a secure handshake between one of my websites and Stripe.
A website tells 1J2 Pay what the customer is buying.
1J2 Pay validates the request, creates the Stripe Checkout Session, sends the customer to Stripe, waits for Stripe to confirm the result and then reports that result back to the original website.
Each system has a clear responsibility.
- The originating website remains responsible for the product, customer and business logic.
- Stripe remains responsible for collecting payment details and processing the payment.
- 1J2 Pay sits between them and connects everything securely.
That separation turned out to be one of the most important decisions in the project.
Why I Wanted a Central Payment Service
The biggest reason was reuse.
I already knew Loyalty needed Stripe, and I'd been thinking about adding product sales to Cock Your Hat. I could also imagine adding payments to several other websites later.
Without a central system, every new idea would require another complete Stripe integration.
With 1J2 Pay, each website only needs to create a signed request containing the relevant checkout information. The central service handles the rest.
That gives me one place to maintain:
- Stripe API keys
- Checkout creation
- Payment logging
- Subscription handling
- Webhook verification
- Duplicate event protection
- Callbacks
- Security checks
- Error reporting
- Future payment features
Adding payments to a future project should now be much quicker.
The website won't need to understand every part of Stripe. It will simply send a secure request to pay.1j2.com and wait for the confirmed result.
The Basic Payment Flow
The process now works like this:
- A customer clicks a payment or upgrade button on one of my websites.
- The website's server creates a payment request.
- The request is signed using a private secret shared between that website and 1J2 Pay.
- The request is sent to pay.1j2.com.
- 1J2 Pay verifies the signature and creates a Stripe Checkout Session.
- The customer is redirected to Stripe's hosted Checkout page.
- Stripe processes the payment.
- Stripe sends a webhook to 1J2 Pay confirming what happened.
- 1J2 Pay updates its payment record.
- 1J2 Pay sends a signed callback to the original website.
- The original website updates its own records.
For Loyalty, that means a business can click Upgrade to Pro, complete the Stripe Checkout process and have its account automatically changed from Free to Pro.
The important part is that Loyalty doesn't trust the browser redirect alone.
A customer could complete the payment and close the page before Stripe redirects them back. The reliable confirmation comes directly from Stripe through the webhook.
Why I Chose Stripe Checkout
I chose Stripe Checkout rather than building my own card form.
That keeps the payment process secure and means I never need to handle card details directly.
Stripe already provides much of the functionality I might need:
- Card payment collection
- Apple Pay and Google Pay where supported
- Recurring subscriptions
- One-off payments
- Billing information
- Shipping address collection
- Multiple line items
- Adjustable quantities
- Promotion codes
- Receipts
- Customer portal support
- Payment authentication
- Mobile-friendly checkout
That allows 1J2 Pay to remain relatively small.
It creates the Checkout Session and sends the customer to Stripe. Stripe handles the sensitive part.
The current Loyalty Pro integration uses a recurring Stripe Price ID stored in Loyalty's plan table. Loyalty sends that Price ID to 1J2 Pay, which then creates the subscription Checkout Session.
A website selling physical products could instead send line items containing the product name, description, image, price and quantity.
The same central endpoint can therefore support both software subscriptions and ordinary product purchases.
Keeping the Important Files Private
The Stripe integration itself was straightforward.
I connected to the server through SSH and installed Stripe's official PHP library using Composer.
Composer created the usual configuration files and vendor directory.
Initially, the vendor directory was inside the public web directory. I moved Composer and the vendor files one level above public_html so they couldn't be accessed directly through the web server.
The private configuration also sits outside the public directory.
That includes:
- The Stripe secret key
- The Stripe webhook secret
- The database password
- The client secrets used by connected websites
The main bootstrap file also sits outside public_html. It loads Composer, reads the private configuration, connects to the database and creates the Stripe client.
Only the files that genuinely need to be accessed through the web are publicly available.
A Deliberately Small Database
I kept the database deliberately small.
1J2 Pay doesn't store a central product catalogue, and it doesn't try to become the source of truth for everything sold across my websites.
The products remain on the website that owns them.
The central payment system currently stores two main types of records.
Payment Transactions
The transaction record includes details such as:
- The internal payment reference
- The website that created the request
- The website's own reference
- Whether the checkout is a payment or subscription
- The original request
- The Stripe Checkout Session ID
- The Stripe Payment Intent ID
- The Stripe Subscription ID
- The Stripe Customer ID
- The amount and currency
- The payment status
- The callback URL and result
- Errors and timestamps
Stripe Events
The second table stores Stripe webhook events.
Stripe may retry an event, so each event has a unique Stripe event ID. If the same event arrives again, the database prevents it from being processed twice.
The database is therefore more of an audit trail and delivery record than a product database.
The website that initiated the payment still owns the customer, order, subscription or account information.
Signing Every Request
Security was one of the most important parts of the design.
I didn't want a customer's browser sending an amount directly to 1J2 Pay without any way to confirm it was genuine. A request created in the browser could potentially be altered before it reached the payment service.
Instead, the payment requests are created server to server.
Each website has its own private client secret.
The calling website creates the JSON payload, adds a timestamp and signs the exact request body using HMAC-SHA256.
1J2 Pay calculates the same signature using its copy of that website's client secret.
If the signatures match, the request is authentic.
If any part of the payload has been changed, the signature no longer matches and the request is rejected.
The timestamp also allows 1J2 Pay to reject requests that are too old, reducing the risk of someone capturing and replaying a valid request later.
Each client will eventually have its own secret. If one smaller website were ever compromised, I could replace only that website's secret rather than changing every integration.
Preventing Duplicate Checkouts
Payment requests can occasionally be repeated because of network errors, double clicks or automatic retries.
To protect against duplicate Checkout Sessions, 1J2 Pay uses both its own unique client references and Stripe idempotency keys.
The client reference identifies the individual checkout attempt.
Loyalty includes the business ID in that reference, along with a unique suffix so the business can try again after cancelling or failing a previous checkout.
Stripe also receives an idempotency key based on the client, reference and unique value attached to the request.
That helps prevent the same request from accidentally creating multiple Checkout Sessions.
Listening for Stripe Webhooks
Once the Checkout Session has been created, the webhook becomes the most important part of the system.
Stripe sends payment and subscription events directly to 1J2 Pay.
The webhook verifies Stripe's signature using the webhook signing secret before trusting the event.
It currently handles events relating to:
- Completed checkouts
- Successful delayed payments
- Failed delayed payments
- Expired checkouts
- Successful Payment Intents
- Failed Payment Intents
- Created subscriptions
- Updated subscriptions
- Deleted subscriptions
The event is recorded before it is processed.
When a successful payment arrives, the payment record is updated with the final Stripe IDs, amount, currency and status.
For subscriptions, the system can also track changes such as active, trialling, past due, unpaid or cancelled.
Sending the Result Back
Once 1J2 Pay has processed the Stripe event, it sends the result back to the website that started the payment.
This callback is also signed using HMAC-SHA256.
It includes information such as:
- The Stripe event ID and event type
- The website's original reference
- The 1J2 Pay payment reference
- The payment mode and status
- The amount and currency
- The Stripe Checkout Session ID
- The Payment Intent ID
- The Subscription ID
- The Stripe Customer ID
The receiving website verifies the signature before trusting the callback.
It also stores the Stripe event ID so the same callback can't be processed twice.
Loyalty Became the First Real Client
Loyalty is the first project using 1J2 Pay.
It has a Free plan and a recurring Pro subscription.
The Pro plan is stored in the Loyalty database with its Stripe Product ID and monthly and yearly Price IDs.
When a business clicks Upgrade to Pro, Loyalty:
- Checks that the user is logged in.
- Finds the active business.
- Checks that the business is still on the Free plan.
- Loads the correct Stripe Price ID.
- Creates a unique client reference.
- Signs the payment request.
- Sends it to 1J2 Pay.
- Redirects the business owner to Stripe Checkout.
After the payment, Stripe notifies 1J2 Pay.
1J2 Pay sends the signed callback to Loyalty.
Loyalty verifies it, stores the Stripe subscription details and changes the business plan to Pro.
I tested the complete process using Sahara Sands as the business.
The test worked from beginning to end.
- Loyalty created the payment request.
- 1J2 Pay created the Stripe Checkout Session.
- Stripe accepted the test payment.
- Stripe sent the webhook.
- 1J2 Pay updated the payment record.
- 1J2 Pay sent the signed callback.
- Loyalty upgraded Sahara Sands to Pro.
Seeing the entire chain work was extremely satisfying.
Keeping the Customer Experience Almost Invisible
I've also thought about how 1J2 Pay should look if it ever needs customer-facing pages.
The aim is for it to feel almost invisible.
It shouldn't look like another complicated website the customer has suddenly been sent to.
Any interface should be extremely clean, with:
- A white background
- Very light grey borders
- A small 1J2 logo
- The relevant website or product branding
- Simple product information
- One clear action
- No navigation
- No distracting marketing content
Stripe Checkout already has a similarly clean appearance, so the transition should feel natural.
I've added the 1J2 branding to Stripe and used a darker version of the main purple for payment buttons so they remain clear and readable.
Where I Could Use It Next
Loyalty Pro is only the first use for 1J2 Pay.
Software Subscriptions
Any future software product could send its subscription request through the same service.
1J2 Pay would handle the Stripe Checkout Session, webhook events, subscription changes and callback delivery.
Physical Products
Cock Your Hat may eventually sell products I've created.
A product page could send one or more line items to 1J2 Pay. Stripe Checkout could display the products, collect the delivery address and process the payment.
The original website would then receive the confirmed result and create or update its own order.
Mini-Sites
I have several smaller websites and ideas that may eventually need payments.
Adding payments to each one could now be as simple as creating a signed payload and sending it to the central endpoint.
Support Links
I could also create simple support links for blog posts or projects.
These could be labelled Support 1J2 or Buy Me a Coffee rather than presenting them as charitable donations.
The request could include the originating website or article, allowing me to see which content generated the support.
Hosting and Recurring Services
I already have customers paying for hosting and ongoing services.
A central payment system could eventually create subscriptions, payment links and reminders for those customers too.
The Possible Connection With accounts.1j2.com
This also connects naturally with another idea I've been considering: accounts.1j2.com.
I currently use QuickFile for accounting, and I wouldn't immediately try to replace every part of professional accounting software. That could easily become an enormous project.
A more sensible first version would focus on the parts specific to my own business:
- Customers
- Websites
- Hosting plans
- Recurring services
- Invoices
- Payment links
- Subscriptions
- Payment status
- Outstanding balances
- Renewal reminders
- Income records
- Expenses
- Stripe fees
- Basic reporting
In that structure, accounts.1j2.com would know who owes what and why.
pay.1j2.com would collect the money.
Stripe would process the actual payment.
Each system would have one clear responsibility.
Eventually, that could bring Loyalty subscriptions, product sales, hosting payments and income from other projects into one central place.
The Biggest Lesson From Building It
The main lesson from today is that centralising something doesn't always mean building a huge platform.
When I first pictured a central payment system, I imagined products, shopping baskets, customers and a large administration dashboard.
What I actually needed was much smaller.
I needed a secure intermediary.
The originating website already knows what's being sold.
Stripe already knows how to collect the money.
1J2 Pay only needs to connect them reliably.
That smaller definition made the project achievable in a day rather than allowing it to turn into another enormous software product.
Stripe's PHP library was straightforward to install and use. Stripe Checkout removed the need to build my own payment interface. Webhooks provided reliable confirmation, while signed requests and callbacks created a secure connection between my systems.
What I'm Focusing on Next
There are still a few things to finish before switching everything fully live.
I need to add the final live API keys, webhook secrets and Price IDs, followed by a genuine low-risk payment test.
But the core architecture is now working.
I have a reusable payment layer that can sit behind Loyalty, Cock Your Hat, my mini-sites, support links, physical products, software subscriptions and potentially a future 1J2 accounts system.
Instead of integrating Stripe from scratch every time I build something new, I can connect each project to the same central service.
That was the real goal.