How Billing Works — What Stripe Handles, and Granting Entitlements by Webhook

This article is part of the IT Foundations course, which builds up from scratch the practical IT knowledge you need at a minimum for programming and vibe coding.
Card data stops at the payment service provider and never passes through your own server. Diagrams show why a webhook notification is what justifies unlocking paid features.

This article covers how billing works.

It splits into two parts: keeping card data from ever reaching your own server, and reflecting the result of a payment in your own app.

Where card data travels, and what justifies granting an entitlement
User enterscard detailsPayment providerreceives itResult arrivesas a notificationCard numberstops heremy-app recordsthe entitlement
The upper branch is the card data, which stops inside the payment service provider. The lower branch is the payment result, and my-app looks only at this to unlock paid features.

The card number stops on the upper branch and never reaches my-app.

The only thing that justifies unlocking paid features is the notification that arrives on the lower branch.

Receiving the card number and exchanging data with the card company both happen on the payment provider's side, and all that is left for my-app is writing the result into a record.

The payment service provider receives the card data, not your own server

Processing a payment requires the card number, expiry date, and security code.

PCI DSS (Payment Card Industry Data Security Standard) is the standard that businesses handling card data must follow, and a payment service provider is an outside service that handles card payments on your behalf.

Stripe and PayPal are the best-known payment service providers.

Where you put the input fields determines where the card number travels.

Where the input fields sitWhere the card number travelsIn scope for PCI DSS
Received and stored by my-appmy-app and the payment providermy-app is in scope too
Received by my-app and forwardedmy-app and the payment providermy-app is in scope too
The payment provider's pageThe payment provider onlymy-app's scope is minimal

Only the third row keeps the card number off my-app's server.

In that case all my-app holds is the value that identifies the payment, while the first two rows leave the card number itself or a record of the traffic behind.

Even if you forward it without storing it, the value still passes through your own server, and the moment it passes through, my-app falls inside PCI DSS scope as well.

To keep the area my-app has to protect as small as possible, you hand the input fields themselves over to the payment service provider.

In a single payment, which data reaches how far
One payment (pay_88)
Stays only inside the payment provider
  • Card number, expiry date, security code
  • Records of the exchange with the card company
Reaches my-app
  • The value that identifies the payment, pay_88
  • The amount, and whether the payment completed
  • Which user the payment belongs to (u_1024)
The outer box is one payment, pay_88. The card data stays only inside the upper box. All my-app holds is the fact that the payment completed and the value that identifies it.

The card number never passes through my-app

If you never receive the card number yourself, the responsibility for protecting it never lands on your side either.

Whether you store it or forward it straight on, the same responsibility applies once it passes through your own server, so you hand the input fields themselves to the payment service provider and let only the fact that the payment completed reach my-app.

The payment finishes on a checkout page on the payment provider's domain

A hosted checkout page puts the entry screen itself somewhere else so that card data never passes through your own server: it is a payment screen the payment service provider prepares and shows on its own domain.

Moving the user to a page on a different domain is a redirect.

In the second stage you pass the amount and the return URL, and the payment service provider returns the URL of a checkout page dedicated to that payment.

This exchange happens by calling a web API, and you attach an API key.

The two domains the browser has open during a payment
User's browser
my-app's domain
  • The paid plan sign-up page
  • /thanks, opened after the payment
The payment provider's domain
  • The checkout page where the card number is entered
  • my-app cannot see what is inside this page
The outer box is the user's browser. The upper box is my-app's page and the lower box is the payment provider's page; a redirect moves from the upper one to the lower one and back again.

The checkout page is also offered as a frame placed inside my-app's own page.

It looks different, but the values entered go to the same place.

Two ways of placing it: what changes and what does not
Moving to aseparate pageURL is theprovider's domainAfter payment,back to /thanksPlacing a framein the pageURL is my-app'sdomainAfter payment,same page staysEither wayCard number goesto the providermy-app only getsthe payment fact
The top two rows are the differences between the two placements, and the bottom row is what stays the same either way. From left to right: the placement, the URL shown in the browser, and what happens after the payment.

The payment service provider provides the checkout page

The page where the card number is entered belongs to the payment service provider, not to my-app.

A redirect moves the user to that page, and after the payment they come back to /thanks; even if you place it as a frame in your own page, the values entered still go to the same place.

An entitlement is granted on the webhook notification, not on the page the user returns to

my-app, not the payment service provider, decides whether paid features may be shown, and that record is the entitlement — a record of the range of features a user who has paid can use.

The user's browser landing back on /thanks does not by itself tell my-app whether the payment completed.

Look at what happens after a payment as two separate paths.

Two paths toward my-app after a payment finishes
Browser movesto /thanksCompletion showson the pageIf closed,nothing arrivesPayment ends onthe checkout pageServer calls/webhookNotification witha signatureRetried for aset period
Two paths branch out from a single payment. The upper one runs through the browser and can stop partway; the lower one runs server to server and continues until it gets a success back.

The upper path stops right there if the user closes the browser just after paying.

The lower path runs server to server, so if the notification does not arrive it is retried for a set number of times over a set period.

Depending on which one you treat as the justification, the same three people end up with different entitlements.

What happened to that userIf you judge by /thanksIf you judge by /webhook
Paid and opened /thanksEntitlement grantedEntitlement granted
Paid, then closed the browser at onceNo entitlement grantedEntitlement granted
Opened /thanks without payingEntitlement grantedNo entitlement granted

If you judge by /thanks, the person who closed the browser gets nothing while the person who never paid gets an entitlement.

The only thing you use to justify granting an entitlement is the notification on the lower path.

In payments, applying the same notification twice extends the usable period twice over.

Record the value that identifies each notification, and leave the entitlement unchanged when the same notification arrives a second time.

How my-app sorts what arrives at /webhook
Notification witha bad signatureFirst arrivalof evt_7Resentevt_7/webhookreceives itDiscard it; theentitlement staysGrant it andrecord evt_7Already recorded,so do nothing
All three on the left arrive at the same /webhook. They are received together in the middle, and the signature and the value that identifies the notification split them into the three on the right.

A notification whose signature does not match is discarded without changing the entitlement.

The only difference between the remaining two is whether that notification has already been applied.

Three kinds of records kept in my-app's database
my-app's database
User record
  • The value that identifies the user, u_1024
  • Password hash
Entitlement record
  • Plan type (paid)
  • Expiry date
  • Which user it belongs to (u_1024)
Record of the notifications received
  • The value that identifies the notification, evt_7
  • When it was applied
The outer box is my-app's database. The entitlement record in the middle holds the plan type and the date access expires. The notification record at the bottom is what keeps a resent notification from being applied twice.

Even if you hide the paid button on the page, that code still reaches the user's device.

Checking the entitlement on the server every time is the same authorization covered in How Login Works.

The card number stops inside the middle box and never enters the box on the right.

The entitlement is granted when the notification on arrow 2 arrives, not when the user's browser comes back.

A notification from the server reports the payment

A completed payment is reported by the notification arriving from the payment service provider's server, not by the page the user comes back to.

You verify the signature on what arrives, leave the entitlement unchanged if the notification has already been applied, write the result into your own database, and decide from it whether to show paid features.

With a subscription, a notification arrives each period and the entitlement changes

A contract charged monthly or yearly is a subscription — a contract in which payment repeats automatically at a set interval.

Between a one-time payment and a subscription, what changes is how many notifications arrive
Payment typeOne-time paymentJust one notificationExpiry date doesnot changeSubscriptionArrives againevery periodExpiry extends tothe next payment
The left column is the type of payment. The upper path is one-time, the lower path is a subscription. Either way the webhook notification is what justifies the entitlement; what differs is how many arrive and the expiry.

A repeating payment does not succeed every time.

On the failure row, the payment service provider waits a few days and retries the charge.

my-app decides whether to leave access in place during that time or cut it off right away.

Test mode is what lets you check things before going live: a state the payment service provider prepares for checking behavior without creating a real charge.

The API keys are separate for test and production, so you switch between them with the value of an environment variable.

Decide the notification that removes an entitlement before you go live

With a subscription a payment happens every time the period comes around, and a notification arrives each time.

If you handle only the notification that grants access, users whose payments have stopped keep their access, so decide granting, extending, and removing for each notification that arrives, and run through the whole flow in test mode before going live.

QUIZ

Knowledge Check

Answer each question one by one.

Q1When you take payments through a payment service provider, where does the card data travel?

Q2Which of these do you rely on to decide that a payment completed and grant the user an entitlement?

Q3When a notification reporting a failed payment arrives for a recurring charge, which of these does your app do?