What the app is#
Commerce is one HubSpot app doing three jobs that share a product catalogue, a pricing engine and a payment layer: it quotes to businesses, sells to consumers, and takes the money for both — and for anything else on the portal that needs to charge for something.
| Kind | Private HubSpot app (platform 2026.03), installed per portal |
| The three parts | CPQ — configure/price/quote for B2B · eCommerce — catalogue, cart and checkout for B2C · Payment platform — the provider seam both use, and the one other apps buy through |
| Stores data in | HubSpot's own commerce objects wherever they exist — Products, Line Items, Quotes, Orders, Carts, Invoices, Payments — plus a small number of custom objects for what HubSpot has no native home for |
| Surface count | 59 HTTP endpoints declared in the project at the verified date above |
| How to get it | Not distributed. Ask for a walkthrough: w@reus.ie |
Note
This page is Commerce's permanent entry point — the URL will not change. It is deliberately an overview plus the one contract outsiders actually need; the internals (provider modules, the payments mirror, the subscription lifecycle) are project documentation and are not published here.
The payment contract#
This is the part of Commerce another app touches, and the reason it exists as a platform rather than a checkout page. The division of labour is the whole design:
Money is Commerce's job. Fulfilment is yours.
You create an Order, hand the buyer to Commerce's checkout, and get told when the money moved. What that means — issue a ticket, mint a voucher, unlock a download, start a subscription — is yours alone. Commerce never touches your records, and you never talk to a payment provider, never hold a key, and never interpret a provider's status vocabulary.
Six steps. Every consumer is the same shape; only the last one differs.
| Step | What you do | |
|---|---|---|
| 1 | Create an Order | POST /hs/serverless/create-order with the buyer's contactId, your own app name as externalSource, and your own correlation id as externalRef. Add line items where you want a real itemised order. Server-side only — the order total is what gets charged |
| 2 | Create the payment | POST /hs/serverless/create-payment with the orderId, a redirectUrl for the buyer and, optionally, a callbackUrl for your backend. You get back a hosted checkout URL |
| 3 | Send the buyer | Redirect to that URL. The provider, the card form and the compliance surface are Commerce's problem from here |
| 4 | Verify | When the buyer returns, ask Commerce whether the money moved. Verify by outcome bucket, never by a raw provider status string — provider vocabularies differ and change |
| 5 | React to the callback | The signed completion callback is a poke, not a source of truth: it tells you to go and check, and step 4 is what you believe. Treat it as optional — it can be missed, duplicated or late |
| 6 | Fulfil | Your side. Idempotent, keyed on your own externalRef, and safe to run twice — because steps 4 and 5 can both fire |
Full request and response shapes, the bucket table, and the reconciliation job are in backend/payments-consuming.md in our documentation corpus — reachable over the Docs MCP, or ask and we will send it. The complete endpoint inventory lives alongside it in commerce-endpoints.md, generated from the app's own manifests; it is a working document for people building on Commerce rather than buying through it, which is why it stays in the corpus rather than appearing here.
Providers#
The provider seam is a module contract, so adding a payment service provider is a bounded piece of work rather than a rewrite. Status is not uniform, and the difference matters:
| Provider | Status |
|---|---|
| Mollie | Live-verified — the provider the platform was proven against end to end |
| Stripe | Implemented, not yet live-verified. The webhook endpoint is not registered automatically; it must be added in the Stripe dashboard |
| Buckaroo | Implemented, not yet live-verified. Two keys per mode, and test and live are different hosts |
Simulator (mock) | Provider-independent testing, and the only way to produce a chargeback on demand |
"Implemented, not yet live-verified" means the code is written and unit-tested and the runbook exists, but no real money has moved through it. Run the runbook before pointing anything real at one.
Surfaces#
| Surface | Where | What it is |
|---|---|---|
| CPQ quote modules | Quote templates | The B2B quote a customer receives — configurable modules, not a fixed template. Documented separately: CPQ quote modules |
| eCommerce module | Website pages | Catalogue, cart and checkout for B2C |
| Commerce app page | HubSpot, Marketplace menu → Commerce | The operator cockpit — revenue KPIs and trends, the work queue, and the payment-platform status tile |
| Settings | HubSpot app settings | Provider credentials and mode, the voucher provider, and the per-portal configuration below |
Integration seams#
Commerce is one of three of our apps that share a portal. Both seams below run through the payment contract or a public endpoint — no shared code, no privileged lane.
| With | What crosses |
|---|---|
| Events | Paid tickets. Events is a payment consumer exactly as described above: it creates the Order with externalSource: "events", and issues tickets on its own side once the money is confirmed. Commerce never knows what a ticket is |
| Keyring | Vouchers at checkout. A shopper types a Keyring gift-card or voucher code into the cart; Commerce validates it, recomputes the discount from the live cart server-side, and spends it against Keyring only when the order genuinely reaches paid. Commerce integrates with Keyring exactly as an external point-of-sale system would — see keyring-integration.md |
Neither is required, and both ship switched off.
Conventions and gotchas#
- Verify by bucket, never by raw status. Provider status vocabularies are not portable and do change under you. This is the single rule most likely to be broken by a consumer reading a provider's own documentation instead of this contract.
- One payment settles exactly one invoice. A HubSpot invoice shows as paid because an associated Payment record rolls up into it, and that association is one-to-one. An order that is expected to produce both a receipt invoice and a subscription invoice will not get both from a single payment — which is why a paid order carrying a recurring line item deliberately gets no receipt invoice.
- A recurring line item leaves a native HubSpot Subscription behind. HubSpot then generates each cycle's invoice and charges nobody — collection for later cycles is not automated. Verified on a live portal; treat any claim that it "just bills" as wrong.
- The search index lags writes. A record created a moment ago may not be findable by search yet. Read back by id, or drive lists by
hs_createdatecursor rather than search, whenever correctness depends on seeing what you just wrote. - Every serverless response body must be
JSON.stringify(...). Returning a plain object produces an empty HTTP 200 with no error anywhere — a platform behaviour, not a Commerce one, and it has cost real time on every app we have built. - Per-portal configuration is genuinely per-portal. Provider credentials, the portal's own public domain, and any host the app is allowed to call are set per install. A setting that looks complete can still be dead if it points at a record that has since been archived — which is why the settings surface has connection tests that mint a real session rather than checking that fields are non-empty.
Verifying it works#
A green settings page is not evidence. In order, on the portal you care about:
- Open the Commerce app page (Marketplace menu → Commerce) and confirm the payment-platform tile reports a configured provider and mode.
- Run a payment through the simulator (
mockprovider) end to end: create an order, complete the checkout, and confirm your own fulfilment ran exactly once. - Confirm the order shows as paid in HubSpot — not only in the app — by finding the Payment record associated with the invoice.
- Produce a chargeback in the simulator and confirm a dispute case appears. This is the path that is never exercised by a happy-path test and is the one most likely to be broken.
- Only then switch the provider to a real one, in test mode, and repeat steps 2–4 with the provider's own test credentials.