Abstract

A shop needs five things: products, a cart that people add to, a checkout that takes money, a record that the money arrived, and an order that lists what was bought. This case study builds all five on nostackai with no servers of your own. A cart is built by a Logic Block. A short Glyph starts a PayPal checkout from that cart. PayPal's own notifications, called webhooks, create the paid order, and an automation copies the cart's lines onto the order as order items.

You will create six records, three Logic Blocks, two automations and one Glyph, set up the PayPal connector once, and test every step from the Playground. Everything except the Glyph imports from a single file. The screens are from a real run in a fresh organization, in PayPal's sandbox, so no real money moves.

Architecture diagram of the shop: storefront, add-to-cart Logic Block, checkout Glyph, PayPal connector, webhook landing, and the capture and order-items automations.
How the shop fits together: the storefront builds a cart, a Glyph starts the PayPal checkout, and PayPal's webhooks create the approval, the paid order and its items.

Who this is for

Anyone who wants to run online sales on nostackai: a developer building a storefront, an agency delivering one, or a founder deciding whether the platform covers checkout. You do not need to write code to follow along; the one Glyph is ten lines of Python you paste.

What you need

  • A nostackai organization with one tenant, and org-admin access to its console.
  • A PayPal developer account with a Merchant sandbox app and a Personal sandbox buyer account. Payment connectors walks through creating both.
  • The organization's Allowed Domains must include the site that will call your app (here, the Playground), and the PayPal API domains the connector card lists.
  • About 45 minutes.

The scenario and what it must do

A small outdoor-gear shop, Trailhead Outfitters, sells three products. A customer adds products to a cart, can add more of the same one, and pays once for the whole cart. When the payment is complete the shop must have an order that says what was paid, when and how much, one order item for each line of the cart, and a cart that is closed so it cannot be paid twice.

RequirementHow it is met
Customers add products and change quantitiesThe add-to-cart Logic Block opens a cart, adds lines, and recomputes the totals from the lines
One payment for the whole cartA Glyph builds the PayPal items from the cart's own lines and starts a PayPal cart checkout
Prices cannot be changed by the customerThe checkout takes only a cart id; items and total are built on the server
Know when it is paidPayPal's webhooks land as records in your own data
An order and its itemsAn automation on the new order copies the cart lines into order items and closes the cart

The data model

Six records hold the whole shop. They are ordinary entities you define once; nothing about them is built into the platform, so you can add fields (a shipping address, a coupon) the same way.

Entity relationship diagram: cart_line points to cart and product; shop_order and payment_approval point to cart; shop_order_item points to shop_order.
The shop's six records and how they link. Green fields hold the id of another record.
RecordHoldsCreated by
productWhat you sell: title, description, price, activeYou (or an import)
cartA shopper's open basket: status, currency, item count, subtotal, owneradd-to-cart
cart_lineOne product in a cart: quantity, the unit price at the time, the line totaladd-to-cart
payment_approvalPayPal reported the customer approved: the PayPal order id and a statusPayPal's approved webhook
shop_orderA paid order: amount, currency, PayPal payment id, paid time, item countPayPal's completed webhook
shop_order_itemOne row per cart line, copied onto the ordercreate-order-items

The price is copied onto the line

A cart line stores the unit price when the product is added. Changing a product's price later does not change carts that already hold it, and the checkout charges what the customer saw.

What you will create

AssetKindWhat it does
add-to-cartLogic BlockAdds a product to a cart, opening one if no cart id is given, and returns the cart id, the cart and its lines
checkout-cartGlyph (code)Reads the cart and its lines, builds the PayPal items, adds up the total, starts the checkout and returns the PayPal link
capture-paypal-orderLogic BlockCaptures the approved PayPal order (this is when the money moves) and marks the approval and the cart
create-order-itemsLogic BlockCopies each cart line onto the order and marks the cart ordered
capture-paypal-order-on-approvalAutomationWhen a payment_approval record is created, runs the capture
create-order-items-on-orderAutomationWhen a shop_order record is created, runs create-order-items
PayPal connectorOrganization and tenant settingsAdopted once for the org; each tenant adds its own credentials, redirect pages and webhook
Webhook landingConnector settingSays which record each PayPal event creates or updates

Part A · Set up the PayPal connector

This is configuration, once per organization and once per tenant. The full PayPal-side walkthrough is in Payment connectors; this is what you do in nostackai.

1Adopt the connector (organization admin). In the console, open Org Settings → Connectors, choose Add connector and pick PayPal. Adopting makes it available to the organization; tenants still bring their own credentials.
2Enter the credentials (tenant). Open Tenant Settings → Connectors → PayPal. Choose *Test Mode*, then enter the Client ID and Client secret of your PayPal sandbox app. Copy the Inbound webhook URL from the same card into your PayPal app's webhook settings, subscribe it to CHECKOUT.ORDER.APPROVED, PAYMENT.CAPTURE.COMPLETED and PAYMENT.CAPTURE.DENIED, then paste the Webhook ID PayPal gives you back here.
3Set the redirect pages. Under *Checkout redirect URLs*, give each checkout action a success and a cancel page. Use full addresses that start with https://; PayPal rejects an address without one. These are where PayPal sends the customer back to.
Org Settings Connectors page showing PayPal adopted with five actions.
PayPal adopted for the organization (Org Settings → Connectors).
Tenant Settings PayPal card with Test Mode selected, masked client id, saved client secret and masked webhook id.
The tenant's PayPal card: environment, credentials (masked here) and the webhook URL to register at PayPal.
Checkout redirect URLs for subscription, one-time and cart checkouts.
Each checkout action has its own success and cancel page. Changing one does not change the others.

Credentials stay with the tenant

The client secret is stored encrypted for the tenant and is never shown again after saving. The screens in this case study mask the identifiers; use your own values.

Part B · Import the shop

The six records, three Logic Blocks and two automations are one file. Download [shop-bundle.json](/casestudies/ecommerce/shop-bundle.json), then in the console open Import Metadata, choose the file and review the preview. Every item is checked against your organization before anything is created, and a name that already exists is refused rather than overwritten.

Import Metadata preview listing six entities, three logic blocks and two events, all ready to import.
The preview: eleven items ready to import.

Choose Import selected. All eleven items are created.

Import Metadata result showing every item imported.
Imported: 11 of 11.

The records now appear in API Studio and the Logic Blocks in the Logic Blocks list.

API Studio entity list including cart, cart_line, payment_approval, product, shop_order and shop_order_item.
The shop's records in API Studio, next to the platform's own.
Logic Blocks list showing add-to-cart, capture-paypal-order and create-order-items.
The three Logic Blocks.

Prefer to build it by hand?

Each asset is listed with its fields and steps in E-commerce assets reference. The import and the hand-built version are identical.

Part C · The assets, one by one

Add to cart

The add-to-cart Logic Block takes a product id, an optional quantity and an optional cart id. With no cart id it opens a new cart for the signed-in customer; with one it reuses that cart. It then checks whether the product is already a line of the cart: if so it raises the quantity and the line total, otherwise it adds a line with the product's current price. Last, it recomputes the cart's item count and subtotal from the lines, so they cannot drift.

Logic Block builder showing the add-to-cart steps with two conditions.
The add-to-cart Logic Block in the builder: get the product, branch on whether a cart exists, then add or raise the line.

Checkout, as a Glyph

A Glyph is a small Python (or JavaScript) function you write once and run on the platform. This one is the only place the shop needs code, because PayPal wants each item as a name, a quantity and a price, and a Glyph builds that from the cart's lines in a few lines. It reads the cart, builds the items, adds up the total itself, starts the PayPal checkout, marks the cart as checkout started and returns the link.

To add it: in the console open Glyphs → New Glyph, name it checkout-cart, paste the code below (also in the downloads), add one input field cart_id (string, required) on the Input tab, save and Activate.

python
def handler(request, ctx):
    cart = ctx.app_db.get_entity("cart", request["cart_id"])
    lines = ctx.app_db.list_entities("cart_line", filters=[{"field": "cart_id", "op": "eq", "value": cart["id"]}],
                                  sort_by="created_at")["items"]
    items = [{"name": l["title"], "quantity": str(int(l["quantity"])),
              "unit_amount": {"currency_code": cart["currency"], "value": f"{float(l['unit_price']):.2f}"}} for l in lines]
    total = sum(int(i["quantity"]) * float(i["unit_amount"]["value"]) for i in items)
    out = ctx.connector.invoke("paypal", "checkout_cart",
                               {"line_items": items, "currency_code": cart["currency"], "total": f"{total:.2f}"},
                               entity_ref=cart["id"])
    ctx.app_db.update_entity("cart", cart["id"], {"status": "checkout_started"})
    return {"checkout_url": out["result"]["checkout_url"]}
Glyph Studio showing the checkout-cart Python code.
The checkout Glyph in Glyph Studio.

Why the cart id is the only input

The caller never sends prices. The Glyph reads them from your own records, so a customer cannot change what they are charged. The cart id is also passed to PayPal as the order's reference (entity_ref), and PayPal sends it back on every webhook: that is how the later events find this cart.

Capture

A one-time PayPal order is only *approved* when the customer returns from PayPal. No money has moved. The capture is the step that takes the payment. The capture-paypal-order Logic Block is one connector step (PayPal, *Capture payment*, with the order id) followed by two updates that mark the approval and the cart. An automation runs it the moment PayPal's approval arrives.

Logic Block builder with the capture connector step selected showing PayPal and the Capture payment action.
The capture Logic Block: a PayPal connector step, then two updates. The step needs only the PayPal order id.
Automation on payment_approval creation that runs the capture-paypal-order Logic Block.
The automation that runs the capture when a payment_approval record is created.

Order items

When PayPal reports the capture complete, a paid shop_order record is created (next section). The create-order-items automation then runs the Logic Block of the same name: it loads the order, lists the cart's lines, creates one shop_order_item for each, stores the item count on the order and marks the cart *ordered*.

How the webhooks land

PayPal sends signed webhooks to the tenant's webhook URL. nostackai verifies the signature, finds the tenant and the original cart from the reference you passed at checkout, ignores a repeated delivery, and then applies your webhook landing: a short map from each event to the record it creates or updates.

PayPal eventWhat it meansWhat your shop does
CHECKOUT.ORDER.APPROVEDThe customer approved the payment on PayPalCreate a payment_approval record (cart id, PayPal order id, status approved). Its creation starts the capture.
PAYMENT.CAPTURE.COMPLETEDThe money was takenCreate a shop_order (paid, amount, currency, payment id, paid time). Its creation starts create-order-items.
PAYMENT.CAPTURE.DENIEDThe capture failedSet the cart's status to payment_failed

You set the landing under Org Settings → Connectors → PayPal → Customize → Webhook. For each event choose the record, whether to create or update it, and the value of each field. The values are templates: {{ipaas_ref.entityRef}} is the cart id you passed at checkout, and {{resource.…}} reads PayPal's own payload.

Webhook landing editor for CHECKOUT.ORDER.APPROVED showing entity payment_approval, operation insert and three field templates.
The webhook landing for CHECKOUT.ORDER.APPROVED: insert a payment_approval with the cart id, the PayPal order id and a status.

The same landing as a file, if you prefer to apply it with the MCP tool: [webhook-landing.json](/casestudies/ecommerce/webhook-landing.json).

Sequence diagram from the customer approving at PayPal to the paid order, its items and the closed cart.
From payment to order: the customer approves, PayPal's two webhooks land as records, and the automations capture the payment and create the order items.

Approved is not paid

Treat PAYMENT.CAPTURE.COMPLETED as the moment an order is paid, never the approval. The approval is only a sign that the customer said yes.

Part D · Test it in the Playground

The Playground is a test client for your organization's own APIs. Everything below is what a storefront would call, run by hand. Sign in as an app user: this is a shopper, not the org admin. Register one on the Auth & Account card, then give the user the *memberacl* policy under Tenant Management → App Users → ACL so they can create records and run Logic Blocks. Point the Playground at your organization in Settings & Auth.

1Create three products. On the Entities (CRUD) card, choose *product* and Create with a name, title, description and price for each. Then List to see them with their ids.
2Add to cart. On the Logic Blocks card, choose *add-to-cart* and run it with a product id and a quantity. No cart id is sent, so a new cart comes back: keep its cart_id.
3Add more. Run it again with the same cart id and a different product, then once more with the first product: its line goes up instead of a new one appearing.
4Check out. Open the *checkout-cart* Glyph in the console, go to Test, send { "cart_id": "…" } against the active version and run it. A PayPal link comes back.
5Pay. Open the link, sign in with your PayPal sandbox Personal account and approve. PayPal returns to the success page you set in Part A.
6Look at the records. Back in the Playground, list *payment_approval*, *shop_order*, *shop_order_item* and *cart*.
Playground entity list showing three products with ids and prices.
Three products created and listed.
Playground running add-to-cart and returning a new cart id.
First call with no cart id: a new cart is opened and its id comes back.
Playground add-to-cart result showing the cart with item count 4 and subtotal 84.5.
Later calls reuse the cart: two products, four items, subtotal 84.50.
Glyph test run returning a PayPal sandbox checkout URL.
The checkout Glyph returns the PayPal link for the whole cart.
PayPal sandbox review page showing a payment of 84.50 dollars.
What the buyer sees on PayPal: the whole cart, $84.50. (Sandbox buyer details are blurred.)
The Playground opened after the PayPal redirect.
After paying, PayPal returns the customer to the success page you set.

What you should see

Within a few seconds of approving, three things have happened without anyone clicking anything: PayPal told nostackai the order was approved, the capture ran, and PayPal told nostackai the money was taken. The Debug Log shows each step.

Debug Log listing inbound PayPal webhook records, the capture-paypal-order run, an outbound PayPal response and the create-order-items run, all successful.
The Debug Log (Console → Debug Log): PayPal's webhooks arriving, the capture Logic Block and the PayPal call it made, then the create-order-items run.
Playground list of payment_approval showing status captured.
payment_approval: the PayPal order id and status captured.
Playground list of shop_order showing amount 84.5, status paid, item_count 4.
shop_order: paid, 84.50 USD, four items, with PayPal's payment id.
Playground list of shop_order_item with two rows.
shop_order_item: one row per cart line (three tote bags and one mug).
Playground list of cart showing status ordered.
The cart is now ordered, so it cannot be paid twice.

That is the whole loop: a cart, one payment, a paid order, its items and a closed cart, from six records, three Logic Blocks, two automations and ten lines of code.

Going to production

  • Live credentials. Switch the tenant's environment to *Live*, enter the Live app's credentials, and register a second webhook in PayPal's live dashboard with the same events. The Webhook ID is different for each.
  • Prices stay on the server. Keep the checkout reading prices from your own cart. If a browser can send prices, a browser can lower them.
  • Retries. PayPal may deliver a webhook more than once. nostackai ignores a repeated delivery, so an order is not created twice; keep your own automations safe to run again too.
  • Roles. The shopper here has the standard member role. Give customers only the access they need, and keep org-admin tools away from them.
  • Not in this study. Refunds, shipping and tax lines, stock levels, discount codes, and subscriptions. Subscriptions use PayPal's checkout action with a plan, not a cart; see Payment connectors.

Adapting it to another provider

The shop is built from the same pieces whichever provider takes the money: a connector, a webhook landing and the same records. What changes is the checkout call and the event that means paid. E-commerce assets reference lists, for Stripe, Paddle and Dodo, the action to call, the events to land and whether a separate capture step exists (PayPal needs one; the others complete the payment at checkout).

What to take from this

  • It is configuration first. The connector, the landing and the automations are settings; the one piece of code is the part that is naturally code.
  • Your records stay yours. The cart, the order and the items are ordinary entities you can query, extend and report on.
  • Webhooks drive the shop. Nothing polls PayPal. A verified event creates a record, and a record creates the next step.
  • You can test every step by hand. Each asset runs on its own in the Playground or the console before they work together.