Skip to main content
The demo on this page is a Notes app with a Pro plan. A signed-in user clicks Upgrade to Pro, pays on the checkout, and comes back with Pro unlocked. The home page then shows the plan and its next charge, a Manage plan section to cancel or upgrade, and My invoices with the invoice PDF and the receipt for each payment. Every call goes to the AgentaOS API through the @agentaos/pay SDK, from the app’s server.
The Notes demo signed in as Grace: Your plan Pro Plus, active, Renews on Oct 29, 2026: €60.76 per month, a Cancel plan button, and My invoices with INV-2026-0006, Sep 29, 2026, €60.76, paid, and the links Invoice PDF and Receipt.

The demo after a paid checkout: the plan, its renewal, Manage plan, and My invoices.

The demo app

The app is in examples/paywall-demo in the AgentaOS repository. It uses Node.js, Express, and plain HTML. The login is fake (you pick a user) and the database is a JSON file, so every other line is the integration itself.

Run it

You need Node.js 20 or later, pnpm, and an AgentaOS account in test mode.
1

Install

Clone the AgentaOS repository, then install and build the SDK from its root:
2

Create the plan

In the dashboard, create a monthly subscription plan. The steps and screens are in Sell a subscription. On the plan’s page, open Building this into your site? and click Copy ID. For the upgrade button, create a second plan in the same currency and with the same interval, and copy its ID too.
3

Fill in the settings

In .env, set AGENTAOS_API_KEY to a test key: in the dashboard, open Developers → API keys in the sidebar and click Generate Test Key. Set PRO_LINK_ID and PRO_PLUS_LINK_ID to the two plan IDs. Set AGENTAOS_WEBHOOK_SECRET to the signing secret from Settings → Developers → Webhooks, Reveal signing secret.
4

Make a local certificate

The checkout sends the buyer back only to an https address, and https://localhost is refused. The demo therefore runs on https://127.0.0.1:4567 with a self-signed certificate:
5

Start the app

Open https://127.0.0.1:4567 and accept the browser’s certificate warning once. Sign in as Ada. The plan says Free.
The Notes demo signed in as Ada: Your plan Free, Pro unlocks unlimited notes and sharing, and an Upgrade to Pro button.
6

Upgrade and pay

Click Upgrade to Pro. The checkout opens with Ada’s email already filled in. Pick a country and click Continue to payment. Pay with the test card 4242 4242 4242 4242, any future expiry date, and any CVC.
The checkout for Pro: €29.00, DUE TODAY €0.00, Then €29.00/mo after your 14-day free trial, the email ada@example.com Set by AgencyOne, Country Germany, and a Continue to payment button.

The checkout for a plan with a 14-day free trial: nothing is due today.

After payment, the checkout sends you to /success?sessionId=.... The page reads the checkout from the API. While the checkout is still open, it says “Still processing” with a Refresh button. Once the checkout is completed, it says “You are on Pro”.
The success page: You are on Pro. Thanks. Your payment is confirmed and Pro is unlocked. A Go to Notes button.
Back on the home page, the plan shows the status and the date of the first charge.
The Notes demo for Ada after the checkout: Your plan Pro, trialing, Free trial. First charge on Oct 13, 2026: €35.96 per month. Buttons Upgrade to Pro Plus and Cancel plan. My invoices: No invoices yet.

How it works

The checkout carries your user ID

When the user clicks Upgrade to Pro, the server creates a checkout from the plan’s link ID. It puts the user’s own ID in metadata, so the success page and the webhook can tell who paid. Do not send signed-in users the plan’s shared link: that link carries no metadata.
src/checkout.ts
The SDK sends an idempotency key with every create call and reuses it when it retries after a network error or a server error, so one call makes one checkout. The success URL must start with https://. The checkout adds ?sessionId= to it when it sends the buyer back.

The success page asks the API

Anyone can open the success URL, so the page never unlocks from the redirect alone. It reads the checkout with checkouts.retrieve, checks that the checkout belongs to the signed-in user, and unlocks Pro only when the status is completed.
src/checkout.ts
metadata.subscriptionId is the ID of the new subscription. grantPro stores it on the user, because it is the only link between later events and the user. In the demo run, the checkout for the paid plan was completed at the first load of the success page. The checkout for the plan with a free trial was open at first and became completed after a few refreshes.

The webhook unlocks Pro and matches renewals

The webhook unlocks the user even when they close the tab before the success page loads. It verifies the signature over the raw body, so the route is mounted with express.raw() before any body parser. See Receive webhooks in Express.
src/webhooks.ts
A renewal’s checkout.session.completed has its own sessionId and does not carry your customerId, so the stored subscription ID is the only way to find the user. webhooks.verify() returns every key in camelCase, including the keys inside metadata. The route answers 400 to a bad signature and 200 to every verified event. AgentaOS does not deliver webhooks to a private address such as 127.0.0.1, so the demo on your machine receives no events. The success page unlocks Pro without them. To test the route locally, run pnpm webhook:test: it signs sample events with AGENTAOS_WEBHOOK_SECRET and posts them to /webhooks.
To receive real events, expose the app through a public HTTPS tunnel, enter its /webhooks URL as the Webhook URL in Settings → Developers → Webhooks, and click Send test event.

Each payment unlocks once

The same event can arrive more than once, and the success page and the webhook both report the first payment. grantPro and recordRenewal record each sessionId and do nothing for a session they have already seen. In the demo run, the webhook replay of Grace’s first payment logged already handled. In your database, a unique constraint on the session ID does the same when two requests arrive at the same moment.
src/grant.ts

Free trials

With a free trial, the buyer pays nothing at checkout and the subscription starts as trialing. The webhook still receives checkout.session.completed, with amount "0" and metadata.trial set to true: treat it as a trial start, not as money received. The checkout that checkouts.retrieve returns has no trial key in its metadata, so read the trial state from the subscription’s status. The first charge happens when the trial ends. See Free trials.

The access check reads the subscription

Before the Pro page, requirePro reads the user’s subscription by the stored ID and allows active or trialing. subscriptions.list() does not return your metadata, so the stored ID is the lookup key.
src/access.ts
The home page reads the same subscription for its plan card:
  • currentPeriodEnd: the end of the current period and the date of the next charge.
  • cancelAtPeriodEnd: true after the user cancels. Access ends at effectiveCancelDate.
  • planName and unitAmountMinor: the plan and the charge per period in integer minor units, tax included. For the €29.00 plan, the demo run returned 3596 (€35.96).
  • pendingPlanChange: set when a downgrade waits for the period end.

The user’s invoices, with PDF and receipt

subscriptions.invoices(id) returns the invoice of each billing cycle of one subscription, newest first. Its amount is in currency units, not minor units: 60.76 for Grace’s first payment. The invoice list has no filter by buyer, so the subscription ID is the key here too. invoices.downloadPdf(id) and invoices.getReceipt(id) return the PDF as a Buffer, not a URL. The demo’s server checks that the invoice belongs to the user’s subscription, then sends the bytes. An invoice ID from another user gets a 404.
src/invoices.ts
A subscription in its free trial has no invoice yet. The first one comes with the first charge, when the trial ends. In test mode the PDF is marked “TEST MODE: not a tax invoice” and its number starts with TEST-.

Cancel and upgrade

Cancel plan calls subscriptions.cancel. The subscription ends at the end of the current period, nothing is refunded, and the user keeps Pro until then. The server rejects an immediate cancellation.
src/plan.ts
The Notes demo for Grace after Cancel plan: Pro Plus, active, Pro ends on Oct 29, 2026. You keep it until then. Manage plan: Cancellation scheduled.
Upgrade to Pro Plus quotes the change with previewPlanChange and shows the amounts. Confirm upgrade applies it with the quote’s prorationDate, so the charge equals the quote.
src/plan.ts
src/plan.ts
The quote’s direction is upgrade, downgrade, or revert. An upgrade charges the prorated difference now and applies at once. A downgrade charges nothing and starts at the end of the period. A revert cancels a pending downgrade. During a free trial, the upgrade quote is €0.00 due today. See Upgrade and downgrade.

Take it to production

  • Login: replace the demo’s cookie login in src/session.ts with your own authentication.
  • Database: replace the JSON file in src/store.ts with tables. Put a unique constraint on the processed session ID.
  • Webhook: register https://yourapp.com/webhooks in the dashboard and set its signing secret in AGENTAOS_WEBHOOK_SECRET.
  • Live key: use your live key (sk_live_...) on the live server. Keep it out of the browser and out of git.
  • Test and live: every event carries livemode. Ignore events with livemode: false on the live server.
  • Address: set APP_URL to your public https address, and remove TLS_CERT and TLS_KEY when a proxy handles TLS in front of the app.
To keep the user on your site, you can embed the checkout instead of redirecting: create the checkout on your server as above and pass its sessionId to the embedded checkout.

Next steps

Receive webhooks in Express

Register the endpoint and verify every event.

Webhook events

Every event and its fields.

Subscriptions

Statuses, trials, plan changes, and cancellation.

Go live

Verify your business and take real payments.