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.

KindPrivate HubSpot app (platform 2026.03), installed per portal
The three partsCPQ — 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 inHubSpot'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 count59 HTTP endpoints declared in the project at the verified date above
How to get itNot 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.

StepWhat you do
1Create an OrderPOST /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
2Create the paymentPOST /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
3Send the buyerRedirect to that URL. The provider, the card form and the compliance surface are Commerce's problem from here
4VerifyWhen 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
5React to the callbackThe 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
6FulfilYour 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:

ProviderStatus
MollieLive-verified — the provider the platform was proven against end to end
StripeImplemented, not yet live-verified. The webhook endpoint is not registered automatically; it must be added in the Stripe dashboard
BuckarooImplemented, 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#

SurfaceWhereWhat it is
CPQ quote modulesQuote templatesThe B2B quote a customer receives — configurable modules, not a fixed template. Documented separately: CPQ quote modules
eCommerce moduleWebsite pagesCatalogue, cart and checkout for B2C
Commerce app pageHubSpot, Marketplace menu → CommerceThe operator cockpit — revenue KPIs and trends, the work queue, and the payment-platform status tile
SettingsHubSpot app settingsProvider 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.

WithWhat crosses
EventsPaid 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
KeyringVouchers 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_createdate cursor 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:

  1. Open the Commerce app page (Marketplace menu → Commerce) and confirm the payment-platform tile reports a configured provider and mode.
  2. Run a payment through the simulator (mock provider) end to end: create an order, complete the checkout, and confirm your own fulfilment ran exactly once.
  3. Confirm the order shows as paid in HubSpot — not only in the app — by finding the Payment record associated with the invoice.
  4. 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.
  5. Only then switch the provider to a real one, in test mode, and repeat steps 2–4 with the provider's own test credentials.